diff --git a/.agentic-ci/config.yml b/.agentic-ci/config.yml index 964dfc04e2..9e42844040 100644 --- a/.agentic-ci/config.yml +++ b/.agentic-ci/config.yml @@ -4,21 +4,27 @@ # https://github.com/opendatahub-io/agentic-ci, docs/sandbox-profiles.md). # autofix reads it from the base branch before the agent runs. An overlay # may set toolchains, setup, validate, skips and env, and open only the -# goproxy, npm and pypi egress presets; anything wider is configured -# centrally in autofix. +# goproxy, npm, pypi and crates egress presets; anything wider is +# configured centrally in autofix. # # Toolchains come from the agentic-ci catalog at the versions pinned in -# mise.toml (python reads .python-version). If an upstream sync bumps a +# mise.toml (python reads .python-version, rust reads rust-toolchain.toml). If an upstream sync bumps a # version in mise.toml, bump it here too: mise does not install it, so # the validate steps fail until the pins match. Setup installs mise from # npm, links the catalog toolchains into mise so it never downloads -# them, and installs the mise npm/go tools through the presets. Rust and -# container, cluster and VM tasks cannot run in the sandbox and are -# declared as skips. +# them, and installs the mise npm/go tools through the presets. It also +# fetches the crates of every tracked Cargo.lock through the crates +# preset, so the Rust checks build offline, and runs rust:lint once to +# warm the build cache, so clippy after the agent only rebuilds what it +# changed. mise leaves rust alone (MISE_DISABLE_TOOLS) and the catalog's +# cargo comes first on PATH. +# cargo-deny, nextest and the container, cluster and VM tasks cannot run +# in the sandbox and are declared as skips. sandbox: toolchains: python: auto # reads .python-version + rust: auto # reads rust-toolchain.toml node: "24.15.0" go: "1.26.7" buf: "1.72.0" @@ -28,6 +34,7 @@ sandbox: - goproxy - npm - pypi + - crates env: MISE_YES: "1" @@ -62,12 +69,52 @@ sandbox: mise link "buf@$(buf --version)" "$(root buf)" mise link "helm@$(helm version --template '{{.Version}}' | sed 's/^v//')" \ "$(dirname "$(command -v helm)")" - mise install \ - npm:markdownlint-cli2 \ - go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ - go:google.golang.org/protobuf/cmd/protoc-gen-go \ - go:google.golang.org/grpc/cmd/protoc-gen-go-grpc \ - go:golang.org/x/tools/cmd/goimports + # A few go module fetches can fail at once when the sandbox's DNS + # relay times out; mise install is idempotent, so retry it. + ok="" + for attempt in 1 2 3; do + if mise install \ + npm:markdownlint-cli2 \ + go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ + go:google.golang.org/protobuf/cmd/protoc-gen-go \ + go:google.golang.org/grpc/cmd/protoc-gen-go-grpc \ + go:golang.org/x/tools/cmd/goimports; then + ok=1 + break + fi + echo "mise install failed (attempt $attempt)" >&2 + sleep 15 + done + test -n "$ok" + + - name: rust-crates + timeout: 1800 + run: | + set -euo pipefail + # mise.toml sets RUSTC_WRAPPER=sccache for every `mise run`, and + # sccache is a GitHub download no preset opens. A pass-through + # wrapper runs rustc directly, without a cache. + mkdir -p "$HOME/.local/bin" + printf '#!/bin/sh\nexec "$@"\n' > "$HOME/.local/bin/sccache" + chmod +x "$HOME/.local/bin/sccache" + git ls-files -z -- ':(glob)**/Cargo.lock' | + while IFS= read -r -d '' lockfile; do + cargo fetch --locked --manifest-path "${lockfile%Cargo.lock}Cargo.toml" + done + + - name: rust-warm + timeout: 3600 + run: | + set -uo pipefail + # Build once before the agent with the exact rust:lint commands, so + # the agent's own clippy runs and the rust-lint validate step only + # rebuild what changed. A lint failure on the base commit does not + # matter here: the build cache is what this step is for. + status=0 + mise run rust:lint || status=$? + if [ "$status" -ne 0 ]; then + echo "rust:lint exited $status during the warm-up; continuing, the build cache is what this step is for" + fi validate: - {name: python-lint, kind: lint, run: mise run python:lint, timeout: 600} @@ -77,13 +124,22 @@ sandbox: - {name: helm-lint, kind: lint, run: mise run helm:lint, timeout: 600} - {name: go-format, kind: lint, run: mise run go:format:check, timeout: 600} - {name: go-lint, kind: lint, run: mise run go:lint, timeout: 900} + - {name: rust-format, kind: lint, run: mise run rust:format:check, timeout: 600} + - {name: rust-lint, kind: lint, run: mise run rust:lint, timeout: 3600} skips: - - match: "rust:*, cargo, clippy, nextest, cargo-deny" - reason: "No Rust toolchain or C compiler in the sandbox, so Rust builds, tests, lints and dependency checks rely on CI" + - match: "rust:deny, cargo deny, test:rust, cargo nextest, rust:verify:*" + reason: "cargo-deny and nextest are not in the toolchain catalog, and the Rust tests and verify scripts are left to CI" - match: "docker:*, gateway:*, sandbox:*, vm:*, package:*" reason: "Image, VM and package builds need a container runtime or privileged features the sandbox blocks" - match: "e2e:*, test:*, helm:test" reason: "End-to-end and cluster tests need a container runtime, a running gateway or a live cluster" - match: "license:check" reason: "Fails on main today: midstream files such as .tekton/*.yaml have no SPDX header" + + # Build output stays in the sandbox and is never downloaded back. + discard_before_download: + - target + - e2e/rust/target + - examples/governance-interceptor/target + - examples/supervisor-middleware-content-guard/target diff --git a/.agents/skills/helm-dev-environment/SKILL.md b/.agents/skills/helm-dev-environment/SKILL.md index 450d23094d..9fdf315c3c 100644 --- a/.agents/skills/helm-dev-environment/SKILL.md +++ b/.agents/skills/helm-dev-environment/SKILL.md @@ -236,8 +236,9 @@ cd ../../.. KUBECONFIG=kubeconfig mise run helm:gateway:apply ``` -`values-gateway.yaml` creates a `Gateway` (listener on port 80, class `eg`) and -`GRPCRoute` in the `openshell` namespace. The `high-availability` profile +`values-gateway.yaml` creates the `openshell` `Gateway` (listener on port 80, +class `eg`) and an `openshell` `GRPCRoute` in the `openshell` namespace. The +`high-availability` profile installs the Envoy Gateway Helm chart and layers both `values-high-availability.yaml` and `values-gateway.yaml` onto the OpenShell release. diff --git a/.agents/skills/test-release-canary/SKILL.md b/.agents/skills/test-release-canary/SKILL.md index 1f6965873c..ed5491e87f 100644 --- a/.agents/skills/test-release-canary/SKILL.md +++ b/.agents/skills/test-release-canary/SKILL.md @@ -143,7 +143,7 @@ Loopback registration auto-derives the gateway name to `openshell` if `--name` i | `macos`/`ubuntu-deb`/`fedora` job fails on `install.sh` | Dev release missing an asset, checksum mismatch, or `install.sh` regression on this branch. | Job log around the `curl … install.sh \| sh` step. | | Sandbox create or exec fails | Published sandbox and supervisor artifacts are missing, incompatible, or cannot establish the protected runtime channel. | Gateway logs plus Docker, Podman, VM, Snap, or Kubernetes runtime diagnostics for the job. | | `macos`/`ubuntu-deb`/`fedora` job fails on `openshell status` | Local gateway service did not start (systemd/brew/podman). Often a driver issue. | Service logs in the job log; `OPENSHELL_COMPUTE_DRIVER` env in the "Ensure …" step. | -| `ubuntu-snap-system-docker` fails during `install.sh` | System Docker was unavailable, the edge revision or automatic interfaces were unavailable, or the gateway did not become reachable. | Failure diagnostics dump system Docker, snap service/connection/change state, gateway and snapd journals, snap logs, and port 17670 listeners. | +| `ubuntu-snap-system-docker` fails during `install.sh` | System Docker was unavailable to the runner user, the edge revision or automatic interfaces were unavailable, or the user gateway did not become reachable. | Failure diagnostics dump system Docker, snap service/connection/change state, user and legacy gateway journals, snap logs, and port 17670 listeners. | | `ubuntu-snap-system-docker` fails during the prover checks | The prover artifact is missing or packaged for the wrong architecture, `openshell.prover` is not exposed or confined to read the test policies, or its solver linkage is not runnable. | The `Verify Snap installation` and `Check a policy boundary with the Snap prover` steps, plus `snap info openshell` and `snap connections openshell`. | | `ubuntu-snap-docker-preflight` unexpectedly succeeds | The installer no longer fails before installing the OpenShell snap when Docker is absent or supplied by the Docker snap. | Inspect `install.log`, `docker-snap.log`, `snap list`, and snapd changes. | | `kubernetes` job fails on `helm install --wait` | Chart did not deploy in 5 min — usually image pull failure or readiness probe failing. | "Diagnostics on failure" step dumps `helm status`, manifest, pod describe, pod logs. | diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4d32b414c1..14f2e5ef61 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,35 @@ -# Broad ownership — core team reviews everything +## Broad ownership — core team reviews everything * @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr -# Vouch list — maintainers only (bot commits bypass, but manual edits need review) +## Vouch list — maintainers only (bot commits bypass, but manual edits need review) .github/VOUCHED.td @NVIDIA/openshell-codeowners + +## Merge enforcement — these paths decide who can merge, so they notify the full +# list of maintainers to raise scrutiny on changes here. +# +# PR checks run local to the branch, so unless the check specifically avoids +# using branch-local files, a PR could change its own gates and appear to pass +# them. Covering the required checks here closes that gap. +# +# The whole workflows directory is covered, not just the approval gate, because +# any workflow can claim a required check context or ask for a broader token. +# +# Normally, we'd use a GitHub team to cover all maintainers, but org-level +# policies prevent outside collaborators from being on a GH team at this time +# so we spell them out explicitly. + +# Core check mechanisms +/.github/workflows/ @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/.github/actions/ @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/.github/CODEOWNERS @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/.github/zizmor.yml @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr + +# `MAINTAINERS.md` maintainer approval checks +/MAINTAINERS.md @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/tasks/scripts/check_maintainer_approval.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/tasks/scripts/alert_maintainer_change.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/tasks/scripts/check_maintainer_approval_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr +/tasks/scripts/alert_maintainer_change_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr + +# Auditable SBOM check +/tasks/scripts/verify-image-sbom.sh @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr diff --git a/.github/workflows/integration-runner.yml b/.github/workflows/integration-runner.yml index 89cc0ca920..4be954eced 100644 --- a/.github/workflows/integration-runner.yml +++ b/.github/workflows/integration-runner.yml @@ -89,3 +89,12 @@ jobs: # Retry dependency preparation, then execute the test suite once. nix build --no-link .#tmachine || nix build --no-link .#tmachine nix run .#tmachine -- test "${ENVIRONMENT}" "${INSTALLER}" "${TESTSUITE}" + + - name: Upload tmachine diagnostics + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: tmachine-diagnostics-${{ inputs.category }}-${{ matrix.environment }}-${{ matrix.installer }}-${{ matrix.testsuite }} + path: artifacts/tmachine-diagnostics + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/maintainer-approval.yml b/.github/workflows/maintainer-approval.yml new file mode 100644 index 0000000000..f478ed97ac --- /dev/null +++ b/.github/workflows/maintainer-approval.yml @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Maintainer Approval + +on: + merge_group: + types: [checks_requested] + pull_request_review: + types: [submitted, dismissed] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + maintainer-approval: + # This name is the required status check context. Changing it silently + # breaks the ruleset entry that gates merges on this job. + name: OpenShell / Maintainer Approval + if: github.repository_owner == 'NVIDIA' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + # Check out the default branch, never the pull request head. Both the + # maintainer list and the decision helper must come from main: reading + # either from the pull request ref would let a contributor add themselves + # to the list, or rewrite the decision logic, and self-approve. + - name: Check out the maintainer list and helper + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + sparse-checkout: | + MAINTAINERS.md + tasks/scripts/check_maintainer_approval.py + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Require an approving review from a maintainer + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + # merge_group carries no pull request in its payload, but the queue + # branch names the entry: gh-readonly-queue/main/pr-3027-. + MERGE_GROUP_REF: ${{ github.ref_name }} + shell: bash + run: | + set -euo pipefail + + if [ -z "${PR_NUMBER:-}" ]; then + if [[ "$MERGE_GROUP_REF" =~ /pr-([0-9]+)-[0-9a-f]+$ ]]; then + PR_NUMBER="${BASH_REMATCH[1]}" + else + # Fail closed: an unrecognised ref must never satisfy the gate. + echo "::error::No pull request resolved from '$MERGE_GROUP_REF'." + exit 1 + fi + fi + + gh api --paginate "repos/$GH_REPO/pulls/$PR_NUMBER/reviews" --jq '.[]' \ + | jq -s '.' > reviews.json + + # Exits non-zero when no maintainer's latest decisive review is an + # approval, and when MAINTAINERS.md yields no handles. That exit code + # is the check result; nothing is posted anywhere. + python3 tasks/scripts/check_maintainer_approval.py \ + --maintainers MAINTAINERS.md \ + --reviews reviews.json diff --git a/.github/workflows/maintainers-change-alert.yml b/.github/workflows/maintainers-change-alert.yml new file mode 100644 index 0000000000..d04052d51b --- /dev/null +++ b/.github/workflows/maintainers-change-alert.yml @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Maintainers Change Alert + +on: + pull_request_target: + types: [opened, reopened, synchronize] + paths: + - MAINTAINERS.md + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + describe-change: + name: Comment on the approver set change if MAINTAINERS.md has changed + if: github.repository_owner == 'NVIDIA' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + # Default branch only. The helper must be the reviewed version, not + # whatever the pull request happens to contain. + - name: Check out the change-alert helper + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + sparse-checkout: tasks/scripts/alert_maintainer_change.py + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Post the maintainer delta + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # Single source of truth for the comment marker: the script emits + # it as the first line of the body, the lookup below matches on it. + COMMENT_MARKER: "" + shell: bash + run: | + set -euo pipefail + + # Fetching file contents is reading data, not executing it. The head + # revision is never checked out or run. + # + # A 404 means the file genuinely does not exist at that revision — a + # pull request that adds or deletes MAINTAINERS.md — and yields an + # empty side of the comparison. Every other failure is fatal: an empty + # file from a rate limit or a 5xx would render as "every maintainer + # was just added" or "nothing changed", both of which mislead the + # reviewer about who can merge code. + fetch_maintainers() { + local ref="$1" out="$2" err + err="$(mktemp)" + if gh api -H "Accept: application/vnd.github.raw" \ + "repos/$GH_REPO/contents/MAINTAINERS.md?ref=$ref" > "$out" 2>"$err"; then + rm -f "$err" + return 0 + fi + if grep -q 'HTTP 404' "$err"; then + rm -f "$err" + : > "$out" + return 0 + fi + echo "::error::Could not fetch MAINTAINERS.md at $ref" + cat "$err" >&2 + rm -f "$err" + return 1 + } + + fetch_maintainers "$BASE_SHA" before.md + fetch_maintainers "$HEAD_SHA" after.md + + # An unparseable result exits non-zero, but the comment explaining + # why still has to be posted before this job fails. + status=0 + python3 tasks/scripts/alert_maintainer_change.py \ + --before before.md --after after.md > body.md || status=$? + + # No output means the approver set did not change. A comment saying + # so is noise, so post nothing. + if [ -s body.md ]; then + cat body.md >> "$GITHUB_STEP_SUMMARY" + + # Update the existing comment rather than stacking one per push. + COMMENT_ID=$(gh api --paginate "repos/$GH_REPO/issues/$PR_NUMBER/comments" \ + --jq '.[] | select(.body | startswith($ENV.COMMENT_MARKER)) | .id' \ + | head -n 1) + + if [ -n "$COMMENT_ID" ]; then + gh api --method PATCH "repos/$GH_REPO/issues/comments/$COMMENT_ID" \ + -F "body=@body.md" >/dev/null + else + gh api --method POST "repos/$GH_REPO/issues/$PR_NUMBER/comments" \ + -F "body=@body.md" >/dev/null + fi + fi + + exit "$status" diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index 286d685672..a989d10e84 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -221,9 +221,10 @@ jobs: - name: Install and check status run: | set -euo pipefail - sudo systemctl set-environment \ + systemctl --user set-environment \ "OPENSHELL_TELEMETRY_ENABLED=${OPENSHELL_TELEMETRY_ENABLED}" - curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | sh + curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/${{ github.event.workflow_run.head_sha || github.sha }}/install.sh | + DOCKER_HOST=unix:///tmp/nonexistent-docker.sock DOCKER_CONTEXT=nonexistent sh sudo snap list openshell if sudo snap list docker >/dev/null 2>&1; then echo "install.sh unexpectedly installed the Docker snap" >&2 @@ -234,8 +235,12 @@ jobs: sudo snap connections openshell | grep -E '^docker +openshell:docker +:docker +' openshell --version openshell.prover --version - sudo snap services openshell - sudo journalctl -b -u snap.openshell.gateway.service --no-pager | + snap services openshell + systemctl --user is-enabled --quiet snap.openshell.user-gateway.service + systemctl --user is-active snap.openshell.user-gateway.service + ! systemctl is-enabled --quiet snap.openshell.system-gateway.service + ! systemctl is-active --quiet snap.openshell.system-gateway.service + journalctl --user -u snap.openshell.user-gateway.service --no-pager | grep -F "mTLS user authentication enabled" openshell gateway list | grep -F "https://127.0.0.1:17670" openshell status @@ -280,10 +285,12 @@ jobs: sudo snap services openshell sudo snap connections openshell sudo snap changes - sudo systemctl status snap.openshell.gateway.service --no-pager - sudo journalctl -b -u snap.openshell.gateway.service --no-pager -n 300 + systemctl --user status snap.openshell.user-gateway.service --no-pager + journalctl --user -u snap.openshell.user-gateway.service --no-pager -n 300 + sudo systemctl status snap.openshell.system-gateway.service --no-pager + sudo journalctl -b -u snap.openshell.system-gateway.service --no-pager -n 300 sudo journalctl -b -u snapd.service --no-pager -n 300 - sudo snap logs openshell.gateway -n=300 + snap logs openshell.user-gateway -n=300 sudo ss -ltnp '( sport = :17670 )' ubuntu-snap-docker-preflight: diff --git a/.github/workflows/required-ci-gates.yml b/.github/workflows/required-ci-gates.yml index 2310d27efe..aee374585d 100644 --- a/.github/workflows/required-ci-gates.yml +++ b/.github/workflows/required-ci-gates.yml @@ -285,5 +285,8 @@ jobs: if [ "$CONTEXT_KIND" != "merge_group" ]; then evaluate_workflow "OpenShell / E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e" "Core E2E result" evaluate_workflow "OpenShell / GPU E2E" "branch-e2e.yml" "Branch E2E Checks" "test:e2e-gpu" "GPU E2E result" + else + post_status "OpenShell / E2E" success "Not applicable: E2E runs only on PRs" "$STATUS_TARGET_URL" + post_status "OpenShell / GPU E2E" success "Not applicable: GPU E2E runs only on PRs" "$STATUS_TARGET_URL" fi post_status "OpenShell / Helm Lint" success "Disabled in downstream fork (job requires runners unavailable downstream)" "$STATUS_TARGET_URL" diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 7b1e035b77..27a89f878e 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -8,6 +8,7 @@ rules: # head code. Keep each suppression scoped to its reviewed trigger block. - dco.yml:3 - e2e-label-help.yml:13 + - maintainers-change-alert.yml:6 - release-canary.yml:3 - required-ci-gates.yml:3 - vouch-check.yml:3 diff --git a/.tekton/odh-openshell-cli-pull-request.yaml b/.tekton/odh-openshell-cli-pull-request.yaml index 0f0d68c786..41a6f2ba51 100644 --- a/.tekton/odh-openshell-cli-pull-request.yaml +++ b/.tekton/odh-openshell-cli-pull-request.yaml @@ -7,12 +7,14 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "true" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-event: '[pull_request]' pipelinesascode.tekton.dev/on-comment: '^/build-konflux' - pipelinesascode.tekton.dev/on-target-branch: '[{{target_branch}}]' + build.appstudio.redhat.com/pull_request_number: "{{pull_request_number}}" + pipelinesascode.tekton.dev/on-cel-expression: |- + event == "pull_request" + && (files.all.exists(p, ! p.matches('^\\.tekton/')) || ".tekton/odh-openshell-cli-pull-request.yaml".pathChanged()) labels: - appstudio.openshift.io/application: automation - appstudio.openshift.io/component: pull-request-pipelines-odh-openshell-cli + appstudio.openshift.io/application: rhoai-v3-6 + appstudio.openshift.io/component: odh-openshell-cli-v3-6 pipelines.appstudio.openshift.io/type: build name: odh-openshell-cli-on-pull-request-{{pull_request_number}} namespace: rhoai-tenant @@ -23,31 +25,44 @@ spec: - name: revision value: '{{revision}}' - name: output-image - value: quay.io/rhoai/pull-request-pipelines:odh-openshell-cli-{{revision}} + value: quay.io/rhoai/odh-openshell-cli-rhel9:on-pr-{{pull_request_number}} - name: dockerfile value: deploy/docker/Dockerfile.konflux.cli - name: path-context value: . - name: image-expires-after - value: 5d + value: 30d - name: enable-slack-failure-notification value: "false" - - name: build-platforms value: - linux/x86_64 - linux-m2xlarge/arm64 + - name: rhoai-version + value: "3.6.0" + - name: hermetic + value: true + - name: fips-check-blocking ##OCPEXCEPT-75 || OCPEXCEPT-76 || https://redhat-internal.slack.com/archives/C0C3BM63VFW/p1790927698394039?thread_ts=1790278918.493159&cid=C0C3BM63VFW + value: false + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "rpm", "path": "deploy/konflux/cli"}, + {"type": "generic", "path": "deploy/konflux/cli", "lockfile": "generic-fetcher.yaml"} + ] + pipelineRef: params: - name: url value: https://github.com/red-hat-data-services/konflux-central.git - name: revision - value: '{{ target_branch }}' + value: '{{ cel: pac.target_branch.matches("^rhoai-\\d+\\.\\d+(-ea\\.\\d+)?$") ? pac.target_branch : "main" }}' - name: pathInRepo value: pipelines/multi-arch-container-build.yaml resolver: git taskRunTemplate: - serviceAccountName: build-pipeline-pull-request-pipelines + serviceAccountName: build-pipeline-odh-openshell-cli-v3-6 podTemplate: imagePullSecrets: - name: redhat-appstudio-staginguser-pull-secret diff --git a/.tekton/odh-openshell-e2e-pull-request.yaml b/.tekton/odh-openshell-e2e-pull-request.yaml index 7b1f6a586e..fda2b683ab 100644 --- a/.tekton/odh-openshell-e2e-pull-request.yaml +++ b/.tekton/odh-openshell-e2e-pull-request.yaml @@ -39,6 +39,7 @@ spec: [ {"type": "cargo", "path": "."}, {"type": "cargo", "path": "e2e/rust"}, + {"type": "pip", "path": "odh", "requirements_files": ["requirements.txt"], "binary": {"packages": ":all:", "arch": "x86_64,aarch64", "os": "linux", "py_version": 311, "py_impl": "cp"}}, {"type": "rpm", "path": "deploy/konflux/e2e-odh"}, {"type": "generic", "path": "deploy/konflux/e2e-odh", "lockfile": "generic-fetcher.yaml"} ] diff --git a/.tekton/odh-openshell-e2e-push.yaml b/.tekton/odh-openshell-e2e-push.yaml index 3bcfb7c17f..13bfb21c07 100644 --- a/.tekton/odh-openshell-e2e-push.yaml +++ b/.tekton/odh-openshell-e2e-push.yaml @@ -40,6 +40,7 @@ spec: [ {"type": "cargo", "path": "."}, {"type": "cargo", "path": "e2e/rust"}, + {"type": "pip", "path": "odh", "requirements_files": ["requirements.txt"], "binary": {"packages": ":all:", "arch": "x86_64,aarch64", "os": "linux", "py_version": 311, "py_impl": "cp"}}, {"type": "rpm", "path": "deploy/konflux/e2e-odh"}, {"type": "generic", "path": "deploy/konflux/e2e-odh", "lockfile": "generic-fetcher.yaml"} ] diff --git a/.tekton/odh-openshell-gateway-pull-request.yaml b/.tekton/odh-openshell-gateway-pull-request.yaml index 15cacaa376..61e535c5d5 100644 --- a/.tekton/odh-openshell-gateway-pull-request.yaml +++ b/.tekton/odh-openshell-gateway-pull-request.yaml @@ -7,12 +7,14 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "true" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-event: '[pull_request]' pipelinesascode.tekton.dev/on-comment: '^/build-konflux' - pipelinesascode.tekton.dev/on-target-branch: '[{{target_branch}}]' + build.appstudio.redhat.com/pull_request_number: "{{pull_request_number}}" + pipelinesascode.tekton.dev/on-cel-expression: |- + event == "pull_request" + && (files.all.exists(p, ! p.matches('^\\.tekton/')) || ".tekton/odh-openshell-gateway-pull-request.yaml".pathChanged()) labels: - appstudio.openshift.io/application: automation - appstudio.openshift.io/component: pull-request-pipelines-odh-openshell-gateway + appstudio.openshift.io/application: rhoai-v3-6 + appstudio.openshift.io/component: odh-openshell-gateway-v3-6 pipelines.appstudio.openshift.io/type: build name: odh-openshell-gateway-on-pull-request-{{pull_request_number}} namespace: rhoai-tenant @@ -23,31 +25,44 @@ spec: - name: revision value: '{{revision}}' - name: output-image - value: quay.io/rhoai/pull-request-pipelines:odh-openshell-gateway-{{revision}} + value: quay.io/rhoai/odh-openshell-gateway-rhel9:on-pr-{{pull_request_number}} - name: dockerfile value: deploy/docker/Dockerfile.konflux.gateway - name: path-context value: . - name: image-expires-after - value: 5d + value: 30d - name: enable-slack-failure-notification value: "false" - - name: build-platforms value: - linux/x86_64 - linux-m2xlarge/arm64 + - name: rhoai-version + value: "3.6.0" + - name: hermetic + value: true + - name: fips-check-blocking ##OCPEXCEPT-75 || OCPEXCEPT-76 || https://redhat-internal.slack.com/archives/C0C3BM63VFW/p1790927698394039?thread_ts=1790278918.493159&cid=C0C3BM63VFW + value: false + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "rpm", "path": "deploy/konflux/gateway"}, + {"type": "generic", "path": "deploy/konflux/gateway", "lockfile": "generic-fetcher.yaml"} + ] + pipelineRef: params: - name: url value: https://github.com/red-hat-data-services/konflux-central.git - name: revision - value: '{{ target_branch }}' + value: '{{ cel: pac.target_branch.matches("^rhoai-\\d+\\.\\d+(-ea\\.\\d+)?$") ? pac.target_branch : "main" }}' - name: pathInRepo value: pipelines/multi-arch-container-build.yaml resolver: git taskRunTemplate: - serviceAccountName: build-pipeline-pull-request-pipelines + serviceAccountName: build-pipeline-odh-openshell-gateway-v3-6 podTemplate: imagePullSecrets: - name: redhat-appstudio-staginguser-pull-secret diff --git a/.tekton/odh-openshell-sandbox-openclaw-tag.yaml b/.tekton/odh-openshell-sandbox-openclaw-tag.yaml new file mode 100644 index 0000000000..9144bb0ad9 --- /dev/null +++ b/.tekton/odh-openshell-sandbox-openclaw-tag.yaml @@ -0,0 +1,60 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +# +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/opendatahub-io/openshell?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: | + event == "push" + && target_branch.matches("^refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+-rhaiv\\.[0-9]+$") + creationTimestamp: null + labels: + appstudio.openshift.io/application: opendatahub-builds + appstudio.openshift.io/component: odh-openshell-sandbox-openclaw-ci + pipelines.appstudio.openshift.io/type: build + name: odh-openshell-sandbox-openclaw-on-tag + namespace: open-data-hub-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/opendatahub/odh-openshell-sandbox-openclaw:{{git_tag}} + - name: dockerfile + value: deploy/docker/Dockerfile.konflux.openclaw + - name: path-context + value: . + - name: hermetic + value: "true" + - name: build-platforms + value: + - linux/x86_64 + - linux/arm64 + - name: prefetch-input + value: | + [ + {"type": "npm", "path": "deploy/konflux/openclaw"}, + {"type": "rpm", "path": "deploy/konflux/openclaw"} + ] + pipelineRef: + resolver: git + params: + - name: url + value: https://github.com/opendatahub-io/odh-konflux-central.git + - name: revision + value: main + - name: pathInRepo + value: pipeline/multi-arch-container-build.yaml + taskRunTemplate: + serviceAccountName: build-pipeline-odh-openshell-sandbox-openclaw-ci + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} diff --git a/.tekton/odh-openshell-sandbox-pull-request.yaml b/.tekton/odh-openshell-sandbox-pull-request.yaml index 0508c9f5ab..09e5894a7c 100644 --- a/.tekton/odh-openshell-sandbox-pull-request.yaml +++ b/.tekton/odh-openshell-sandbox-pull-request.yaml @@ -7,12 +7,14 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "true" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-event: '[pull_request]' pipelinesascode.tekton.dev/on-comment: '^/build-konflux' - pipelinesascode.tekton.dev/on-target-branch: '[{{target_branch}}]' + build.appstudio.redhat.com/pull_request_number: "{{pull_request_number}}" + pipelinesascode.tekton.dev/on-cel-expression: |- + event == "pull_request" + && (files.all.exists(p, ! p.matches('^\\.tekton/')) || ".tekton/odh-openshell-sandbox-pull-request.yaml".pathChanged()) labels: - appstudio.openshift.io/application: automation - appstudio.openshift.io/component: pull-request-pipelines-odh-openshell-sandbox + appstudio.openshift.io/application: rhoai-v3-6 + appstudio.openshift.io/component: odh-openshell-sandbox-v3-6 pipelines.appstudio.openshift.io/type: build name: odh-openshell-sandbox-on-pull-request-{{pull_request_number}} namespace: rhoai-tenant @@ -23,31 +25,43 @@ spec: - name: revision value: '{{revision}}' - name: output-image - value: quay.io/rhoai/pull-request-pipelines:odh-openshell-sandbox-{{revision}} + value: quay.io/rhoai/odh-openshell-sandbox-rhel9:on-pr-{{pull_request_number}} - name: dockerfile value: deploy/docker/Dockerfile.konflux.sandbox - name: path-context value: . - name: image-expires-after - value: 5d + value: 30d - name: enable-slack-failure-notification value: "false" - - name: build-platforms value: - linux/x86_64 - linux-m2xlarge/arm64 + - name: rhoai-version + value: "3.6.0" + - name: hermetic + value: true + - name: fips-check-blocking ##OCPEXCEPT-75 || OCPEXCEPT-76 || https://redhat-internal.slack.com/archives/C0C3BM63VFW/p1790927698394039?thread_ts=1790278918.493159&cid=C0C3BM63VFW + value: false + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "rpm", "path": "deploy/konflux/sandbox"}, + {"type": "generic", "path": "deploy/konflux/sandbox", "lockfile": "generic-fetcher.yaml"} + ] pipelineRef: params: - name: url value: https://github.com/red-hat-data-services/konflux-central.git - name: revision - value: '{{ target_branch }}' + value: '{{ cel: pac.target_branch.matches("^rhoai-\\d+\\.\\d+(-ea\\.\\d+)?$") ? pac.target_branch : "main" }}' - name: pathInRepo value: pipelines/multi-arch-container-build.yaml resolver: git taskRunTemplate: - serviceAccountName: build-pipeline-pull-request-pipelines + serviceAccountName: build-pipeline-odh-openshell-sandbox-v3-6 podTemplate: imagePullSecrets: - name: redhat-appstudio-staginguser-pull-secret diff --git a/.tekton/odh-openshell-supervisor-pull-request.yaml b/.tekton/odh-openshell-supervisor-pull-request.yaml index 5db9355965..02e1572164 100644 --- a/.tekton/odh-openshell-supervisor-pull-request.yaml +++ b/.tekton/odh-openshell-supervisor-pull-request.yaml @@ -7,12 +7,14 @@ metadata: build.appstudio.redhat.com/target_branch: '{{target_branch}}' pipelinesascode.tekton.dev/cancel-in-progress: "true" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-event: '[pull_request]' pipelinesascode.tekton.dev/on-comment: '^/build-konflux' - pipelinesascode.tekton.dev/on-target-branch: '[{{target_branch}}]' + build.appstudio.redhat.com/pull_request_number: "{{pull_request_number}}" + pipelinesascode.tekton.dev/on-cel-expression: |- + event == "pull_request" + && (files.all.exists(p, ! p.matches('^\\.tekton/')) || ".tekton/odh-openshell-supervisor-pull-request.yaml".pathChanged()) labels: - appstudio.openshift.io/application: automation - appstudio.openshift.io/component: pull-request-pipelines-odh-openshell-supervisor + appstudio.openshift.io/application: rhoai-v3-6 + appstudio.openshift.io/component: odh-openshell-supervisor-v3-6 pipelines.appstudio.openshift.io/type: build name: odh-openshell-supervisor-on-pull-request-{{pull_request_number}} namespace: rhoai-tenant @@ -23,31 +25,44 @@ spec: - name: revision value: '{{revision}}' - name: output-image - value: quay.io/rhoai/pull-request-pipelines:odh-openshell-supervisor-{{revision}} + value: quay.io/rhoai/odh-openshell-supervisor-rhel9:on-pr-{{pull_request_number}} - name: dockerfile value: deploy/docker/Dockerfile.konflux.supervisor - name: path-context value: . - name: image-expires-after - value: 5d + value: 30d - name: enable-slack-failure-notification value: "false" - - name: build-platforms value: - linux/x86_64 - linux-m2xlarge/arm64 + - name: rhoai-version + value: "3.6.0" + - name: hermetic + value: true + - name: fips-check-blocking ##OCPEXCEPT-75 || OCPEXCEPT-76 || https://redhat-internal.slack.com/archives/C0C3BM63VFW/p1790927698394039?thread_ts=1790278918.493159&cid=C0C3BM63VFW + value: false + - name: prefetch-input + value: | + [ + {"type": "cargo", "path": "."}, + {"type": "rpm", "path": "deploy/konflux/supervisor"}, + {"type": "generic", "path": "deploy/konflux/supervisor", "lockfile": "generic-fetcher.yaml"} + ] + pipelineRef: params: - name: url value: https://github.com/red-hat-data-services/konflux-central.git - name: revision - value: '{{ target_branch }}' + value: '{{ cel: pac.target_branch.matches("^rhoai-\\d+\\.\\d+(-ea\\.\\d+)?$") ? pac.target_branch : "main" }}' - name: pathInRepo value: pipelines/multi-arch-container-build.yaml resolver: git taskRunTemplate: - serviceAccountName: build-pipeline-pull-request-pipelines + serviceAccountName: build-pipeline-odh-openshell-supervisor-v3-6 podTemplate: imagePullSecrets: - name: redhat-appstudio-staginguser-pull-secret diff --git a/CI.md b/CI.md index f38504aef3..de6c53511b 100644 --- a/CI.md +++ b/CI.md @@ -149,6 +149,12 @@ compatibility baseline for the v1beta1 Sandbox API. It does not track the local K3s development default, currently v1.0.3. OpenShell also supports v0.4.6 through its v1alpha1 fallback, so v0.5.0 is not the overall minimum supported version. +The `ubuntu-k3s` lane registers the gateway's ClusterIP Service directly, so +recreating the Service requires reprovisioning the fixture. Before conformance, +every installer waits up to five minutes for a connected gateway; interactive +shells skip the wait. K3s failures upload diagnostics as `tmachine-diagnostics-*` +artifacts. + ### Run only the policy advisor conformance tests Manually dispatch `Integration Tests` on the candidate branch with an @@ -487,7 +493,7 @@ its own stable result status. Merge-group runs use the `merge_group` event. The event is distinct from `pull_request` and `push`, and GitHub will not report required checks for queued PRs unless the workflows include it. In this repository: - `Branch Checks` runs the standard non-E2E gates on the merge-group SHA. -- `Branch E2E Checks` does not run for merge groups. E2E suites remain opt-in on PRs. +- `Branch E2E Checks` does not run for merge groups. E2E suites remain opt-in on PRs. `Required CI Gates` publishes successful not-applicable statuses for both E2E contexts on merge-group events and subsequent merge-group workflow completions. - `Helm Lint` runs for merge groups without the PR diff optimization, because the merge-group branch is the final integration state. - `Trivy Changes` compares the merge-group configuration with its base and rejects new High or Critical findings. - `Required CI Gates` posts the same `OpenShell / ...` statuses to the merge-group SHA and does not require a `pull-request/` mirror for merge-group events. @@ -543,7 +549,7 @@ These workflows run after merge to publish dev/tagged artifacts and verify them. | `.github/workflows/release-tag.yml` | Publishes tagged stable releases and manually dispatched pre-releases. Its automatic tag trigger excludes `-pre.*`. Both require the currently implemented qualification profile to pass before publication; the summary identifies the remaining RFC 0014 coverage. Failed candidates retain build artifacts and evidence in Actions storage. Source-SHA OCI images remain available as qualification inputs. | | `.github/workflows/snap-package.yml` | Builds Snap and component artifacts for Release Dev and Release Tag without Store credentials or publication. | | `.github/workflows/snap-publish.yml` | Uploads existing Snap and component artifacts to the Store without rebuilding. Release Dev calls it directly for `latest/edge` after Snap builds and requires it to succeed before creating the dev release. Release Tag calls it for `latest/stable` only for stable releases, after qualification and release assembly succeed. | -| `.github/workflows/release-canary.yml` | Smoke-tests published dev artifacts in the `macos`, `ubuntu-deb`, `ubuntu-snap-system-docker`, `fedora`, and `kubernetes` (kind + Helm) jobs. Each job reaches its gateway and creates, exercises, and deletes a sandbox. The Snap lanes verify a compatible system Docker lifecycle and `ubuntu-snap-docker-preflight` tests fail-fast behavior when Docker is absent or supplied by the Docker snap. The positive Snap lane also runs a local policy containment check with the packaged prover. It runs automatically after `Release Dev` succeeds and supports manual dispatch (`gh workflow run release-canary.yml --ref `). See the `test-release-canary` skill for the playbook and local kind reproduction. | +| `.github/workflows/release-canary.yml` | Smoke-tests published dev artifacts in the `macos`, `ubuntu-deb`, `ubuntu-snap-system-docker`, `fedora`, and `kubernetes` (kind + Helm) jobs. Each job reaches its gateway and creates, exercises, and deletes a sandbox. The `ubuntu-snap-system-docker` job verifies a compatible system Docker lifecycle, while `ubuntu-snap-docker-preflight` tests fail-fast behavior when Docker is absent or supplied by the Docker snap. The positive Snap lane also runs a local policy containment check with the packaged prover. It runs automatically after `Release Dev` succeeds and supports manual dispatch (`gh workflow run release-canary.yml --ref `). See the `test-release-canary` skill for the playbook and local kind reproduction. | ## Required status contexts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ea44911a0..77bc795197 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,12 @@ Do not start substantial issue-backed work until a maintainer has accepted the i Use agents and the repository skills as needed to understand the affected code, evaluate tradeoffs, implement the smallest coherent change, and verify it. The pull request should explain what changed and how it was tested; it should not substitute an agent transcript for the contributor's understanding. +Every pull request must be approved by someone listed in [MAINTAINERS.md](MAINTAINERS.md) before it can merge. This is enforced by the `OpenShell / Maintainer Approval` status check, which turns green once one of those reviewers approves. Reviews from other contributors are welcome and count toward the general approval requirement, but they do not satisfy this check. + +Pull requests that touch the enforcement machinery itself — anything under `.github/workflows/`, `.github/actions/`, `MAINTAINERS.md`, `.github/CODEOWNERS`, or the enforcement scripts in `tasks/scripts/` — additionally require a code owner's approval, listed in [.github/CODEOWNERS](.github/CODEOWNERS). The status check reads its inputs from `main`, but a workflow triggered by a review runs the pull request's own copy of the workflow file, so GitHub's native code owner requirement is what keeps a change from disabling the gate that would have blocked it. + +Maintainers are not requested automatically. If your pull request has been idle, ask for a reviewer in the pull request or in the CNCF Slack channel rather than waiting. + ## Agent Skills OpenShell keeps skills for using the product separate from skills for developing the repository. diff --git a/Cargo.lock b/Cargo.lock index e8db9e8103..35193cd8f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3975,16 +3975,15 @@ dependencies = [ [[package]] name = "noyalib" -version = "0.0.28" +version = "0.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" dependencies = [ "hashbrown 0.17.0", "indexmap", "libm", "memchr", "rustc-hash 2.1.2", - "serde", "serde_core", "smallvec", ] diff --git a/Cargo.toml b/Cargo.toml index 0882a15adb..c6136bdc26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ socket2 = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_path_to_error = "0.1" -serde_yml = { package = "noyalib", version = "0.0.28", default-features = false, features = ["std", "compat-serde-yaml"] } +serde_yml = { package = "noyalib", version = "0.0.53", default-features = false, features = ["std", "compat-serde-yaml"] } toml = "0.8" apollo-parser = "0.8.5" tower-mcp-types = "=0.22.2" diff --git a/crates/openshell-cli/src/output.rs b/crates/openshell-cli/src/output.rs index 7a8120bf3b..aee8a2c198 100644 --- a/crates/openshell-cli/src/output.rs +++ b/crates/openshell-cli/src/output.rs @@ -91,7 +91,10 @@ where "yaml" => { let envelope = paginated_collection_value(collection_name, items, next_page_token, to_json); - print!("{}", serde_yml::to_string(&envelope).into_diagnostic()?); + print!( + "{}", + openshell_core::yaml::to_string(&envelope).into_diagnostic()? + ); Ok(true) } "table" => Ok(false), @@ -138,7 +141,10 @@ where } "yaml" => { let values: Vec<_> = items.iter().map(to_json).collect(); - print!("{}", serde_yml::to_string(&values).into_diagnostic()?); + print!( + "{}", + openshell_core::yaml::to_string(&values).into_diagnostic()? + ); Ok(true) } "table" => Ok(false), @@ -177,7 +183,10 @@ where } "yaml" => { let value = to_json(item); - print!("{}", serde_yml::to_string(&value).into_diagnostic()?); + print!( + "{}", + openshell_core::yaml::to_string(&value).into_diagnostic()? + ); Ok(true) } "table" => Ok(false), @@ -265,7 +274,7 @@ where write!( writer, "{}", - serde_yml::to_string(&values).into_diagnostic()? + openshell_core::yaml::to_string(&values).into_diagnostic()? ) .into_diagnostic()?; Ok(true) @@ -309,7 +318,7 @@ where write!( writer, "{}", - serde_yml::to_string(&envelope).into_diagnostic()? + openshell_core::yaml::to_string(&envelope).into_diagnostic()? ) .into_diagnostic()?; Ok(true) @@ -356,7 +365,7 @@ where write!( writer, "{}", - serde_yml::to_string(&value).into_diagnostic()? + openshell_core::yaml::to_string(&value).into_diagnostic()? ) .into_diagnostic()?; Ok(true) diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index e7221e0079..c08f49296b 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -339,6 +339,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &self, request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index ca969dde13..6a4bafd19d 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -295,6 +295,13 @@ impl OpenShell for TestOpenShell { )) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &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 2cbeaffdf6..e53352d079 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -751,6 +751,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &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 bb0abceb2f..f71108691e 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -547,6 +547,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ProviderResponse::default())) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_exec_streaming_integration.rs b/crates/openshell-cli/tests/sandbox_exec_streaming_integration.rs index 359895838a..c57e2d4ea5 100644 --- a/crates/openshell-cli/tests/sandbox_exec_streaming_integration.rs +++ b/crates/openshell-cli/tests/sandbox_exec_streaming_integration.rs @@ -243,6 +243,7 @@ mock_gateway! { delete_service(proto::DeleteServiceRequest) -> proto::DeleteServiceResponse; revoke_ssh_session(proto::RevokeSshSessionRequest) -> proto::RevokeSshSessionResponse; create_provider(proto::CreateProviderRequest) -> proto::ProviderResponse; + get_provider_credentials(proto::GetProviderCredentialsRequest) -> proto::GetProviderCredentialsResponse; get_provider(proto::GetProviderRequest) -> proto::ProviderResponse; list_providers(proto::ListProvidersRequest) -> proto::ListProvidersResponse; list_provider_profiles(proto::ListProviderProfilesRequest) -> proto::ListProviderProfilesResponse; diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 7a5780d7f3..fb8d39895f 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -357,6 +357,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(ProviderResponse::default())) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index 56e314127d..12cf145f44 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -410,6 +410,10 @@ pub struct OidcConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct MtlsAuthConfig { + /// Enable gateway-wide operators authenticated by verified certificates with OU=operator. + /// Independent of ordinary mTLS user authentication; defaults off. + #[serde(default)] + pub operator_enabled: bool, /// When true, the gateway maps a verified TLS client certificate into a /// user principal. Sandbox and supervisor clients use bearer identity, so /// this setting is independent of the selected compute driver. diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index 1cbf6968e0..84d4f8ccf5 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -42,6 +42,7 @@ pub mod net; pub mod oauth; pub mod paths; pub mod policy; +pub use openshell_policy_schema::yaml; pub mod policy_identity; pub mod progress; pub mod proposals; diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index a67ae30c5c..f34c791adc 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -336,6 +336,28 @@ mod tests { assert_eq!(decoded, request); } + #[test] + fn operator_credential_response_omits_values_inside_maps() { + let codec = ProtoJsonCodec::openshell().unwrap(); + let response = openshell_core::proto::GetProviderCredentialsResponse { + credentials: HashMap::from([( + "ACCESS_TOKEN".to_string(), + openshell_core::proto::ProviderCredentialValue { + value: "must-not-observe".to_string(), + expiration_time: None, + }, + )]), + }; + let view = codec + .decode_bytes_to_interceptor_json( + "openshell.v1.GetProviderCredentialsResponse", + &response.encode_to_vec(), + ) + .unwrap(); + assert!(view["credentials"]["ACCESS_TOKEN"].get("value").is_none()); + assert!(!view.to_string().contains("must-not-observe")); + } + #[test] fn interceptor_view_omits_nested_secrets_but_keeps_non_secret_fields() { let codec = ProtoJsonCodec::openshell().unwrap(); diff --git a/crates/openshell-policy-schema/README.md b/crates/openshell-policy-schema/README.md new file mode 100644 index 0000000000..7debf228b3 --- /dev/null +++ b/crates/openshell-policy-schema/README.md @@ -0,0 +1,16 @@ +# openshell-policy-schema + +Canonical authored policy types, bounded YAML parsing, and pure validation. +Runtime enforcement and protobuf adaptation remain outside this crate. + +The `yaml` module supplies the shared authored-data compatibility boundary used +by policy, provider-profile, and prover loaders and by CLI YAML output. Each +loader declares the schema object paths that must reject null before ordinary +Serde decoding. Optional objects and untyped user-data values retain their +ordinary null semantics. Empty or null top-level documents are rejected. +Loaders reject duplicate mapping keys before decoding their typed schema. + +Policy parsing retains its explicit resource budgets and field-path diagnostics. +YAML exports quote strings and mapping keys that legacy readers could interpret +as dates, numbers, or booleans, without changing actual numeric or boolean data. +Formatting the already-generated output does not impose new input-size limits. diff --git a/crates/openshell-policy-schema/src/lib.rs b/crates/openshell-policy-schema/src/lib.rs index 8642ce8599..656433b794 100644 --- a/crates/openshell-policy-schema/src/lib.rs +++ b/crates/openshell-policy-schema/src/lib.rs @@ -17,6 +17,8 @@ use std::str::FromStr; use miette::{IntoDiagnostic, Result, WrapErr}; use serde::{Deserialize, Deserializer, Serialize}; +pub mod yaml; + /// Fixed resource bound for legacy MCP request batches inspected by Tower. pub const MAX_MCP_LEGACY_BATCH_MESSAGES: usize = 64; @@ -518,7 +520,9 @@ where D: Deserializer<'de>, T: Deserialize<'de>, { - T::deserialize(deserializer).map(Some) + Option::::deserialize(deserializer)? + .map(Some) + .ok_or_else(|| serde::de::Error::custom("null is not allowed for this field")) } const MAX_UNKNOWN_FIELD_PATH_BYTES: usize = 1_024; @@ -591,6 +595,36 @@ pub fn parse_policy_with_limits(source: &str, limits: ParseLimits) -> Result String { /// Serialize the authored representation to YAML. pub fn serialize_policy(document: &PolicyDocument) -> Result { - serde_yml::to_string(document) + yaml::to_string(document) .into_diagnostic() .wrap_err("failed to serialize policy to YAML") } @@ -1198,6 +1232,55 @@ mod tests { } } + #[test] + fn nested_null_objects_remain_invalid() { + for spelling in ["null", "~", ""] { + for body in [ + format!("network_policies: {spelling}"), + format!("network_policies: {{x: {spelling}}}"), + format!("network_middlewares: {spelling}"), + format!("landlock: {spelling}"), + format!("network_middlewares: {{x: {{middleware: foo, config: {spelling}}}}}"), + format!("network_middlewares: {{x: {{middleware: foo, endpoints: {spelling}}}}}"), + format!("network_policies: {{x: {{endpoints: [{spelling}]}}}}"), + ] { + // A bare [] is an empty collection rather than a null entry. + if spelling.is_empty() && body.ends_with("[]}}") { + continue; + } + assert!( + parse_policy(&format!("version: 1\n{body}\n")).is_err(), + "{body}" + ); + } + for field in [ + format!("rules: [{{allow: {spelling}}}]"), + format!("deny_rules: [{{method: GET, query: {spelling}}}]"), + format!("deny_rules: [{spelling}]"), + format!("rules: [{{allow: {{query: {spelling}}}}}]"), + format!("rules: [{{allow: {{params: {spelling}}}}}]"), + format!("rules: [{{allow: {{tool: {spelling}}}}}]"), + format!("deny_rules: [{{tool: {spelling}}}]"), + format!("graphql_persisted_queries: {spelling}"), + format!("graphql_persisted_queries: {{op: {spelling}}}"), + format!("credential_binding: {spelling}"), + format!("json_rpc: {spelling}"), + format!("mcp: {spelling}"), + ] { + if field == "deny_rules: []" { + continue; + } + let yaml = format!( + "version: 1\nnetwork_policies: {{x: {{endpoints: [{{host: example.com, port: 443, {field}}}]}}}}\n" + ); + assert!(parse_policy(&yaml).is_err(), "{yaml}"); + } + } + let yaml = + "version: 1\nnetwork_middlewares: {x: {middleware: foo, config: {optional: null}}}\n"; + assert!(parse_policy(yaml).is_ok()); + } + #[test] fn truncation_paths_are_unicode_safe() { let oversized = "é".repeat(ParseLimits::default().max_bytes); diff --git a/crates/openshell-policy-schema/src/yaml.rs b/crates/openshell-policy-schema/src/yaml.rs new file mode 100644 index 0000000000..3f3c072d9e --- /dev/null +++ b/crates/openshell-policy-schema/src/yaml.rs @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! YAML compatibility at `OpenShell`'s authored-data boundaries. + +use serde::Serialize; +use serde::de::DeserializeOwned; +use serde_yml::{Error, Value}; + +/// Load authored YAML with bounded parsing and duplicate-key rejection. +pub fn from_str(source: &str) -> Result { + from_str_with_object_paths(source, &[]) +} + +/// Reject null at schema-declared object paths before ordinary typed decoding. +/// +/// A `*` selects each sequence element or mapping value. Missing fields are +/// left to Serde defaults; optional objects should not be listed themselves. +pub fn from_str_with_object_paths( + source: &str, + paths: &[&str], +) -> Result { + let config = + serde_yml::ParserConfig::new().duplicate_key_policy(serde_yml::DuplicateKeyPolicy::Error); + let value: Value = serde_yml::from_str_with_config(source, &config)?; + reject_null_objects(&value, paths)?; + T::deserialize(serde_yml::Deserializer::new(&value)) +} + +/// Validate only declared schema objects, not arbitrary user-data mappings. +pub fn reject_null_objects(value: &Value, paths: &[&str]) -> Result<(), Error> { + reject_null_at_path(value, "", "")?; + for path in paths { + reject_null_at_path(value, path, "")?; + } + Ok(()) +} + +fn reject_null_at_path(value: &Value, remaining: &str, location: &str) -> Result<(), Error> { + if let Value::Tagged(tagged) = value { + return reject_null_at_path(tagged.value(), remaining, location); + } + if remaining.is_empty() { + return if value.is_null() { + Err(Error::Parse(format!( + "{}: null is not an object", + if location.is_empty() { + ".".to_owned() + } else { + crate::bound_path(location.to_owned()) + } + ))) + } else { + Ok(()) + }; + } + let (field, rest) = remaining.split_once('.').unwrap_or((remaining, "")); + if field == "*" { + if let Some(mapping) = value.as_mapping() { + for (key, child) in mapping { + reject_null_at_path(child, rest, &format!("{location}[{:?}]", key.as_str()))?; + } + } else if let Some(sequence) = value.as_sequence() { + for (index, child) in sequence.iter().enumerate() { + reject_null_at_path(child, rest, &format!("{location}[{index}]"))?; + } + } + } else if let Some(child) = value.as_mapping().and_then(|mapping| mapping.get(field)) { + let path = if location.is_empty() { + field.to_owned() + } else { + format!("{location}.{field}") + }; + reject_null_at_path(child, rest, &path)?; + } + Ok(()) +} + +/// Preserve authored strings even for YAML 1.1 readers (dates, timestamps, +/// underscore-separated integers, and legacy boolean words). +pub fn to_string(value: &T) -> Result { + let value = serde_yml::to_value(&value)?; + let mut yaml = serde_yml::to_string(&value)?; + let mut strings = Vec::new(); + collect_strings(&value, "", &mut strings); + if strings.is_empty() { + return Ok(yaml); + } + // This is our already-emitted output, not a fresh untrusted input. Size the + // formatting walk to that output so an otherwise valid large export does + // not acquire the input parser's collection or byte limits. + let mut config = serde_yml::ParserConfig::new(); + config.max_document_length = config.max_document_length.max(yaml.len()); + config.max_total_scalar_bytes = config.max_total_scalar_bytes.max(yaml.len()); + config.max_events = config.max_events.max(yaml.len().saturating_mul(4)); + config.max_nodes = config.max_nodes.max(yaml.len().saturating_mul(2)); + config.max_mapping_keys = config.max_mapping_keys.max(yaml.len()); + config.max_sequence_length = config.max_sequence_length.max(yaml.len()); + let document = serde_yml::cst::parse_document_with_config(&yaml, &config)?; + let mut edits = Vec::new(); + for (path, string, is_key) in strings { + let (start, end) = if is_key { + document.key_span(&path) + } else { + document.span_at(&path) + } + .ok_or_else(|| Error::Parse("serialized YAML string missing".into()))?; + // Already quoted or block-styled strings retain their emitter style. + if !yaml[start..end].starts_with(['\'', '"', '|', '>']) { + let quoted = serde_yml::to_string_with_config( + &string, + &serde_yml::SerializerConfig::new().quote_all(true), + )?; + edits.push((start, end, quoted.trim_end().to_owned())); + } + } + edits.sort_unstable_by_key(|(start, _, _)| *start); + for (start, end, quoted) in edits.into_iter().rev() { + yaml.replace_range(start..end, "ed); + } + Ok(yaml) +} + +fn collect_strings<'a>(value: &'a Value, path: &str, result: &mut Vec<(String, &'a str, bool)>) { + match value { + Value::String(string) if needs_legacy_quotes(string) => { + result.push((path.into(), string, false)); + } + Value::Mapping(mapping) => { + for (key, child) in mapping { + let child_path = format!("{path}{}", serde_yml::path::quote_key(key.as_str())); + if needs_legacy_quotes(key.as_str()) { + result.push((child_path.clone(), key.as_str(), true)); + } + collect_strings(child, &child_path, result); + } + } + Value::Sequence(sequence) => { + for (index, child) in sequence.iter().enumerate() { + collect_strings(child, &format!("{path}[{index}]"), result); + } + } + Value::Tagged(tagged) => collect_strings(tagged.value(), path, result), + _ => {} + } +} + +fn needs_legacy_quotes(string: &str) -> bool { + let unsigned = string.trim_start_matches(['+', '-']); + (unsigned.as_bytes().first().is_some_and(u8::is_ascii_digit) + && unsigned.bytes().all(|byte| { + byte.is_ascii_digit() + || matches!( + byte, + b'_' | b'.' + | b':' + | b'/' + | b'+' + | b'-' + | b' ' + | b'T' + | b't' + | b'Z' + | b'z' + | b'E' + | b'e' + | b'X' + | b'x' + | b'O' + | b'o' + | b'B' + | b'b' + ) + })) + || matches!( + string.to_ascii_lowercase().as_str(), + "yes" | "no" | "on" | "off" | "y" | "n" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + #[derive(Debug, Deserialize)] + struct Document { + #[serde(default)] + entries: std::collections::BTreeMap, + } + #[derive(Debug, Default, Deserialize)] + struct Entry { + #[serde(default)] + config: std::collections::BTreeMap, + } + + #[test] + fn null_objects_are_rejected_but_user_data_null_is_retained() { + for source in [ + "entries: null", + "entries: {x: null}", + "entries: {x: {config: null}}", + ] { + assert!( + from_str_with_object_paths::( + source, + &["entries", "entries.*", "entries.*.config"] + ) + .is_err(), + "{source}" + ); + } + let parsed: Document = from_str_with_object_paths( + "entries: {x: {config: {optional: null}}}", + &["entries", "entries.*", "entries.*.config"], + ) + .unwrap(); + assert!(parsed.entries["x"].config["optional"].is_null()); + } + #[test] + fn duplicate_fields_are_rejected() { + assert!(from_str::("entries: {}\nentries: {x: {}}").is_err()); + } + #[test] + fn empty_documents_do_not_become_default_objects() { + for source in ["", "# comment only", "null", "~"] { + assert!(from_str::(source).is_err(), "{source}"); + } + assert!(from_str::("{}").is_ok()); + } + + #[test] + fn object_paths_preserve_optional_nulls_and_visit_sequence_elements() { + #[derive(Debug, Deserialize)] + struct Profile { + optional: Option, + endpoints: Vec, + } + let paths = ["endpoints.*", "endpoints.*.config", "optional.config"]; + let parsed: Profile = from_str_with_object_paths( + "optional: null\nendpoints: [{config: {data: null}}]", + &paths, + ) + .unwrap(); + assert!(parsed.optional.is_none()); + assert!(parsed.endpoints[0].config["data"].is_null()); + for source in [ + "optional: null\nendpoints: [null]", + "optional: {config: null}\nendpoints: []", + ] { + assert!( + from_str_with_object_paths::(source, &paths).is_err(), + "{source}" + ); + } + let source = "entries: {'key.with.dots': null}"; + let error = from_str_with_object_paths::(source, &["entries.*"]).unwrap_err(); + assert!(error.to_string().contains("key.with.dots")); + } + #[test] + fn legacy_reader_sensitive_strings_are_quoted_without_changing_values() { + let value = serde_json::json!({"versions": ["2025-11-25"], "timestamp": "2026-01-01T00:00:00Z", "number": "1_000", "boolean": "yes", "duration": "1.500s", "actual_number": 1000, "data": null}); + let yaml = to_string(&value).unwrap(); + for string in ["2025-11-25", "2026-01-01T00:00:00Z", "1_000", "yes"] { + assert!( + yaml.contains(&format!("\"{string}\"")) || yaml.contains(&format!("'{string}'")), + "{yaml}" + ); + } + assert_eq!(from_str::(&yaml).unwrap(), value); + } +} diff --git a/crates/openshell-prover/src/accepted_risks.rs b/crates/openshell-prover/src/accepted_risks.rs index 8c28a44188..52c77947f5 100644 --- a/crates/openshell-prover/src/accepted_risks.rs +++ b/crates/openshell-prover/src/accepted_risks.rs @@ -57,9 +57,10 @@ pub fn load_accepted_risks(path: &Path) -> Result> { let contents = std::fs::read_to_string(path) .into_diagnostic() .wrap_err_with(|| format!("reading accepted risks {}", path.display()))?; - let raw: AcceptedRisksFile = serde_yml::from_str(&contents) - .into_diagnostic() - .wrap_err("parsing accepted risks YAML")?; + let raw: AcceptedRisksFile = + openshell_policy_schema::yaml::from_str_with_object_paths(&contents, &["accepted_risks.*"]) + .into_diagnostic() + .wrap_err("parsing accepted risks YAML")?; Ok(raw .accepted_risks diff --git a/crates/openshell-prover/src/containment.rs b/crates/openshell-prover/src/containment.rs index 9a9c9cb72a..d2bd213e95 100644 --- a/crates/openshell-prover/src/containment.rs +++ b/crates/openshell-prover/src/containment.rs @@ -966,12 +966,35 @@ fn endpoint_matches_connection(endpoint: &Endpoint, action: &SymbolicAction) -> } fn endpoint_path_matches(endpoint: &Endpoint, action: &SymbolicAction) -> Bool { - let path = if endpoint.path.is_empty() { - "**" + let pattern = endpoint.path.as_str(); + // Mirror openshell_core::endpoint_path, not the REST rule glob matcher. + if matches!(pattern, "" | "**" | "/**") { + return Bool::from_bool(true); + } + if let Some(prefix) = pattern.strip_suffix("/**") { + // The entire prefix is literal, including any wildcard characters. + return Bool::or(&[ + action.path.eq(prefix), + Z3String::from_str(&format!("{prefix}/")) + .unwrap() + .prefix(&action.path), + ]); + } + if glob::Pattern::new(pattern).is_err() { + return action.path.eq(pattern); + } + // Rust glob consumes the slash after ** while parsing. A terminal **/ + // therefore matches the rest of the path, even without a trailing slash. + let pattern = if pattern.ends_with("/**/") { + &pattern[..pattern.len() - 1] } else { - &endpoint.path + pattern }; - action.path.regex_matches(&glob_regex(path, "/")) + // Validation excludes ?, brackets, braces and escapes. For the remaining + // Rust glob language, a single star also crosses path separators. + action + .path + .regex_matches(&path_glob_regex(pattern, &Regexp::full())) } fn method_and_path_match(method: &str, path: &str, action: &SymbolicAction) -> Bool { @@ -2136,7 +2159,7 @@ fn glob_regex(pattern: &str, separator: &str) -> Regexp { return Regexp::full(); } if separator == "/" { - return path_glob_regex(pattern); + return path_glob_regex(pattern, &non_separator_regex("/").star()); } let mut parts = Vec::new(); let mut chars = pattern.chars().peekable(); @@ -2159,7 +2182,7 @@ fn glob_regex(pattern: &str, separator: &str) -> Regexp { } } -fn path_glob_regex(pattern: &str) -> Regexp { +fn path_glob_regex(pattern: &str, single_star: &Regexp) -> Regexp { let mut parts = Vec::new(); let mut segments = pattern.split('/').peekable(); while let Some(segment) = segments.next() { @@ -2180,9 +2203,10 @@ fn path_glob_regex(pattern: &str) -> Regexp { }); } else { for character in segment.chars() { - // Embedded stars, including **, cannot cross a separator. + // REST rule stars stay within a segment; endpoint selector + // stars use the unrestricted Rust glob match language. parts.push(if character == '*' { - non_separator_regex("/").star() + single_star.clone() } else { Regexp::literal(&character.to_string()) }); @@ -3316,6 +3340,71 @@ network_policies: } } + #[test] + fn endpoint_paths_match_the_canonical_runtime_matcher() { + for pattern in [ + "", + "**", + "/**", + "/a", + "/a/*", + "/a/*/b", + "/a/**", + "/a/**/b", + "/a/**/**/b", + "/a/**/", + "/**/", + "/a/**/**/", + "/a/**/b/**/", + "/a/*/**/", + "/a*/**", + "/a/**/**", + "/a/x**/b", + "/a/**x/b", + "/a/***/b", + ] { + let endpoint: Endpoint = serde_json::from_value(serde_json::json!({ + "host": "api.example.com", "port": 443, "path": pattern, + })) + .unwrap(); + let mut action = symbolic_action("endpoint_path_parity"); + for path in [ + pattern, + "/", + "/a", + "/a/", + "/a/b", + "/a/x/b", + "/a/x/y/b", + "/a/x/y/", + "/a/xb/b", + "/a/xx/b", + "/ab/c", + "/a*", + "/a*/b", + "/a/**", + "/a/**/b", + "/a/é/b", + "/a/汉/b", + "/a/e\u{301}/b", + "/a/😀/b", + ] { + action.path = Z3String::from_str(path).unwrap(); + let solver = Solver::new(); + solver.assert(endpoint_path_matches(&endpoint, &action)); + assert_eq!( + solver.check(), + if openshell_core::endpoint_path::matches(pattern, path) { + SatResult::Sat + } else { + SatResult::Unsat + }, + "pattern={pattern:?} path={path:?}" + ); + } + } + } + #[test] fn path_globs_match_the_runtime_builtin() { let mut runtime = regorus::Engine::new(); diff --git a/crates/openshell-prover/src/credentials.rs b/crates/openshell-prover/src/credentials.rs index 6c9a1a74f3..a7be2d559f 100644 --- a/crates/openshell-prover/src/credentials.rs +++ b/crates/openshell-prover/src/credentials.rs @@ -268,9 +268,10 @@ pub fn load_credentials(path: &Path) -> Result> { let contents = std::fs::read_to_string(path) .into_diagnostic() .wrap_err_with(|| format!("reading credentials file {}", path.display()))?; - let raw: CredentialsFile = serde_yml::from_str(&contents) - .into_diagnostic() - .wrap_err("parsing credentials YAML")?; + let raw: CredentialsFile = + openshell_policy_schema::yaml::from_str_with_object_paths(&contents, &["credentials.*"]) + .into_diagnostic() + .wrap_err("parsing credentials YAML")?; Ok(raw .credentials @@ -286,9 +287,16 @@ pub fn load_credentials(path: &Path) -> Result> { } fn parse_api_registry(contents: &str, source: &str) -> Result { - let raw: ApiRegistryDef = serde_yml::from_str(contents) - .into_diagnostic() - .wrap_err_with(|| format!("parsing API registry {source}"))?; + let raw: ApiRegistryDef = openshell_policy_schema::yaml::from_str_with_object_paths( + contents, + &[ + "scope_capabilities", + "scope_capabilities.*.*", + "action_risk", + ], + ) + .into_diagnostic() + .wrap_err_with(|| format!("parsing API registry {source}"))?; let scope_capabilities = raw .scope_capabilities diff --git a/crates/openshell-prover/src/registry.rs b/crates/openshell-prover/src/registry.rs index 63a3fce3bc..c425b2e9be 100644 --- a/crates/openshell-prover/src/registry.rs +++ b/crates/openshell-prover/src/registry.rs @@ -194,9 +194,12 @@ impl BinaryRegistry { // --------------------------------------------------------------------------- fn parse_binary_capability(contents: &str, source: &str) -> Result { - let raw: BinaryCapabilityDef = serde_yml::from_str(contents) - .into_diagnostic() - .wrap_err_with(|| format!("parsing binary descriptor {source}"))?; + let raw: BinaryCapabilityDef = openshell_policy_schema::yaml::from_str_with_object_paths( + contents, + &["protocols.*", "protocols.*.actions.*"], + ) + .into_diagnostic() + .wrap_err_with(|| format!("parsing binary descriptor {source}"))?; let protocols = raw .protocols diff --git a/crates/openshell-prover/tests/runtime_parity.rs b/crates/openshell-prover/tests/runtime_parity.rs index 198198bdae..81b4557cb4 100644 --- a/crates/openshell-prover/tests/runtime_parity.rs +++ b/crates/openshell-prover/tests/runtime_parity.rs @@ -36,6 +36,19 @@ fn runtime_engine_with_identity(policy: &str, require_binary_identity: bool) -> ); let mut engine = Engine::new(); + // The runtime registers this native builtin for endpoint selectors only. + engine + .add_extension( + "openshell.endpoint_path_matches".into(), + 2, + Box::new(|params: Vec| { + Ok(Value::from(openshell_core::endpoint_path::matches( + params[0].as_string()?, + params[1].as_string()?, + ))) + }), + ) + .expect("runtime endpoint path matcher should register"); engine .add_policy("sandbox-policy.rego".into(), SANDBOX_POLICY_REGO.into()) .expect("runtime Rego should compile"); @@ -46,6 +59,16 @@ fn runtime_engine_with_identity(policy: &str, require_binary_identity: bool) -> } fn runtime_input(binary: &str, ancestors: &[&str], host: &str, method: &str) -> Value { + runtime_input_with_path(binary, ancestors, host, method, "/") +} + +fn runtime_input_with_path( + binary: &str, + ancestors: &[&str], + host: &str, + method: &str, + path: &str, +) -> Value { serde_json::from_value(json!({ "exec": { "path": binary, @@ -58,7 +81,7 @@ fn runtime_input(binary: &str, ancestors: &[&str], host: &str, method: &str) -> }, "request": { "method": method, - "path": "/", + "path": path, "query_params": {}, }, })) @@ -85,6 +108,133 @@ fn eval_array_len(engine: &mut Engine, input: &Value, rule: &str) -> usize { } } +fn endpoint_path_policy(selector: &str, rule_path: &str) -> String { + json!({ + "version": 1, + "network_policies": {"egress": { + "binaries": [{"path": "/usr/bin/curl"}], + "endpoints": [{ + "host": "api.example.com", "ports": [443], + "protocol": "rest", "enforcement": "enforce", "path": selector, + "rules": [{"allow": {"method": "GET", "path": rule_path}}] + }] + }} + }) + .to_string() +} + +fn assert_runtime_path_expansion_replays(boundary: &str, candidate: &str, path: &str) { + let input = runtime_input_with_path("/usr/bin/curl", &[], "api.example.com", "GET", path); + assert!(eval_bool( + &mut runtime_engine(candidate), + &input, + "data.openshell.sandbox.allow_request" + )); + assert!(!eval_bool( + &mut runtime_engine(boundary), + &input, + "data.openshell.sandbox.allow_request" + )); + + let result = check(boundary, candidate); + let CheckResult::Exceeds(evidence) = result else { + panic!("runtime expansion must exceed the boundary: {result:?}"); + }; + let Counterexample::Network { + binary, + ancestor_binary, + binary_identity_required, + host, + port, + method: Some(method), + path: Some(path), + .. + } = evidence.counterexample() + else { + panic!("expected a REST counterexample"); + }; + let input = serde_json::from_value(json!({ + "exec": { + "path": binary.as_deref().unwrap_or(""), + "ancestors": ancestor_binary.iter().collect::>(), + "cmdline_paths": [], + }, + "network": {"host": host, "port": port}, + "request": {"method": method, "path": path, "query_params": {}} + })) + .unwrap(); + assert!(eval_bool( + &mut runtime_engine_with_identity(candidate, *binary_identity_required), + &input, + "data.openshell.sandbox.allow_request" + )); + assert!(!eval_bool( + &mut runtime_engine_with_identity(boundary, *binary_identity_required), + &input, + "data.openshell.sandbox.allow_request" + )); +} + +#[test] +fn endpoint_selector_single_star_crosses_path_separators() { + assert_runtime_path_expansion_replays( + &endpoint_path_policy("", "/v1/*"), + &endpoint_path_policy("/v1/*", "/**"), + "/v1/a/b", + ); +} + +#[test] +fn endpoint_selector_subtree_includes_the_prefix_itself() { + assert_runtime_path_expansion_replays( + &endpoint_path_policy("", "/p/**"), + &endpoint_path_policy("/p/**", "/**"), + "/p", + ); +} + +#[test] +fn endpoint_selector_scoped_denies_match_runtime() { + for (selector, path) in [("/v1/*", "/v1/a/b"), ("/p/**", "/p")] { + let mut boundary: serde_json::Value = + serde_json::from_str(&endpoint_path_policy("", "/**")).unwrap(); + let endpoints = boundary["network_policies"]["egress"]["endpoints"] + .as_array_mut() + .unwrap(); + let mut deny = endpoints[0].clone(); + deny["path"] = json!(selector); + deny["deny_rules"] = json!([{"method": "GET", "path": "/**"}]); + endpoints.push(deny); + assert_runtime_path_expansion_replays( + &boundary.to_string(), + &endpoint_path_policy("", path), + path, + ); + } +} + +#[test] +fn endpoint_selector_contained_policies_remain_within() { + for (boundary_selector, boundary_rule, candidate_selector, candidate_rule, path) in [ + ("/v1/*", "/**", "", "/v1/**", "/v1/a/b"), + ("/p/**", "/**", "", "/p", "/p"), + ("/p/**", "/**", "/p/**", "/**", "/p/a"), + ] { + let boundary = endpoint_path_policy(boundary_selector, boundary_rule); + let candidate = endpoint_path_policy(candidate_selector, candidate_rule); + let input = runtime_input_with_path("/usr/bin/curl", &[], "api.example.com", "GET", path); + for policy in [&boundary, &candidate] { + assert!(eval_bool( + &mut runtime_engine(policy), + &input, + "data.openshell.sandbox.allow_request" + )); + } + let result = check(&boundary, &candidate); + assert!(matches!(result, CheckResult::Within(_)), "{result:?}"); + } +} + #[test] fn underscore_host_counterexample_replays_at_runtime() { let boundary = "version: 1\n"; diff --git a/crates/openshell-providers/README.md b/crates/openshell-providers/README.md index f0b5b79233..2a9b686019 100644 --- a/crates/openshell-providers/README.md +++ b/crates/openshell-providers/README.md @@ -13,6 +13,9 @@ out of the CLI and gateway control flow. - Discover local credentials from environment variables and known config files. - Normalize discovered data into provider records. - Keep provider-specific parsing rules in provider modules. +- Use the shared authored-YAML compatibility boundary for profile imports and + exports: reject duplicate keys and null objects without coercion, retain + optional nulls, and preserve string types for YAML 1.1 readers. - Avoid logging credential values. ## Non-Responsibilities diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index c93206c733..1f7655aae3 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -1974,7 +1974,31 @@ fn graphql_operation_from_proto(operation: &GraphqlOperation) -> GraphqlOperatio } pub fn parse_profile_yaml(input: &str) -> Result { - Ok(serde_yml::from_str::(input)?) + Ok(openshell_core::yaml::from_str_with_object_paths::< + ProviderTypeProfile, + >( + input, + &[ + "annotations", + "discovery", + "credentials.*", + "files.*", + "endpoints.*", + "binaries.*", + "credentials.*.refresh.material.*", + "credentials.*.refresh.additional_outputs.*", + "credentials.*.token_grant.audience_overrides.*", + "endpoints.*.mcp", + "endpoints.*.graphql_persisted_queries", + "endpoints.*.graphql_persisted_queries.*", + "endpoints.*.rules.*", + "endpoints.*.rules.*.allow.query", + "endpoints.*.rules.*.allow.params", + "endpoints.*.deny_rules.*", + "endpoints.*.deny_rules.*.query", + "endpoints.*.deny_rules.*.params", + ], + )?) } pub fn parse_profile_json(input: &str) -> Result { @@ -1982,7 +2006,7 @@ pub fn parse_profile_json(input: &str) -> Result Result { - Ok(serde_yml::to_string(profile)?) + Ok(openshell_core::yaml::to_string(profile)?) } pub fn profile_to_json(profile: &ProviderTypeProfile) -> Result { @@ -1990,7 +2014,7 @@ pub fn profile_to_json(profile: &ProviderTypeProfile) -> Result Result { - Ok(serde_yml::to_string(profiles)?) + Ok(openshell_core::yaml::to_string(profiles)?) } pub fn profiles_to_json(profiles: &[ProviderTypeProfile]) -> Result { @@ -3795,9 +3819,18 @@ credentials: let exact = ProviderTypeProfile::from_proto(&proto); let yaml = profile_to_yaml(&exact).unwrap(); - assert!(yaml.contains("refresh_before: \"0s\""), "{yaml}"); - assert!(yaml.contains("max_lifetime: \"1.500s\""), "{yaml}"); - assert!(yaml.contains("cache_ttl: \"0.500s\""), "{yaml}"); + // Check string values rather than the emitter's choice of quote style. + let document: serde_yml::Value = serde_yml::from_str(&yaml).unwrap(); + let credential = &document["credentials"][0]; + assert_eq!(credential["refresh"]["refresh_before"].as_str(), Some("0s")); + assert_eq!( + credential["refresh"]["max_lifetime"].as_str(), + Some("1.500s") + ); + assert_eq!( + credential["token_grant"]["cache_ttl"].as_str(), + Some("0.500s") + ); assert!(!yaml.contains("refresh_before_seconds")); assert!(!yaml.contains("max_lifetime_seconds")); assert!(!yaml.contains("cache_ttl_seconds")); @@ -3813,6 +3846,24 @@ credentials: ); } + #[test] + fn yaml_profile_rejects_null_objects_and_duplicate_fields() { + for field in [ + "annotations: null", + "discovery: null", + "endpoints: [null]", + "endpoints: [{host: example.com, port: 443, rules: [{allow: {query: null}}]}]", + "endpoints: [{host: example.com, port: 443, rules: [{allow: {params: null}}]}]", + "endpoints: [{host: example.com, port: 443, graphql_persisted_queries: null}]", + ] { + let yaml = format!("id: sample\ndisplay_name: Sample\n{field}\n"); + assert!(parse_profile_yaml(&yaml).is_err(), "{yaml}"); + } + assert!(parse_profile_yaml("id: first\nid: second\ndisplay_name: Sample\n").is_err()); + // Existing optional fields still accept null as absence. + assert!(parse_profile_yaml("id: sample\ndisplay_name: Sample\nendpoints: [{host: example.com, port: 443, rules: [{allow: null}]}]\n").is_ok()); + } + #[test] fn canonical_zero_max_lifetime_is_rejected() { let profile = parse_profile_yaml( diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index fbd2df886e..f6e08e78aa 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -82,6 +82,11 @@ mod linux { const MAX_CONTROL_CONNECTIONS: usize = 128; const MAX_REPLAY_LEDGER_ENTRIES: usize = 4096; const MAX_RETAINED_EXEC_PROCESSES: usize = 64; + // Three stdio pipes require six descriptors before fork, and Rust's + // process launcher may briefly need additional bookkeeping descriptors. + // Reclaim closed mediated sockets before each serialized spawn so stale + // socket sources cannot consume that bounded launch budget. + const EXEC_SPAWN_DESCRIPTOR_HEADROOM: usize = 16; // NVML may traverse the persistenced socket directory during initialization; // WSL2 supplies GPU libraries under /usr/lib/wsl and the /dev/dxg device. @@ -1398,6 +1403,29 @@ mod linux { status: Arc>>, } + async fn retain_exec_completion( + process: Arc, + session: Arc, + status: Arc>>, + ) { + let exit_status = match process.wait().await { + Ok(exit_status) => ExitStatusWire::from(exit_status), + Err(error) => { + // A process wait error is terminal per the BoundaryProcess + // contract. Retaining `None` here makes this handle look live + // forever and can permanently exhaust exec admission. + tracing::warn!(%error, "Exec process wait failed; recording terminal status"); + ExitStatusWire::Exited(74) + } + }; + *lock(&status) = Some(exit_status); + let exit_code = match exit_status { + ExitStatusWire::Exited(code) => code, + ExitStatusWire::Signaled(signal) => 128 + signal, + }; + let _ = session.finish_remote(exit_code, false).await; + } + #[derive(Default)] struct ExecRequestLedger { requests: std::collections::HashSet, @@ -2162,6 +2190,14 @@ mod linux { } requests.reserve(request_id, expires_at)?; drop(requests); + self.network_broker + .ensure_descriptor_headroom(EXEC_SPAWN_DESCRIPTOR_HEADROOM) + .map_err(|error| { + guest_error( + BoundaryErrorKind::Unavailable, + format!("exec descriptor headroom unavailable: {error}"), + ) + })?; let session = self .process_runtime .block_on(executor.exec(spec.into())) @@ -2202,18 +2238,7 @@ mod linux { let wait_session = retained.clone(); let wait_status = status.clone(); self.process_runtime.spawn(async move { - if let Ok(exit_status) = wait_process.wait().await { - *lock(&wait_status) = Some(ExitStatusWire::from(exit_status)); - let exit_code = match exit_status { - openshell_isolation_interface::contract::BoundaryExitStatus::Exited( - code, - ) => code, - openshell_isolation_interface::contract::BoundaryExitStatus::Signaled( - signal, - ) => 128 + signal, - }; - let _ = wait_session.finish_remote(exit_code, false).await; - } + retain_exec_completion(wait_process, wait_session, wait_status).await; }); let handle = ExecHandle { request_id: request_id.to_string(), @@ -3820,6 +3845,52 @@ mod linux { }; use rcgen::{KeyPair, PKCS_ED25519}; + struct FailedWaitProcess; + + #[async_trait::async_trait] + impl BoundaryProcess for FailedWaitProcess { + async fn wait( + &self, + ) -> Result< + openshell_isolation_interface::contract::BoundaryExitStatus, + openshell_isolation_interface::contract::BackendError, + > { + Err( + openshell_isolation_interface::contract::BackendError::Terminated( + "test boundary loss".to_string(), + ), + ) + } + + async fn signal( + &self, + _signal: openshell_isolation_interface::contract::BoundarySignal, + ) -> Result<(), openshell_isolation_interface::contract::BackendError> { + Ok(()) + } + + async fn terminate( + &self, + ) -> Result<(), openshell_isolation_interface::contract::BackendError> { + Ok(()) + } + } + + #[tokio::test] + async fn exec_wait_failure_records_reclaimable_terminal_status() { + let session = MainSession::inert(); + let mut output = session.subscribe(); + let status = Arc::new(Mutex::new(None)); + + retain_exec_completion(Arc::new(FailedWaitProcess), session, status.clone()).await; + + assert_eq!(*lock(&status), Some(ExitStatusWire::Exited(74))); + assert!(matches!( + output.recv().await.expect("terminal exec output"), + MainOutput::Exit(74) + )); + } + #[test] fn exec_tombstones_expire_without_a_lifetime_limit() { let mut ledger = ExecRequestLedger::default(); diff --git a/crates/openshell-sandbox/src/network_broker.rs b/crates/openshell-sandbox/src/network_broker.rs index 8988ae3328..3266b55726 100644 --- a/crates/openshell-sandbox/src/network_broker.rs +++ b/crates/openshell-sandbox/src/network_broker.rs @@ -174,7 +174,7 @@ struct NotificationQueues { pending: mpsc::Sender, dns_relay: DnsRelay, active_opens: Arc, - retained_socket_capacity: usize, + descriptor_soft_limit: usize, decision_timeout: Duration, } @@ -185,6 +185,8 @@ pub struct NetworkBroker { workload_frozen: Arc, pending: Arc>>, pending_dns: Arc>>, + registry: Arc>, + descriptor_soft_limit: usize, dns_address: SocketAddr, healthy: Arc, } @@ -231,7 +233,7 @@ impl NetworkBroker { let active_opens = Arc::new(AtomicUsize::new(0)); let dns_relay = start_dns_relay(dns_address, pending_dns_tx)?; let dns_address = dns_relay.address; - let retained_socket_capacity = retained_socket_capacity()?; + let descriptor_soft_limit = descriptor_soft_limit()?; let registry = Arc::new(Mutex::new(SocketRegistry::new(1, SOCKET_CAPACITY)?)); let provider_files = crate::provider_files::ProviderFiles::default(); let workload_frozen = Arc::new(AtomicBool::new(false)); @@ -243,11 +245,12 @@ impl NetworkBroker { pending: pending_tx, dns_relay, active_opens, - retained_socket_capacity, + descriptor_soft_limit, decision_timeout, }; let healthy = Arc::new(AtomicBool::new(true)); let broker_healthy = healthy.clone(); + let broker_registry = Arc::clone(®istry); std::thread::Builder::new() .name("openshell-network-broker".to_string()) .spawn(move || { @@ -272,7 +275,7 @@ impl NetworkBroker { // broker keeps mediating the rest. let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { dispatch_notification( - Arc::clone(®istry), + Arc::clone(&broker_registry), Arc::clone(&listener), notification, queues.clone(), @@ -314,11 +317,21 @@ impl NetworkBroker { workload_frozen, pending: Arc::new(tokio::sync::Mutex::new(pending_rx)), pending_dns: Arc::new(tokio::sync::Mutex::new(pending_dns_rx)), + registry, + descriptor_soft_limit, dns_address, healthy, }) } + /// Reclaim closed workload sockets before an operation that needs several + /// descriptors at once. Socket creation already protects its own + /// descriptor, but exec startup creates multiple pipes without first + /// issuing another mediated socket syscall. + pub(crate) fn ensure_descriptor_headroom(&self, required: usize) -> io::Result<()> { + ensure_descriptor_headroom(&self.registry, self.descriptor_soft_limit, required) + } + /// Record whether the boundary has stopped the workload for supervisor /// recovery. While frozen, workload requests to send `SIGCONT` are refused /// so a process that was not yet stopped cannot resume the others. Set @@ -571,7 +584,7 @@ fn dispatch_notification( ®istry, &listener, notification, - queues.retained_socket_capacity, + queues.descriptor_soft_limit, ); } if syscall == libc::SYS_connect { @@ -632,7 +645,7 @@ fn create_socket( registry: &Mutex, listener: &NotificationListener, notification: Notification, - retained_socket_capacity: usize, + descriptor_soft_limit: usize, ) -> io::Result<()> { let domain = i32::try_from(notification.args[0]) .map_err(|_| io::Error::from_raw_os_error(libc::EAFNOSUPPORT))?; @@ -663,7 +676,7 @@ fn create_socket( // Reclaim stale descriptors before the broker exhausts its process limit. // Connected sockets remain in the metadata registry without consuming // this broker-owned descriptor budget. - if let Err(error) = prepare_registry_for_socket(registry, retained_socket_capacity) { + if let Err(error) = prepare_registry_for_socket(registry, descriptor_soft_limit) { return listener.respond_errno(notification.id, error_to_errno(&error)); } // SAFETY: arguments were reduced to the supported native INET matrix. A @@ -705,7 +718,7 @@ fn create_socket( Ok(()) } -fn retained_socket_capacity() -> io::Result { +fn descriptor_soft_limit() -> io::Result { let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0, @@ -714,30 +727,75 @@ fn retained_socket_capacity() -> io::Result { if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut limit) } < 0 { return Err(io::Error::last_os_error()); } - let soft_limit = usize::try_from(limit.rlim_cur).unwrap_or(usize::MAX); - let open_descriptors = std::fs::read_dir("/proc/self/fd")?.count(); - Ok(retained_socket_capacity_for_limit( - soft_limit, - open_descriptors, - )) + Ok(usize::try_from(limit.rlim_cur).unwrap_or(usize::MAX)) } -fn retained_socket_capacity_for_limit(soft_limit: usize, open_descriptors: usize) -> usize { - soft_limit - .saturating_sub(open_descriptors) - .saturating_sub(SOCKET_FD_HEADROOM) - .clamp(1, SOCKET_CAPACITY) +fn open_descriptor_count() -> io::Result { + std::fs::read_dir("/proc/self/fd").map(Iterator::count) +} + +fn descriptor_headroom_exhausted( + soft_limit: usize, + open_descriptors: usize, + required: usize, +) -> bool { + open_descriptors.saturating_add(required) > soft_limit } fn prepare_registry_for_socket( registry: &Mutex, - retained_socket_capacity: usize, + descriptor_soft_limit: usize, +) -> io::Result<()> { + // Descriptor use changes after broker startup as control streams and execs + // come and go. Recompute it for each socket request so retained pre-connect + // sockets cannot consume the headroom reserved for those control paths. + prepare_registry_for_socket_with_count(registry, descriptor_soft_limit, open_descriptor_count) +} + +fn prepare_registry_for_socket_with_count( + registry: &Mutex, + descriptor_soft_limit: usize, + mut open_descriptors: impl FnMut() -> io::Result, +) -> io::Result<()> { + let required = SOCKET_FD_HEADROOM.saturating_add(1); + let mut registry = lock(registry); + if registry.is_full() + || descriptor_headroom_exhausted(descriptor_soft_limit, open_descriptors()?, required) + { + collect_closed_socket_entries_locked(&mut registry)?; + } + if registry.is_full() + || descriptor_headroom_exhausted(descriptor_soft_limit, open_descriptors()?, required) + { + return Err(io::Error::from_raw_os_error(libc::EMFILE)); + } + Ok(()) +} + +fn ensure_descriptor_headroom( + registry: &Mutex, + descriptor_soft_limit: usize, + required: usize, +) -> io::Result<()> { + ensure_descriptor_headroom_with_count( + registry, + descriptor_soft_limit, + required, + open_descriptor_count, + ) +} + +fn ensure_descriptor_headroom_with_count( + registry: &Mutex, + descriptor_soft_limit: usize, + required: usize, + mut open_descriptors: impl FnMut() -> io::Result, ) -> io::Result<()> { let mut registry = lock(registry); - if registry.is_full() || registry.retained_preconnect_count() >= retained_socket_capacity { + if descriptor_headroom_exhausted(descriptor_soft_limit, open_descriptors()?, required) { collect_closed_socket_entries_locked(&mut registry)?; } - if registry.is_full() || registry.retained_preconnect_count() >= retained_socket_capacity { + if descriptor_headroom_exhausted(descriptor_soft_limit, open_descriptors()?, required) { return Err(io::Error::from_raw_os_error(libc::EMFILE)); } Ok(()) @@ -1761,18 +1819,15 @@ mod tests { use std::os::unix::net::{UnixListener, UnixStream}; #[test] - fn retained_socket_capacity_reserves_process_descriptor_headroom() { - assert_eq!(retained_socket_capacity_for_limit(1_024, 24), 936); - assert_eq!(retained_socket_capacity_for_limit(128, 32), 32); - assert_eq!(retained_socket_capacity_for_limit(64, 0), 1); - assert_eq!( - retained_socket_capacity_for_limit(usize::MAX, 0), - SOCKET_CAPACITY - ); + fn descriptor_budget_reserves_process_headroom_from_current_usage() { + assert!(!descriptor_headroom_exhausted(1_024, 959, 65)); + assert!(descriptor_headroom_exhausted(1_024, 960, 65)); + assert!(descriptor_headroom_exhausted(64, 0, 65)); + assert!(!descriptor_headroom_exhausted(usize::MAX, 4_096, 65)); } #[test] - fn retained_socket_limit_reclaims_stale_entry_before_opening_another_socket() { + fn descriptor_pressure_reclaims_stale_socket_after_ambient_usage_grows() { // SAFETY: socket returns one newly owned descriptor on success. let fd = unsafe { libc::socket( @@ -1798,13 +1853,19 @@ mod tests { assert_eq!(registry.retained_preconnect_count(), 1); let registry = Mutex::new(registry); - prepare_registry_for_socket(®istry, 1).unwrap(); + let mut observed = std::collections::VecDeque::from([64, 63]); + prepare_registry_for_socket_with_count(®istry, 128, || { + observed + .pop_front() + .ok_or_else(|| io::Error::other("unexpected descriptor recount")) + }) + .unwrap(); assert!(lock(®istry).is_empty()); } #[test] - fn retained_socket_limit_does_not_cap_connected_metadata() { + fn exec_headroom_reclaims_stale_socket_before_pipe_allocation() { // SAFETY: socket returns one newly owned descriptor on success. let fd = unsafe { libc::socket( @@ -1825,6 +1886,42 @@ mod tests { }; let mut registry = SocketRegistry::new(1, 2).unwrap(); let tentative = registry.stage(socket, metadata).unwrap(); + registry.commit(tentative).unwrap(); + + let registry = Mutex::new(registry); + let mut observed = std::collections::VecDeque::from([120, 112]); + ensure_descriptor_headroom_with_count(®istry, 128, 16, || { + observed + .pop_front() + .ok_or_else(|| io::Error::other("unexpected descriptor recount")) + }) + .unwrap(); + + assert!(lock(®istry).is_empty()); + } + + #[test] + fn descriptor_budget_does_not_cap_connected_metadata() { + // SAFETY: socket returns one newly owned descriptor on success. + let fd = unsafe { + libc::socket( + libc::AF_INET, + libc::SOCK_STREAM | libc::SOCK_CLOEXEC, + libc::IPPROTO_TCP, + ) + }; + assert!(fd >= 0, "socket: {}", io::Error::last_os_error()); + // SAFETY: successful socket returned one owned descriptor. + let socket = unsafe { OwnedFd::from_raw_fd(fd) }; + let metadata = SocketMetadata { + family: InetFamily::V4, + kind: InetKind::Tcp, + close_on_exec: true, + nonblocking: false, + creator_generation: 1, + }; + let mut registry = SocketRegistry::new(1, 1).unwrap(); + let tentative = registry.stage(socket, metadata).unwrap(); registry .commit_with_state( tentative, @@ -1834,10 +1931,11 @@ mod tests { ) .unwrap(); assert_eq!(registry.len(), 1); + assert!(registry.is_full()); assert_eq!(registry.retained_preconnect_count(), 0); let registry = Mutex::new(registry); - prepare_registry_for_socket(®istry, 1).unwrap(); + ensure_descriptor_headroom_with_count(®istry, 128, 16, || Ok(32)).unwrap(); assert_eq!(lock(®istry).len(), 1); } diff --git a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs index 486495c956..d8dad148af 100644 --- a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs +++ b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs @@ -149,25 +149,32 @@ fn prepare_capability_free_baseline_at(root: &Path) -> Result { .into_diagnostic()? .create() .into_diagnostic()?; - let entries = capability_free_baseline_entries(root)?; - if entries.is_empty() { - return Err(miette::miette!( - "capability-free Landlock baseline found no usable root entries" - )); - } - for (_, fd) in entries { + let mut usable_entries = 0; + // Consume the lazy iterator directly so each path descriptor is added and + // dropped before the next root entry is opened. Images can contain + // thousands of root entries while the boundary has a 1,024-FD limit. + for entry in capability_free_baseline_entries(root)? { + let (_, fd) = entry?; + usable_entries += 1; let allowed = access_for_path_fd(&fd, access, abi)?; ruleset = ruleset .add_rule(PathBeneath::new(fd, allowed)) .into_diagnostic()?; } + if usable_entries == 0 { + return Err(miette::miette!( + "capability-free Landlock baseline found no usable root entries" + )); + } Ok(PreparedRuleset { ruleset, compatibility: LandlockCompatibility::HardRequirement, }) } -fn capability_free_baseline_entries(root: &Path) -> Result> { +fn capability_free_baseline_entries( + root: &Path, +) -> Result>> { use rustix::fs::{Mode, OFlags, open, openat}; const PRIVATE_ROOT: &str = ".openshell"; @@ -200,27 +207,32 @@ fn capability_free_baseline_entries(root: &Path) -> Result fd, - Err(rustix::io::Errno::NOENT | rustix::io::Errno::ACCESS) => continue, - Err(error) => return Err(error).into_diagnostic(), + Err(rustix::io::Errno::NOENT | rustix::io::Errno::ACCESS) => return None, + Err(error) => return Some(Err(error).into_diagnostic()), + }; + let stat = match rustix::fs::fstat(&fd) { + Ok(stat) => stat, + Err(error) => return Some(Err(error).into_diagnostic()), }; - let stat = rustix::fs::fstat(&fd).into_diagnostic()?; if rustix::fs::FileType::from_raw_mode(stat.st_mode) == rustix::fs::FileType::Symlink { - continue; + return None; } - entries.push((entry.path(), fd)); - } - entries.sort_by(|left, right| left.0.cmp(&right.0)); - Ok(entries) + Some(Ok((path, fd))) + })) } fn prepare_with_path_open_mode( @@ -627,8 +639,7 @@ mod tests { .unwrap(); let paths: Vec<_> = capability_free_baseline_entries(root.path()) .unwrap() - .into_iter() - .map(|(path, _)| path) + .map(|entry| entry.unwrap().0) .collect(); assert_eq!( paths, @@ -652,6 +663,30 @@ mod tests { assert!(capability_free_baseline_entries(root.path()).is_err()); } + #[test] + fn capability_free_baseline_drops_each_root_entry_before_opening_the_next() { + let root = tempfile::tempdir().unwrap(); + for index in 0..128 { + std::fs::write(root.path().join(format!("entry-{index:03}")), b"public").unwrap(); + } + std::fs::create_dir(root.path().join(".openshell")).unwrap(); + + let baseline = std::fs::read_dir("/proc/self/fd").unwrap().count(); + let mut visited = 0; + let mut peak = baseline; + for entry in capability_free_baseline_entries(root.path()).unwrap() { + let _entry = entry.unwrap(); + visited += 1; + peak = peak.max(std::fs::read_dir("/proc/self/fd").unwrap().count()); + } + + assert_eq!(visited, 128); + assert!( + peak <= baseline + 16, + "streamed baseline preparation retained too many descriptors: baseline={baseline} peak={peak}" + ); + } + #[test] fn capability_free_baseline_denies_alias_reads_and_path_truncation() { if !matches!(probe_availability(), LandlockAvailability::Available { abi } if abi >= 3) { diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 9abc5e9e54..9784bea474 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -685,6 +685,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(proto::ProviderResponse::default())) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &self, _: tonic::Request, diff --git a/crates/openshell-server/README.md b/crates/openshell-server/README.md new file mode 100644 index 0000000000..4e35cd7504 --- /dev/null +++ b/crates/openshell-server/README.md @@ -0,0 +1,76 @@ + + +# OpenShell gateway server + +The server owns control-plane RPCs, authentication, provider credential delivery, +and sandbox lifecycle orchestration. The `openshell-gateway` crate composes the +server with selected compute drivers. + +## Operator credential export + +`GetProviderCredentials` exports selected profile-declared runtime environment +keys from one provider in an explicitly named workspace. It requires the distinct +`Principal::Operator`, established only from a verified TLS leaf certificate with +`OU=operator` when `mtls_auth.operator_enabled` is enabled. The TLS listener supplies +its certificate fingerprint separately from user identity; the ordinary +identity-only serving path cannot establish an operator. + +Operators explicitly inherit user/platform-admin permissions, but not sandbox or +peer identities. Descriptor authorization and a handler guard both protect the +export RPC. Operator mutation admission uses a separate caller namespace from +ordinary mTLS users with the same certificate subject. Credential responses never +enter durable mutation replay or generic interceptor routing. + +The export resolver validates the entire selection before minting, resolves only +selected credential handles, and fences provider revisions, refresh epochs, and +profile declarations before delivery. Driver expiration and provider expiration +combine using the earlier timestamp. OAuth minting inputs, broker-only keys, and +supervisor-bound dynamic grants are not exportable. A failed response can still +have committed refresh effects. Missing refresh-owned stored outputs trigger +refresh-on-read; missing static outputs and other credential-backend errors fail +closed without falling back to inline values. + +## Refresh coordination + +Explicit rotation, background refresh, and operator refresh-on-read share the +same refresh coordination: + +- A process-local weak keyed mutex coalesces waiters without retaining deleted + providers indefinitely. +- A bounded admission queue and executor pool limit detached refresh work. +- PostgreSQL deployments also take a dedicated per-provider/per-credential + advisory lock; SQLite uses the local mutex. Worker secret cleanup takes the + same locks and reloads state before changing its resource version. +- A durable `refresh_in_progress` marker precedes upstream minting. A + `refresh_committing` marker protects the interval between grant persistence + and provider-handle persistence. Only a completed provider commit publishes + `refreshed` state, with a unique completed-mint identity in metadata + annotations. Waiters coalesce against that identity, not bookkeeping changes + to the resource version. +- Automatic callers recheck live recovery and retry deadlines under both locks. + A failed mint blocks queued automatic exchanges without disabling scheduled + worker retries or a separately requested manual rotation. +- Detached refresh tasks complete despite request cancellation, preserving + replacement refresh tokens returned by the issuer. +- Existing generation/CAS fencing remains active for deletion and + reconfiguration. An ambiguous rotating-token exchange is parked for recovery + rather than automatically consuming the grant again. + +A crash or lost database-lock session can leave a parked marker. Reconfigure or +reauthorize the refresh grant after investigating its issuer state. Changing a +marker directly is not a supported recovery workflow. + +## Focused verification + +```shell +cargo test -p openshell-server --features test-support,prebuilt-z3 +cargo clippy -p openshell-server --all-targets --features test-support,prebuilt-z3 -- -D warnings +``` + +The ignored `refresh_locks_coordinate_independent_replica_sessions` test requires +`OPENSHELL_REFRESH_TEST_DATABASE_URL` pointing to a disposable PostgreSQL database. +It exercises independent sessions without the local mutex. Do not point this +fixture at an existing deployment database. diff --git a/crates/openshell-server/src/auth/descriptor_authz.rs b/crates/openshell-server/src/auth/descriptor_authz.rs index 7558d14b12..cbdadbdb29 100644 --- a/crates/openshell-server/src/auth/descriptor_authz.rs +++ b/crates/openshell-server/src/auth/descriptor_authz.rs @@ -124,6 +124,7 @@ impl DescriptorAuthTable { "bearer" => AuthMode::Bearer, "dual" => AuthMode::Dual, "peer" => AuthMode::Peer, + "operator" => AuthMode::Operator, other => { return Err(format!("method {path}: unknown auth_mode '{other}'")); } diff --git a/crates/openshell-server/src/auth/guard.rs b/crates/openshell-server/src/auth/guard.rs index a3d55eac71..2ab00a125a 100644 --- a/crates/openshell-server/src/auth/guard.rs +++ b/crates/openshell-server/src/auth/guard.rs @@ -29,7 +29,7 @@ use tracing::info; #[allow(clippy::result_large_err)] pub fn ensure_sandbox_scope(principal: &Principal, claimed_sandbox_id: &str) -> Result<(), Status> { match principal { - Principal::User(_) => Ok(()), + Principal::User(_) | Principal::Operator(_) => Ok(()), Principal::Peer(_) => Err(Status::permission_denied( "gateway peer principals may not call sandbox-scoped methods", )), @@ -87,15 +87,39 @@ pub fn ensure_sandbox_principal_scope( ensure_sandbox_scope(principal, claimed_sandbox_id)?; Ok(p.clone()) } - Principal::User(_) | Principal::Peer(_) => Err(Status::permission_denied( - "supervisor RPCs require a sandbox principal", - )), + Principal::User(_) | Principal::Operator(_) | Principal::Peer(_) => Err( + Status::permission_denied("supervisor RPCs require a sandbox principal"), + ), Principal::Anonymous => Err(Status::unauthenticated( "supervisor RPCs require an authenticated sandbox principal", )), } } +/// Require an operator; ordinary admin authority cannot satisfy this guard. +#[allow(clippy::result_large_err)] +pub fn require_operator( + request: &tonic::Request, +) -> Result<&super::principal::OperatorPrincipal, Status> { + match request.extensions().get::() { + Some(Principal::Operator(operator)) + if operator.identity.provider == super::identity::IdentityProvider::Mtls + && operator + .identity + .roles + .iter() + .any(|role| role == "operator") + && !operator.certificate_sha256.is_empty() => + { + Ok(operator) + } + None | Some(Principal::Anonymous) => { + Err(Status::unauthenticated("operator authentication required")) + } + _ => Err(Status::permission_denied("verified mTLS operator required")), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-server/src/auth/method_authz.rs b/crates/openshell-server/src/auth/method_authz.rs index 06f6784cc8..fb9f3ae461 100644 --- a/crates/openshell-server/src/auth/method_authz.rs +++ b/crates/openshell-server/src/auth/method_authz.rs @@ -24,6 +24,8 @@ pub enum AuthMode { Dual, /// Only callable by a gateway peer principal. Peer, + /// Only a verified mTLS operator may call this method. + Operator, } /// Coarse role mapping. Maps to the configured `admin_role` / @@ -88,11 +90,22 @@ pub fn is_sandbox_callable(method: &str) -> bool { #[must_use] pub fn is_user_callable(method: &str) -> bool { match lookup(method).map(|m| m.auth_mode) { - Some(AuthMode::Sandbox | AuthMode::Unauthenticated | AuthMode::Peer) => false, + Some( + AuthMode::Sandbox | AuthMode::Unauthenticated | AuthMode::Peer | AuthMode::Operator, + ) => false, Some(AuthMode::Bearer | AuthMode::Dual) | None => true, } } +/// Operators have user/admin capabilities, but not supervisor or peer identities. +#[must_use] +pub fn is_operator_callable(method: &str) -> bool { + matches!( + lookup(method).map(|entry| entry.auth_mode), + Some(AuthMode::Operator | AuthMode::Bearer | AuthMode::Dual) + ) +} + /// `true` if the method is callable by a gateway peer. #[must_use] pub fn is_peer_callable(method: &str) -> bool { diff --git a/crates/openshell-server/src/auth/principal.rs b/crates/openshell-server/src/auth/principal.rs index 298533278a..7a4a316d62 100644 --- a/crates/openshell-server/src/auth/principal.rs +++ b/crates/openshell-server/src/auth/principal.rs @@ -24,6 +24,8 @@ use super::identity::Identity; pub enum Principal { /// Human caller authenticated via OIDC (Keycloak, Entra ID, Okta, etc.). User(UserPrincipal), + /// Gateway-wide administrator authenticated only by a verified operator certificate. + Operator(OperatorPrincipal), /// Sandbox supervisor authenticated by an identity bound to a specific /// sandbox UUID. The wrapped `sandbox_id` MUST match any sandbox referenced /// in the request body for sandbox-class methods. @@ -43,6 +45,26 @@ pub struct UserPrincipal { pub identity: Identity, } +/// Certificate-authenticated gateway operator. Never constructed from bearer claims. +#[derive(Debug, Clone)] +pub struct OperatorPrincipal { + /// Verified mTLS certificate subject and roles. + pub identity: Identity, + /// SHA-256 fingerprint of the verified leaf certificate (not a private key). + pub certificate_sha256: String, +} + +impl Principal { + /// Identity of a human/API caller, preserving its distinct principal variant. + pub fn user_identity(&self) -> Option<&Identity> { + match self { + Self::User(user) => Some(&user.identity), + Self::Operator(operator) => Some(&operator.identity), + _ => None, + } + } +} + /// Sandbox caller — bound to one specific sandbox UUID. /// /// `sandbox_id` and `source` are consumed by the router and handler guards. diff --git a/crates/openshell-server/src/auth/workspace_authz.rs b/crates/openshell-server/src/auth/workspace_authz.rs index 0aa38fe0bb..0b8e351b08 100644 --- a/crates/openshell-server/src/auth/workspace_authz.rs +++ b/crates/openshell-server/src/auth/workspace_authz.rs @@ -186,6 +186,10 @@ pub async fn authorize_workspace( grant: AuthGrant::Member(member_role), }) } + Principal::Operator(_) => Ok(AuthorizedWorkspace { + workspace, + grant: AuthGrant::PlatformAdmin, + }), Principal::Sandbox(_) => Ok(AuthorizedWorkspace { workspace, grant: AuthGrant::Sandbox, @@ -219,6 +223,7 @@ pub async fn authorize_sandbox_workspace( #[allow(clippy::result_large_err)] pub fn require_platform_admin(admin_role: &str, principal: &Principal) -> Result<(), Status> { match principal { + Principal::Operator(_) => Ok(()), Principal::User(user) if is_platform_admin(&user.identity.roles, admin_role) => Ok(()), Principal::User(_) => Err(Status::permission_denied( "platform admin role required for cross-workspace operations", diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index bfe237af39..001127761e 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -192,6 +192,10 @@ struct RunArgs { )] enable_mtls_auth: bool, + /// Enable gateway-wide mTLS operators (trusted certificate OU=operator). + #[arg(long, env = "OPENSHELL_ENABLE_OPERATOR_AUTH", default_value_t = false, action = ArgAction::Set)] + enable_operator_auth: bool, + /// Expected OIDC audience claim (typically the client ID). #[arg(long, env = "OPENSHELL_OIDC_AUDIENCE", default_value = "openshell-cli")] oidc_audience: String, @@ -388,6 +392,11 @@ fn prepare_server_config_with_drivers( "--disable-tls and --tls-client-ca are mutually exclusive. Client certificate verification requires that TLS be enabled." )); } + if args.enable_operator_auth && (args.disable_tls || !has_client_ca) { + return Err(miette::miette!( + "operator authentication requires TLS and --tls-client-ca" + )); + } if mtls_auth_enabled && args.disable_tls { return Err(miette::miette!( "mTLS user authentication requires TLS. Remove --disable-tls or disable --enable-mtls-auth." @@ -455,6 +464,7 @@ fn prepare_server_config_with_drivers( config.auth = auth; } config.mtls_auth.enabled = mtls_auth_enabled; + config.mtls_auth.operator_enabled = args.enable_operator_auth; // Listener addresses for the health and metrics endpoints. The file may // pin a different interface than the main listener (e.g. health on @@ -676,6 +686,11 @@ async fn run_from_args( if prepared.config.mtls_auth.enabled { info!("mTLS user authentication enabled"); } + if prepared.config.mtls_auth.operator_enabled { + info!( + "mTLS operator authentication enabled (OU=operator grants gateway-wide administration)" + ); + } if has_oidc { info!("OIDC authentication enabled"); } @@ -699,6 +714,7 @@ async fn run_from_args( if !prepared.config.auth.allow_unauthenticated_users && !prepared.config.mtls_auth.enabled + && !prepared.config.mtls_auth.operator_enabled && !has_oidc && prepared.config.gateway_jwt.is_none() { @@ -971,6 +987,11 @@ fn validate_preflight_semantics( "--disable-tls and --tls-client-ca are mutually exclusive" )); } + if args.enable_operator_auth && (args.disable_tls || !has_client_ca) { + return Err(miette::miette!( + "operator authentication requires TLS and --tls-client-ca" + )); + } if mtls_auth_enabled && args.disable_tls { return Err(miette::miette!("mTLS user authentication requires TLS")); } @@ -1179,6 +1200,11 @@ fn merge_file_into_args(args: &mut RunArgs, file: &GatewayFileSection, matches: { args.enable_mtls_auth = mtls_auth.enabled; } + if let Some(mtls_auth) = &file.mtls_auth + && arg_defaulted(matches, "enable_operator_auth") + { + args.enable_operator_auth = mtls_auth.operator_enabled; + } if let Some(disabled) = file.disable_tls && arg_defaulted(matches, "disable_tls") { @@ -2825,6 +2851,86 @@ enabled = false )); } + #[test] + fn operator_auth_is_opt_in_and_file_flag_precedence_is_explicit() { + let _lock = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let _env = EnvVarGuard::remove("OPENSHELL_ENABLE_OPERATOR_AUTH"); + let file = config_file_from_toml( + "[openshell.gateway.mtls_auth]\noperator_enabled = true\nenabled = false\n", + ); + let (mut args, matches) = parse_with_args(&["openshell-gateway"]); + assert!(!args.enable_operator_auth); + merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + assert!(args.enable_operator_auth); + assert!(!args.enable_mtls_auth); + let (mut args, matches) = + parse_with_args(&["openshell-gateway", "--enable-operator-auth", "false"]); + merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + assert!(!args.enable_operator_auth); + } + + #[test] + fn operator_preflight_requires_tls_and_ca_independently_of_user_auth() { + let _lock = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let config_home = tempfile::tempdir().unwrap(); + let _home = EnvVarGuard::set("XDG_CONFIG_HOME", config_home.path().to_str().unwrap()); + let _config = EnvVarGuard::remove("OPENSHELL_GATEWAY_CONFIG"); + let _legacy = EnvVarGuard::remove("OPENSHELL_DRIVERS"); + let _env = EnvVarGuard::remove("OPENSHELL_ENABLE_OPERATOR_AUTH"); + let registry = test_registry("shared", false); + for tls_args in [vec!["--disable-tls"], vec![]] { + let mut argv = vec![ + "openshell-gateway", + "--compute-driver", + "shared", + "--enable-operator-auth", + "true", + "--enable-mtls-auth", + "false", + ]; + argv.extend(tls_args); + let (run, matches) = parse_with_args(&argv); + let error = super::run_config_preflight_with_drivers( + super::ConfigPreflightArgs::default(), + run, + &matches, + ®istry, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("operator authentication requires TLS") + ); + } + let (run, matches) = parse_with_args(&[ + "openshell-gateway", + "--compute-driver", + "shared", + "--enable-operator-auth", + "true", + "--enable-mtls-auth", + "false", + "--tls-cert", + "/tls/server.pem", + "--tls-key", + "/tls/key.pem", + "--tls-client-ca", + "/tls/ca.pem", + ]); + super::run_config_preflight_with_drivers( + super::ConfigPreflightArgs::default(), + run, + &matches, + ®istry, + ) + .unwrap(); + } + #[test] fn file_value_applies_when_cli_uses_default() { let _lock = ENV_LOCK diff --git a/crates/openshell-server/src/grpc/auth_rpc.rs b/crates/openshell-server/src/grpc/auth_rpc.rs index 848adae6f5..5ba6b4fef7 100644 --- a/crates/openshell-server/src/grpc/auth_rpc.rs +++ b/crates/openshell-server/src/grpc/auth_rpc.rs @@ -34,13 +34,12 @@ pub async fn handle_get_current_user( request: Request, ) -> Result, Status> { let principal = super::extract_principal(&request)?; - let Principal::User(user) = principal else { + let Some(identity) = principal.user_identity().cloned() else { return Err(Status::permission_denied( "GetCurrentUser requires a user principal", )); }; - let identity = user.identity; Ok(Response::new(GetCurrentUserResponse { subject: identity.subject, display_name: identity.display_name.unwrap_or_default(), diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 5ac58307a4..fee2c5e8f9 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -7,6 +7,7 @@ mod auth_rpc; pub mod mutation_replay; pub mod policy; pub mod provider; +mod provider_credentials; pub mod provider_readiness; mod sandbox; pub use sandbox::mint_persisted_authentication; @@ -505,6 +506,13 @@ impl OpenShell for OpenShellService { mutation_replay::run(&self.state, request).await } + async fn get_provider_credentials( + &self, + request: Request, + ) -> Result, Status> { + provider_credentials::handle(&self.state, request).await + } + async fn get_provider( &self, request: Request, diff --git a/crates/openshell-server/src/grpc/mutation_replay.rs b/crates/openshell-server/src/grpc/mutation_replay.rs index 19ce936888..a331ea2ec6 100644 --- a/crates/openshell-server/src/grpc/mutation_replay.rs +++ b/crates/openshell-server/src/grpc/mutation_replay.rs @@ -150,7 +150,7 @@ pub(super) async fn run( } let request_id = validate_request_id(request.get_ref().request_id())?; let principal = super::extract_principal(&request)?; - let Principal::User(user) = &principal else { + let Some(identity) = principal.user_identity() else { return Err(Status::permission_denied( "request admission requires a user principal", )); @@ -183,7 +183,7 @@ pub(super) async fn run( } else { (fingerprint(original)?, None) }; - let (provider, issuer) = match user.identity.provider { + let (provider, issuer) = match identity.provider { IdentityProvider::Oidc => ( "oidc", state @@ -192,15 +192,18 @@ pub(super) async fn run( .as_ref() .map_or("", |config| config.issuer.as_str()), ), - IdentityProvider::Mtls => ("mtls", ""), + IdentityProvider::Mtls => ( + if matches!(principal, Principal::Operator(_)) { + "mtls-operator" + } else { + "mtls" + }, + "", + ), IdentityProvider::CloudflareAccess => ("cloudflare_access", ""), IdentityProvider::LocalDev => ("local_dev", ""), }; - let caller = hash_json(&serde_json::json!([ - provider, - issuer, - user.identity.subject, - ]))?; + let caller = hash_json(&serde_json::json!([provider, issuer, identity.subject,]))?; // Not a valid user workspace name; ordinary workspace cleanup cannot touch // these records. The bucket supports an atomic per-caller storage quota. let bucket = format!("_mutation/{caller}"); diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 4a0aa0fbc2..8edf83139e 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -2688,7 +2688,9 @@ async fn resolve_sandbox_by_name_for_principal( )?; Ok(sandbox) } - Principal::User(_) => sandbox.ok_or_else(|| Status::not_found("sandbox not found")), + Principal::User(_) | Principal::Operator(_) => { + sandbox.ok_or_else(|| Status::not_found("sandbox not found")) + } Principal::Peer(_) => Err(Status::permission_denied( "gateway peer principals may not resolve sandbox configuration", )), diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index ff8189eab6..9d8b242bd8 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -2377,7 +2377,7 @@ fn broker_only_provider_credential_keys_for_provider( .unwrap_or_default() } -fn broker_only_provider_credential_keys(profile: &ProviderProfile) -> HashSet { +pub(super) fn broker_only_provider_credential_keys(profile: &ProviderProfile) -> HashSet { profile .credentials .iter() @@ -2404,7 +2404,7 @@ fn provider_credential_not_expired(provider: &Provider, key: &str, now_ms: i64) .is_none_or(|expiration_ms| expiration_ms > now_ms) } -fn is_non_injectable_provider_credential(provider: &Provider, key: &str) -> bool { +pub(super) fn is_non_injectable_provider_credential(provider: &Provider, key: &str) -> bool { normalize_profile_id(&provider.r#type).as_deref() == Some("google-vertex-ai") && key == "GOOGLE_SERVICE_ACCOUNT_KEY" } diff --git a/crates/openshell-server/src/grpc/provider_credentials.rs b/crates/openshell-server/src/grpc/provider_credentials.rs new file mode 100644 index 0000000000..61666d4d2c --- /dev/null +++ b/crates/openshell-server/src/grpc/provider_credentials.rs @@ -0,0 +1,503 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Privileged export of selected runtime credentials, not raw storage access. + +#![allow(clippy::result_large_err)] + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::Duration; + +use openshell_core::ObjectId; +use openshell_core::proto::{ + GetProviderCredentialsRequest, GetProviderCredentialsResponse, Provider, + ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStrategy, + ProviderCredentialValue, +}; +use tonic::{Request, Response, Status}; + +use crate::ServerState; +use crate::storage_proto::StoredProviderCredentialRefreshStateV2; + +#[cfg(test)] +#[path = "provider_credentials_tests.rs"] +mod tests; + +const MAX_KEYS: usize = 32; +const MAX_RESPONSE_BYTES: usize = 1024 * 1024; +const DEFAULT_LIFETIME: Duration = Duration::from_mins(5); +const MAX_LIFETIME: Duration = Duration::from_hours(24); + +#[derive(Default)] +struct RefreshAudit { + attempts: usize, + completed: usize, +} + +pub(super) async fn handle( + state: &Arc, + request: Request, +) -> Result, Status> { + let operator = crate::auth::guard::require_operator(&request)?; + let subject = operator.identity.subject.clone(); + let fingerprint = operator.certificate_sha256.clone(); + let request = request.into_inner(); + // Validate before logging selectors: a malformed key could contain a value. + let selection = + crate::auth::workspace_authz::selected_workspace_name(request.workspace_scope.as_ref()) + .and_then(|workspace| validate(&request).map(|lifetime| (workspace, lifetime))); + let (workspace, lifetime) = match selection { + Ok(selection) => selection, + Err(error) => { + tracing::info!(operator_subject = %subject, certificate_sha256 = %fingerprint, + outcome = "invalid_request", status = ?error.code(), "operator provider credential retrieval"); + return Err(error); + } + }; + // Admission survives RPC cancellation, while completion records the refresh + // effects even if the final all-or-nothing delivery fails. + tracing::info!( + operator_subject = %subject, + certificate_sha256 = %fingerprint, + workspace, + provider = %request.name, + credential_keys = ?request.credential_keys, + outcome = "requested", + "operator provider credential retrieval" + ); + let mut refresh_audit = RefreshAudit::default(); + let result = retrieve(state, workspace, &request, lifetime, &mut refresh_audit).await; + tracing::info!( + operator_subject = %subject, + certificate_sha256 = %fingerprint, + workspace, + provider = %request.name, + credential_keys = ?request.credential_keys, + outcome = if result.is_ok() { "delivered" } else { "failed" }, + refresh_attempts = refresh_audit.attempts, + refresh_completed = refresh_audit.completed, + "operator provider credential retrieval" + ); + result.map(Response::new) +} + +fn validate(request: &GetProviderCredentialsRequest) -> Result { + if request.name.is_empty() || request.name.trim() != request.name || request.name.len() > 253 { + return Err(Status::invalid_argument( + "canonical provider name is required", + )); + } + if request.credential_keys.is_empty() || request.credential_keys.len() > MAX_KEYS { + return Err(Status::invalid_argument( + "select between 1 and 32 runtime credential keys", + )); + } + let mut seen = HashSet::new(); + for key in &request.credential_keys { + if key.len() > 256 || !super::provider::is_valid_env_key(key) || !seen.insert(key) { + return Err(Status::invalid_argument( + "credential keys must be unique runtime environment keys", + )); + } + } + let lifetime = request + .minimum_remaining_lifetime + .as_ref() + .map(openshell_core::time::duration_to_std) + .transpose() + .map_err(|_| Status::invalid_argument("invalid minimum_remaining_lifetime"))? + .filter(|duration| !duration.is_zero()) + .unwrap_or(DEFAULT_LIFETIME); + if lifetime > MAX_LIFETIME { + return Err(Status::invalid_argument( + "minimum_remaining_lifetime exceeds 24 hours", + )); + } + Ok(lifetime) +} + +async fn load(state: &ServerState, workspace: &str, name: &str) -> Result { + state + .store + .get_message_by_name::(workspace, name) + .await + .map_err(|_| Status::internal("provider lookup failed"))? + .ok_or_else(|| Status::not_found("provider not found")) +} + +fn sanitize(error: Status) -> Status { + // Driver diagnostics may contain backend details. Never propagate them to + // the public export API, or attach them to its audit record. + Status::new( + error.code(), + "runtime credential retrieval failed; inspect provider refresh status", + ) +} + +async fn retrieve( + state: &ServerState, + workspace: &str, + request: &GetProviderCredentialsRequest, + lifetime: Duration, + refresh_audit: &mut RefreshAudit, +) -> Result { + let provider = load(state, workspace, &request.name).await?; + let catalog = state + .provider_profile_sources + .snapshot_catalog(state.store.as_ref(), workspace) + .await + .map_err(sanitize)?; + let profile = super::provider::get_provider_type_profile_for_scope( + &catalog, + &provider.r#type, + &provider.profile_workspace, + ) + .ok_or_else(|| { + Status::failed_precondition("provider has no supported runtime credential profile") + })? + .to_proto(); + let broker_keys = super::provider::broker_only_provider_credential_keys(&profile); + // Prevalidate the entire selection before resolving secrets or minting. + for key in &request.credential_keys { + let declaration = profile + .credentials + .iter() + .find(|credential| credential.env_vars.contains(key)); + if broker_keys.contains(key) + || super::provider::is_non_injectable_provider_credential(&provider, key) + || declaration.is_none() + || declaration.is_some_and(|credential| credential.token_grant.is_some()) + { + return Err(Status::failed_precondition( + "selected key is not an exportable static or gateway-refresh-managed runtime credential", + )); + } + } + let refreshes = crate::provider_refresh::list_refresh_states_for_provider( + state.store.as_ref(), + provider.object_id(), + ) + .await + .map_err(sanitize)?; + let mut owners = HashMap::new(); + for key in &request.credential_keys { + let matching: Vec<_> = refreshes + .iter() + .filter(|refresh| { + refresh.credential_key == *key + || refresh + .additional_output_keys + .values() + .any(|output| output == key) + }) + .collect(); + if matching.len() > 1 { + return Err(Status::failed_precondition( + "runtime credential has ambiguous refresh ownership", + )); + } + if let Some(refresh) = matching.first() { + if refresh + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + { + return Err(Status::failed_precondition( + "credential refresh is being deleted", + )); + } + owners.insert(key.clone(), (*refresh).clone()); + } + if !provider.credentials.contains_key(key) + && !provider.credential_handles.contains_key(key) + && !owners.contains_key(key) + { + return Err(Status::not_found("selected runtime credential is missing")); + } + } + // State and provider reads are not a transaction. Reload after observing + // refresh generations so a just-completed mint is reused, not repeated + // against an old provider snapshot paired with a new refresh generation. + let initial_provider = load(state, workspace, &request.name).await?; + if initial_provider.object_id() != provider.object_id() + || initial_provider.r#type != provider.r#type + || initial_provider.profile_workspace != provider.profile_workspace + { + return Err(Status::aborted("provider was replaced or reconfigured")); + } + // Resolve initial handles individually so NOT_FOUND can be attributed to + // a refresh-owned output. A missing static handle or any other backend + // error still fails closed, without exporting an inline fallback. + let mut initial = HashMap::new(); + for key in &request.credential_keys { + match resolve_selected(state, &initial_provider, std::slice::from_ref(key)).await { + Ok(values) => initial.extend(values), + Err(error) if error.code() == tonic::Code::NotFound && owners.contains_key(key) => {} + Err(error) => return Err(error), + } + } + let mut minted = HashSet::new(); + for key in &request.credential_keys { + if sufficiently_valid(initial.get(key), lifetime)? { + continue; + } + let refresh = owners.get(key).ok_or_else(|| { + Status::failed_precondition("credential cannot meet the requested remaining lifetime") + })?; + ensure_refreshable(refresh)?; + if minted.insert(refresh.object_id().to_string()) { + refresh_audit.attempts += 1; + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + refresh.clone(), + ) + .await + .map_err(sanitize)?; + refresh_audit.completed += 1; + } + } + // Capture refresh generations before resolving the final provider snapshot. + let mut delivery_owners = HashMap::new(); + for owner in owners.values() { + let latest = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + workspace, + provider.object_id(), + &owner.credential_key, + ) + .await + .map_err(sanitize)? + .ok_or_else(|| Status::aborted("credential refresh was deleted"))?; + if latest.object_id() != owner.object_id() + || crate::provider_refresh::effective_authorization_epoch(&latest)? + != crate::provider_refresh::effective_authorization_epoch(owner)? + || latest + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + || matches!( + latest.status.as_str(), + "refresh_in_progress" | "refresh_committing" + ) + { + return Err(Status::aborted( + "credential refresh changed during retrieval", + )); + } + delivery_owners.insert(owner.credential_key.clone(), latest); + } + let current = load(state, workspace, &request.name).await?; + if current.object_id() != provider.object_id() + || current.r#type != provider.r#type + || current.profile_workspace != provider.profile_workspace + { + return Err(Status::aborted("provider was replaced or reconfigured")); + } + // Fresh handles/expiration come from committed state, never transient minted values. + let values = resolve_selected(state, ¤t, &request.credential_keys).await?; + for key in &request.credential_keys { + if !sufficiently_valid(values.get(key), lifetime)? { + return Err(Status::failed_precondition( + "credential cannot meet the requested remaining lifetime", + )); + } + } + let latest = load(state, workspace, &request.name).await?; + if latest + .metadata + .as_ref() + .map(|metadata| (&metadata.id, metadata.resource_version)) + != current + .metadata + .as_ref() + .map(|metadata| (&metadata.id, metadata.resource_version)) + { + return Err(Status::aborted( + "provider changed during credential resolution", + )); + } + // A refresh grant can be reconfigured without a provider update. + for owner in delivery_owners.values() { + let latest = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + workspace, + current.object_id(), + &owner.credential_key, + ) + .await + .map_err(sanitize)? + .ok_or_else(|| Status::aborted("credential refresh was deleted"))?; + if latest + .metadata + .as_ref() + .map(|metadata| (&metadata.id, metadata.resource_version)) + != owner + .metadata + .as_ref() + .map(|metadata| (&metadata.id, metadata.resource_version)) + || crate::provider_refresh::effective_authorization_epoch(&latest)? + != crate::provider_refresh::effective_authorization_epoch(owner)? + || latest + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + { + return Err(Status::aborted("credential refresh was reconfigured")); + } + } + let latest_catalog = state + .provider_profile_sources + .snapshot_catalog(state.store.as_ref(), workspace) + .await + .map_err(sanitize)?; + let latest_profile = super::provider::get_provider_type_profile_for_scope( + &latest_catalog, + ¤t.r#type, + ¤t.profile_workspace, + ) + .map(|profile| profile.to_proto()); + if latest_profile.as_ref() != Some(&profile) { + return Err(Status::aborted( + "provider credential declarations changed during retrieval", + )); + } + if values + .iter() + .map(|(key, value)| key.len() + value.value.len() + 64) + .sum::() + > MAX_RESPONSE_BYTES + { + return Err(Status::resource_exhausted( + "credential response exceeds 1 MiB", + )); + } + // Recheck at delivery, after all driver and database round trips. + for value in values.values() { + if !sufficiently_valid(Some(value), lifetime)? { + return Err(Status::failed_precondition( + "credential lifetime elapsed during resolution", + )); + } + } + Ok(GetProviderCredentialsResponse { + credentials: values, + }) +} + +fn ensure_refreshable(refresh: &StoredProviderCredentialRefreshStateV2) -> Result<(), Status> { + let strategy = ProviderCredentialRefreshStrategy::try_from(refresh.strategy) + .unwrap_or(ProviderCredentialRefreshStrategy::Unspecified); + if !matches!( + strategy, + ProviderCredentialRefreshStrategy::Oauth2RefreshToken + | ProviderCredentialRefreshStrategy::Oauth2ClientCredentials + | ProviderCredentialRefreshStrategy::GoogleServiceAccountJwt + | ProviderCredentialRefreshStrategy::AwsStsAssumeRole + ) { + return Err(Status::failed_precondition( + "credential has no gateway-managed minting strategy", + )); + } + // Wait for an active mint through shared coordination. If its owner died, + // the durable marker is still rejected after acquiring the distributed lock. + if matches!( + refresh.status.as_str(), + "refresh_in_progress" | "refresh_committing" + ) { + return Ok(()); + } + if matches!( + ProviderCredentialRefreshRecoveryAction::try_from(refresh.recovery_action), + Ok(ProviderCredentialRefreshRecoveryAction::Reauthorize + | ProviderCredentialRefreshRecoveryAction::FixConfiguration + | ProviderCredentialRefreshRecoveryAction::Investigate) + ) { + return Err(Status::failed_precondition( + "credential refresh requires recovery", + )); + } + if refresh.recovery_action == ProviderCredentialRefreshRecoveryAction::Retry as i32 + && refresh.next_refresh_at_ms > crate::persistence::current_time_ms() + { + return Err(Status::unavailable( + "credential refresh is in retry backoff", + )); + } + Ok(()) +} + +async fn resolve_selected( + state: &ServerState, + provider: &Provider, + keys: &[String], +) -> Result, Status> { + let mut selected = provider.clone(); + selected + .credential_handles + .retain(|key, _| keys.contains(key)); + // Expired handle-backed credentials are omitted by the shared runtime and + // consequently trigger refresh, or a failure for nonrefreshable credentials. + let resolved = state + .credentials + .resolve_provider_handles(&selected, crate::persistence::current_time_ms()) + .await + .map_err(sanitize)?; + let mut result = HashMap::new(); + for key in keys { + if provider.credential_handles.contains_key(key) { + if let Some(value) = resolved.values.get(key) { + result.insert( + key.clone(), + ProviderCredentialValue { + value: value.clone(), + expiration_time: resolved + .expires_at_ms + .get(key) + .map(|expiration| { + openshell_core::time::timestamp_from_millis(*expiration) + }) + .transpose() + .map_err(|_| { + Status::failed_precondition("invalid credential expiration") + })?, + }, + ); + } + } else if let Some(value) = provider.credentials.get(key) { + result.insert( + key.clone(), + ProviderCredentialValue { + value: value.clone(), + expiration_time: provider.credential_expiration_times.get(key).copied(), + }, + ); + } + } + Ok(result) +} + +fn sufficiently_valid( + value: Option<&ProviderCredentialValue>, + lifetime: Duration, +) -> Result { + let Some(value) = value else { + return Ok(false); + }; + let Some(expiration) = value.expiration_time.as_ref() else { + return Ok(true); + }; + let expiration = openshell_core::time::timestamp_to_millis(expiration) + .map_err(|_| Status::failed_precondition("invalid credential expiration"))?; + let millis = + lifetime.as_millis() + u128::from(!lifetime.subsec_nanos().is_multiple_of(1_000_000)); + let deadline = crate::persistence::current_time_ms() + .checked_add( + i64::try_from(millis) + .map_err(|_| Status::invalid_argument("invalid remaining lifetime"))?, + ) + .ok_or_else(|| Status::invalid_argument("invalid remaining lifetime"))?; + Ok(expiration > deadline) +} diff --git a/crates/openshell-server/src/grpc/provider_credentials_tests.rs b/crates/openshell-server/src/grpc/provider_credentials_tests.rs new file mode 100644 index 0000000000..8e4c6a2fde --- /dev/null +++ b/crates/openshell-server/src/grpc/provider_credentials_tests.rs @@ -0,0 +1,860 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::auth::identity::{Identity, IdentityProvider}; +use crate::auth::principal::{OperatorPrincipal, Principal}; +use openshell_core::proto::{ + ObjectMeta, ProviderProfile, ProviderProfileCredential, workspace_selector, +}; +use tonic::Code; + +fn operator() -> Principal { + Principal::Operator(OperatorPrincipal { + identity: Identity { + subject: "operator-test".into(), + display_name: None, + roles: vec!["operator".into()], + scopes: vec![], + provider: IdentityProvider::Mtls, + }, + certificate_sha256: "verified-test-certificate".into(), + }) +} + +fn request(keys: &[&str]) -> GetProviderCredentialsRequest { + GetProviderCredentialsRequest { + workspace_scope: Some(workspace_selector("other")), + name: "provider".into(), + credential_keys: keys.iter().map(|key| (*key).into()).collect(), + minimum_remaining_lifetime: None, + } +} + +fn authenticated(inner: GetProviderCredentialsRequest) -> Request { + let mut request = Request::new(inner); + request.extensions_mut().insert(operator()); + request +} + +async fn fixture() -> (Arc, Provider) { + let state = crate::grpc::test_support::test_server_state().await; + state + .store + .put_message(&crate::provider_profile_sources::stored_provider_profile( + ProviderProfile { + id: "operator-test".into(), + credentials: vec![ + ProviderProfileCredential { + name: "access_token".into(), + env_vars: vec!["ACCESS_TOKEN".into()], + ..Default::default() + }, + ProviderProfileCredential { + name: "static_key".into(), + env_vars: vec!["STATIC_KEY".into()], + ..Default::default() + }, + ], + ..Default::default() + }, + )) + .await + .unwrap(); + let provider = Provider { + metadata: Some(ObjectMeta { + id: uuid::Uuid::new_v4().to_string(), + name: "provider".into(), + workspace: "other".into(), + ..Default::default() + }), + r#type: "operator-test".into(), + credentials: HashMap::from([ + ("ACCESS_TOKEN".into(), "old-access-token".into()), + ("STATIC_KEY".into(), "static-secret".into()), + ("REFRESH_TOKEN".into(), "must-not-export".into()), + ]), + ..Default::default() + }; + state.store.put_message(&provider).await.unwrap(); + (state, provider) +} + +#[tokio::test] +async fn audits_exports_and_invalid_attempts_without_values() { + struct AuditWriter(Arc>>); + impl std::io::Write for AuditWriter { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + let (state, _) = fixture().await; + let output = Arc::new(std::sync::Mutex::new(Vec::new())); + let writer = output.clone(); + let subscriber = tracing_subscriber::fmt() + .without_time() + .with_ansi(false) + .with_writer(move || AuditWriter(writer.clone())) + .finish(); + let _guard = tracing::subscriber::set_default(subscriber); + handle( + &state, + authenticated(request(&["ACCESS_TOKEN", "STATIC_KEY"])), + ) + .await + .unwrap(); + let error = handle( + &state, + authenticated(request(&["API_KEY=secret-from-invalid-input"])), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::InvalidArgument); + let output = String::from_utf8(output.lock().unwrap().clone()).unwrap(); + assert!(output.contains("operator-test")); + assert!(output.contains("verified-test-certificate")); + assert!(output.contains("ACCESS_TOKEN")); + assert!(output.contains("STATIC_KEY")); + assert!(output.contains("delivered")); + assert!(output.contains("invalid_request")); + assert!(output.contains("refresh_attempts=0")); + for secret in [ + "old-access-token", + "static-secret", + "must-not-export", + "secret-from-invalid-input", + ] { + assert!(!output.contains(secret)); + } +} + +#[tokio::test] +async fn exports_only_selected_runtime_keys_across_workspaces() { + let (state, _) = fixture().await; + let response = handle(&state, authenticated(request(&["STATIC_KEY"]))) + .await + .unwrap() + .into_inner(); + assert_eq!(response.credentials.len(), 1); + assert_eq!(response.credentials["STATIC_KEY"].value, "static-secret"); + assert!(response.credentials["STATIC_KEY"].expiration_time.is_none()); + let error = handle( + &state, + authenticated(request(&["STATIC_KEY", "REFRESH_TOKEN"])), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(!error.message().contains("must-not-export")); +} + +#[tokio::test] +async fn rejects_ordinary_admin_and_anonymous() { + let (state, _) = fixture().await; + assert_eq!( + handle( + &state, + crate::grpc::test_support::authed_request(request(&["STATIC_KEY"])) + ) + .await + .unwrap_err() + .code(), + Code::PermissionDenied + ); + assert_eq!( + handle(&state, Request::new(request(&["STATIC_KEY"]))) + .await + .unwrap_err() + .code(), + Code::Unauthenticated + ); +} + +#[test] +fn validates_selection_and_lifetime() { + assert!(validate(&request(&[])).is_err()); + assert!(validate(&request(&["STATIC_KEY", "STATIC_KEY"])).is_err()); + assert!(validate(&request(&["../handle"])).is_err()); + let mut req = request(&["STATIC_KEY"]); + req.minimum_remaining_lifetime = Some(prost_types::Duration { + seconds: -1, + nanos: 0, + }); + assert!(validate(&req).is_err()); + req.minimum_remaining_lifetime = Some(prost_types::Duration { + seconds: 86401, + nanos: 0, + }); + assert!(validate(&req).is_err()); + req.minimum_remaining_lifetime = Some(prost_types::Duration::default()); + assert_eq!(validate(&req).unwrap(), DEFAULT_LIFETIME); +} + +#[tokio::test] +async fn lifetime_failure_does_not_return_partial_values() { + let (state, mut provider) = fixture().await; + provider.credential_expiration_times.insert( + "ACCESS_TOKEN".into(), + openshell_core::time::timestamp_from_millis(crate::persistence::current_time_ms() + 1000) + .unwrap(), + ); + state.store.put_message(&provider).await.unwrap(); + assert_eq!( + handle( + &state, + authenticated(request(&["STATIC_KEY", "ACCESS_TOKEN"])) + ) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); +} + +async fn refresh_fixture( + state: &ServerState, + provider: &Provider, + endpoint: String, +) -> StoredProviderCredentialRefreshStateV2 { + let refresh = crate::provider_refresh::new_refresh_state( + provider, + "other", + "ACCESS_TOKEN", + crate::provider_refresh::NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken, + material: HashMap::from([ + ("refresh_token".into(), "private-refresh-token".into()), + ("client_id".into(), "client".into()), + ]), + secret_material_keys: vec!["refresh_token".into()], + expires_at_ms: 1, + token_url: endpoint, + scopes: vec![], + refresh_before: None, + max_lifetime: None, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + crate::provider_refresh::put_refresh_state(state.store.as_ref(), &refresh) + .await + .unwrap(); + crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap() +} + +#[tokio::test] +async fn refreshes_access_token_and_reuses_it() { + let (state, mut provider) = fixture().await; + provider.credential_expiration_times.insert( + "ACCESS_TOKEN".into(), + openshell_core::time::timestamp_from_millis(1).unwrap(), + ); + state.store.put_message(&provider).await.unwrap(); + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")).and(wiremock::matchers::body_string_contains("refresh_token=private-refresh-token")) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({"access_token":"new-access-token","refresh_token":"rotated-private-token","expires_in":3600}))) + .expect(1).mount(&issuer).await; + refresh_fixture(&state, &provider, issuer.uri()).await; + let (first, concurrent) = tokio::join!( + handle(&state, authenticated(request(&["ACCESS_TOKEN"]))), + handle(&state, authenticated(request(&["ACCESS_TOKEN"]))) + ); + assert_eq!( + first.unwrap().into_inner().credentials["ACCESS_TOKEN"].value, + "new-access-token" + ); + assert_eq!( + concurrent.unwrap().into_inner().credentials["ACCESS_TOKEN"].value, + "new-access-token" + ); + for _ in 0..2 { + let values = handle(&state, authenticated(request(&["ACCESS_TOKEN"]))) + .await + .unwrap() + .into_inner() + .credentials; + assert_eq!(values["ACCESS_TOKEN"].value, "new-access-token"); + assert!(values["ACCESS_TOKEN"].expiration_time.is_some()); + assert_eq!(values.len(), 1); + } + issuer.verify().await; +} + +#[tokio::test] +async fn missing_refresh_owned_handle_is_repaired_without_inline_fallback() { + let (state, mut provider) = fixture().await; + // The stored handle is authoritative, even when a usable-looking inline + // value remains. Its absence must mint, never export that inline value. + provider.credential_handles.insert( + "ACCESS_TOKEN".into(), + openshell_core::proto::CredentialHandle { + driver: "test-static".into(), + handle: "missing-access-token".into(), + ..Default::default() + }, + ); + state.store.put_message(&provider).await.unwrap(); + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .and(wiremock::matchers::body_string_contains( + "refresh_token=private-refresh-token", + )) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json( + serde_json::json!({"access_token":"repaired-access-token","refresh_token":"replacement-grant","expires_in":3600}), + )) + .expect(1) + .mount(&issuer) + .await; + refresh_fixture(&state, &provider, issuer.uri()).await; + let values = handle( + &state, + authenticated(request(&["STATIC_KEY", "ACCESS_TOKEN"])), + ) + .await + .unwrap() + .into_inner() + .credentials; + assert_eq!(values.len(), 2); + assert_eq!(values["STATIC_KEY"].value, "static-secret"); + assert_eq!(values["ACCESS_TOKEN"].value, "repaired-access-token"); + assert!(values["ACCESS_TOKEN"].expiration_time.is_some()); + issuer.verify().await; +} + +#[tokio::test] +async fn missing_static_handle_and_invalid_refresh_driver_fail_closed() { + for (key, driver, expected) in [ + ("STATIC_KEY", "test-static", Code::NotFound), + ("ACCESS_TOKEN", "unconfigured-driver", Code::InvalidArgument), + ] { + let (state, mut provider) = fixture().await; + provider.credential_handles.insert( + key.into(), + openshell_core::proto::CredentialHandle { + driver: driver.into(), + handle: "missing-handle".into(), + ..Default::default() + }, + ); + state.store.put_message(&provider).await.unwrap(); + let issuer = wiremock::MockServer::start().await; + refresh_fixture(&state, &provider, issuer.uri()).await; + let error = handle( + &state, + authenticated(request(&["STATIC_KEY", "ACCESS_TOKEN"])), + ) + .await + .unwrap_err(); + assert_eq!(error.code(), expected); + assert!(!error.message().contains("missing-handle")); + assert!(issuer.received_requests().await.unwrap().is_empty()); + } +} + +#[tokio::test] +async fn bookkeeping_changes_do_not_coalesce_refresh_requests() { + // Cover both legacy states without a mint identity and states that already + // have one. Cleanup/metadata updates must not count as another mint. + for previously_minted in [false, true] { + let (state, provider) = fixture().await; + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json( + serde_json::json!({"access_token":"new-access-token","expires_in":3600}), + )) + .expect(if previously_minted { 2 } else { 1 }) + .mount(&issuer) + .await; + let mut snapshot = refresh_fixture(&state, &provider, issuer.uri()).await; + if previously_minted { + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot, + ) + .await + .unwrap(); + } else { + snapshot.status = "refreshed".into(); + crate::provider_refresh::put_refresh_state(state.store.as_ref(), &snapshot) + .await + .unwrap(); + } + let snapshot = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + let mut bookkeeping = snapshot.clone(); + bookkeeping + .metadata + .as_mut() + .unwrap() + .labels + .insert("bookkeeping".into(), "updated".into()); + crate::provider_refresh::put_refresh_state(state.store.as_ref(), &bookkeeping) + .await + .unwrap(); + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot, + ) + .await + .unwrap(); + issuer.verify().await; + } +} + +#[tokio::test] +async fn cancelled_retrieval_finishes_rotating_refresh_token_persistence() { + let (state, mut provider) = fixture().await; + provider.credential_expiration_times.insert( + "ACCESS_TOKEN".into(), + openshell_core::time::timestamp_from_millis(1).unwrap(), + ); + state.store.put_message(&provider).await.unwrap(); + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({"access_token":"after-cancel","refresh_token":"rotated-after-cancel","expires_in":3600})).set_delay(Duration::from_millis(200))) + .expect(1).mount(&issuer).await; + refresh_fixture(&state, &provider, issuer.uri()).await; + let caller_state = state.clone(); + let caller = tokio::spawn(async move { + handle(&caller_state, authenticated(request(&["ACCESS_TOKEN"]))).await + }); + tokio::time::timeout(Duration::from_secs(10), async { + loop { + if !issuer.received_requests().await.unwrap().is_empty() { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .unwrap(); + caller.abort(); + assert!(caller.await.unwrap_err().is_cancelled()); + tokio::time::timeout(Duration::from_secs(10), async { + loop { + let refresh = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + if refresh.status == "refreshed" { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .unwrap(); + let values = handle(&state, authenticated(request(&["ACCESS_TOKEN"]))) + .await + .unwrap() + .into_inner() + .credentials; + assert_eq!(values["ACCESS_TOKEN"].value, "after-cancel"); + let refresh = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + let material = state + .credentials + .resolve_refresh_material( + crate::provider_refresh::refresh_material_scope(&refresh), + &refresh.secret_material_handles, + ) + .await + .unwrap(); + assert_eq!(material["refresh_token"], "rotated-after-cancel"); + assert!(!refresh.material.contains_key("refresh_token")); + issuer.verify().await; +} + +#[tokio::test] +async fn coalesces_observed_refresh_generation_and_parks_uncertain_mint() { + let (state, provider) = fixture().await; + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .respond_with( + wiremock::ResponseTemplate::new(200) + .set_body_json( + serde_json::json!({"access_token":"new-access-token","expires_in":3600}), + ) + .set_delay(Duration::from_millis(100)), + ) + .expect(1) + .mount(&issuer) + .await; + let snapshot = refresh_fixture(&state, &provider, issuer.uri()).await; + let (a, b) = tokio::join!( + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot.clone() + ), + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot.clone() + ) + ); + a.unwrap(); + b.unwrap(); + issuer.verify().await; + let mut parked = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + parked.status = "refresh_in_progress".into(); + crate::provider_refresh::put_refresh_state(state.store.as_ref(), &parked) + .await + .unwrap(); + let error = crate::provider_refresh::refresh_provider_credential( + state.store.as_ref(), + "other", + &state.credentials, + Some(&state.compute), + "provider", + "ACCESS_TOKEN", + ) + .await + .unwrap_err(); + assert_eq!(error.code(), Code::FailedPrecondition); + issuer.verify().await; +} + +async fn automatic_waiters_honor_failed_refresh( + response: wiremock::ResponseTemplate, + expected_code: Code, + expected_status: &str, + expected_recovery: ProviderCredentialRefreshRecoveryAction, +) { + let (state, provider) = fixture().await; + let issuer = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .respond_with(response.set_delay(Duration::from_millis(100))) + .expect(1) + .mount(&issuer) + .await; + let snapshot = refresh_fixture(&state, &provider, issuer.uri()).await; + let (a, b) = tokio::join!( + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot.clone(), + ), + crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + snapshot.clone(), + ), + ); + assert_eq!(a.unwrap_err().code(), expected_code); + assert_eq!(b.unwrap_err().code(), expected_code); + issuer.verify().await; + let failed = crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(failed.status, expected_status); + assert_eq!(failed.recovery_action, expected_recovery as i32); + assert!(!failed.failure_code.is_empty()); + assert!(failed.next_refresh_at_ms > crate::persistence::current_time_ms()); + if expected_recovery == ProviderCredentialRefreshRecoveryAction::Reauthorize { + assert_eq!(failed.next_refresh_at_ms, i64::MAX); + } + // Both a stale snapshot and a fresh automatic request must respect the + // committed failure, without changing its retry deadline or error receipt. + for observed in [snapshot, failed.clone()] { + let error = crate::provider_refresh::refresh_from_snapshot( + state.store.as_ref(), + &state.credentials, + Some(&state.compute), + observed, + ) + .await + .unwrap_err(); + assert_eq!(error.code(), expected_code); + } + assert_eq!( + crate::provider_refresh::get_refresh_state( + state.store.as_ref(), + "other", + provider.object_id(), + "ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(), + failed, + ); + issuer.verify().await; + + // A separately requested manual rotation can retry a parked grant or + // override retry backoff; it still goes through the same coordination. + issuer.reset().await; + wiremock::Mock::given(wiremock::matchers::method("POST")) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json( + serde_json::json!({"access_token":"manually-refreshed","expires_in":3600}), + )) + .expect(1) + .mount(&issuer) + .await; + let rotated = crate::provider_refresh::refresh_provider_credential( + state.store.as_ref(), + "other", + &state.credentials, + Some(&state.compute), + "provider", + "ACCESS_TOKEN", + ) + .await + .unwrap(); + assert_eq!(rotated.status, "refreshed"); + assert_eq!( + rotated.recovery_action, + ProviderCredentialRefreshRecoveryAction::Unspecified as i32, + ); + issuer.verify().await; +} + +#[tokio::test] +async fn automatic_waiters_do_not_repeat_unusable_refresh_token_exchange() { + automatic_waiters_honor_failed_refresh( + wiremock::ResponseTemplate::new(200).set_body_string("not JSON"), + Code::FailedPrecondition, + "reauthorization_required", + ProviderCredentialRefreshRecoveryAction::Reauthorize, + ) + .await; +} + +#[tokio::test] +async fn automatic_waiters_do_not_bypass_rate_limit_backoff() { + automatic_waiters_honor_failed_refresh( + wiremock::ResponseTemplate::new(429) + .set_body_json(serde_json::json!({"error":"temporarily_unavailable"})), + Code::Unavailable, + "error", + ProviderCredentialRefreshRecoveryAction::Retry, + ) + .await; +} + +#[tokio::test] +async fn rejects_supervisor_dynamic_grants_even_with_stored_values() { + let (state, _) = fixture().await; + let mut profile = ProviderProfile { + id: "operator-test".into(), + credentials: vec![ProviderProfileCredential { + name: "dynamic".into(), + env_vars: vec!["ACCESS_TOKEN".into()], + token_grant: Some(openshell_core::proto::ProviderCredentialTokenGrant::default()), + ..Default::default() + }], + ..Default::default() + }; + // The fixture tests classification, without accepting or executing a grant. + profile.source = "user".into(); + state + .store + .put_message(&crate::provider_profile_sources::stored_provider_profile( + profile, + )) + .await + .unwrap(); + assert_eq!( + handle(&state, authenticated(request(&["ACCESS_TOKEN"]))) + .await + .unwrap_err() + .code(), + Code::FailedPrecondition + ); +} + +#[tokio::test] +async fn real_tls_operator_boundary_and_admin_capabilities() { + use crate::multiplex::{ + MultiplexService, extract_peer_certificate_sha256, extract_peer_identity, + }; + use openshell_core::proto::{ + GetCurrentUserRequest, ListWorkspacesRequest, open_shell_client::OpenShellClient, + }; + use tonic::transport::{ClientTlsConfig, Endpoint}; + + let (mut state, _) = fixture().await; + let config = &mut Arc::get_mut(&mut state).unwrap().config; + config.mtls_auth.operator_enabled = true; + config.mtls_auth.enabled = false; + let dir = tempfile::tempdir().unwrap(); + let (ca, ca_key) = crate::tls_test_utils::generate_test_certs_with_ca(dir.path()); + let acceptor = crate::TlsAcceptor::from_files( + &dir.path().join("server-cert.pem"), + &dir.path().join("server-key.pem"), + Some(&dir.path().join("ca.pem")), + false, + None, + None, + vec![], + ) + .unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let service = MultiplexService::new(state); + let server = tokio::spawn(async move { + loop { + let (stream, _) = listener.accept().await.unwrap(); + openshell_core::net::set_tcp_nodelay_best_effort(&stream); + let acceptor = acceptor.acceptor(); + let service = service.clone(); + tokio::spawn(async move { + if let Ok(stream) = acceptor.accept(stream).await { + let identity = extract_peer_identity(&stream); + let fingerprint = extract_peer_certificate_sha256(&stream); + let _ = service + .serve_with_verified_certificate(stream, identity, fingerprint) + .await; + } + }); + } + }); + for role in [ + Some("operator"), + Some("Operator"), + Some("openshell-admin"), + None, + ] { + let mut tls = ClientTlsConfig::new() + .ca_certificate(tonic::transport::Certificate::from_pem(ca.pem())) + .domain_name("localhost"); + if let Some(role) = role { + let mut params = rcgen::CertificateParams::new(vec![]).unwrap(); + params + .distinguished_name + .push(rcgen::DnType::CommonName, "tls-test"); + params + .distinguished_name + .push(rcgen::DnType::OrganizationalUnitName, role); + params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ClientAuth]; + let key = rcgen::KeyPair::generate().unwrap(); + let cert = params.signed_by(&key, &ca, &ca_key).unwrap(); + tls = tls.identity(tonic::transport::Identity::from_pem( + cert.pem(), + key.serialize_pem(), + )); + } + let channel = Endpoint::from_shared(format!("https://localhost:{}", address.port())) + .unwrap() + .tls_config(tls) + .unwrap() + .connect() + .await + .unwrap(); + let mut client = OpenShellClient::new(channel); + let response = client + .get_provider_credentials(request(&["STATIC_KEY"])) + .await; + if role == Some("operator") { + assert_eq!( + response.unwrap().into_inner().credentials["STATIC_KEY"].value, + "static-secret" + ); + assert_eq!( + client + .get_current_user(GetCurrentUserRequest {}) + .await + .unwrap() + .into_inner() + .identity_provider, + "mtls" + ); + client + .list_workspaces(ListWorkspacesRequest::default()) + .await + .unwrap(); + let create = openshell_core::proto::CreateWorkspaceRequest { + name: "operator-created".into(), + request_id: uuid::Uuid::new_v4().to_string(), + labels: HashMap::new(), + }; + let first = client + .create_workspace(create.clone()) + .await + .unwrap() + .into_inner(); + let replay = client.create_workspace(create).await.unwrap().into_inner(); + assert_eq!(first, replay); + let mut mixed = Request::new(request(&["STATIC_KEY"])); + mixed + .metadata_mut() + .insert("authorization", "Bearer forbidden".parse().unwrap()); + assert_eq!( + client + .get_provider_credentials(mixed) + .await + .unwrap_err() + .code(), + Code::Unauthenticated + ); + } else { + assert_eq!(response.unwrap_err().code(), Code::PermissionDenied); + let mut spoof = Request::new(request(&["STATIC_KEY"])); + spoof + .metadata_mut() + .insert("x-forwarded-client-cert", "OU=operator".parse().unwrap()); + assert_eq!( + client + .get_provider_credentials(spoof) + .await + .unwrap_err() + .code(), + Code::PermissionDenied + ); + } + } + server.abort(); +} diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index bc432d3582..815fcdd84c 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -322,6 +322,9 @@ pub(super) async fn handle_begin_rootfs_tar_staging( fn principal_subject(principal: &crate::auth::principal::Principal) -> Result { match principal { crate::auth::principal::Principal::User(user) => Ok(user.identity.subject.clone()), + crate::auth::principal::Principal::Operator(operator) => { + Ok(format!("operator:{}", operator.identity.subject)) + } _ => Err(Status::permission_denied( "rootfs tar staging requires a user principal", )), diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index c8677d890d..8cbc04f527 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -67,7 +67,7 @@ fn membership_filter_subject<'a>( Ok(Some(&u.identity.subject)) } } - Principal::Sandbox(_) => Ok(None), + Principal::Sandbox(_) | Principal::Operator(_) => Ok(None), Principal::Peer(_) => Err(Status::permission_denied( "gateway peer principals cannot list workspaces", )), diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 8c860ec35c..1de522d257 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -1152,8 +1152,14 @@ fn spawn_gateway_connection( match acceptor.acceptor().accept(stream).await { Ok(tls_stream) => { let peer_identity = multiplex::extract_peer_identity(&tls_stream); + let certificate_sha256 = + multiplex::extract_peer_certificate_sha256(&tls_stream); if let Err(e) = service - .serve_with_peer_identity(tls_stream, peer_identity) + .serve_with_verified_certificate( + tls_stream, + peer_identity, + certificate_sha256, + ) .await { if is_benign_connection_close(e.as_ref()) { diff --git a/crates/openshell-server/src/multiplex.rs b/crates/openshell-server/src/multiplex.rs index 972df8b965..a383f75c4a 100644 --- a/crates/openshell-server/src/multiplex.rs +++ b/crates/openshell-server/src/multiplex.rs @@ -47,7 +47,7 @@ use crate::{ auth::authz::AuthzPolicy, auth::identity::Identity, auth::oidc::{self, OidcAuthenticator}, - auth::principal::{Principal, UserPrincipal}, + auth::principal::{OperatorPrincipal, Principal, UserPrincipal}, auth::workspace_authz::{MinWorkspaceRole, authorize_workspace}, http_router, service_http_router, }; @@ -252,6 +252,35 @@ impl MultiplexService { where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, { + self.serve_with_verified_certificate(stream, peer_identity, None) + .await + } + + /// TLS listener only: fingerprint and identity must originate from its verified handshake. + pub async fn serve_with_verified_certificate( + &self, + stream: S, + peer_identity: Option, + certificate_sha256: Option, + ) -> Result<(), Box> + where + S: AsyncRead + AsyncWrite + Unpin + Send + 'static, + { + let operator = if self.state.config.mtls_auth.operator_enabled { + peer_identity + .as_ref() + .filter(|identity| { + identity.provider == crate::auth::identity::IdentityProvider::Mtls + && identity.roles.iter().any(|role| role == "operator") + }) + .zip(certificate_sha256) + .map(|(identity, certificate_sha256)| OperatorPrincipal { + identity: identity.clone(), + certificate_sha256, + }) + } else { + None + }; let openshell = OpenShellServer::new(OpenShellService::new(self.state.clone())) .max_decoding_message_size(MAX_GRPC_DECODE_SIZE); let openshell = GatewayInterceptorGrpcService::new( @@ -277,7 +306,8 @@ impl MultiplexService { .flatten(), self.state.config.mtls_auth.enabled, self.state.config.auth.allow_unauthenticated_users, - ); + ) + .with_operator(operator); let grpc_service = GrpcRateLimitService::new(grpc_service, self.state.grpc_rate_limiter.clone()); let http_service = http_router(self.state.clone()); @@ -685,6 +715,15 @@ fn gateway_principal_fields(principal: &Principal) -> BTreeMap { fields.insert("scopes".to_string(), user.identity.scopes.join(",")); } } + Principal::Operator(operator) => { + fields.insert("kind".to_string(), "operator".to_string()); + fields.insert("subject".to_string(), operator.identity.subject.clone()); + fields.insert("provider".to_string(), "mtls".to_string()); + fields.insert( + "certificate_sha256".to_string(), + operator.certificate_sha256.clone(), + ); + } Principal::Sandbox(sandbox) => { fields.insert("kind".to_string(), "sandbox".to_string()); fields.insert("sandbox_id".to_string(), sandbox.sandbox_id.clone()); @@ -945,6 +984,7 @@ pub struct AuthGrpcRouter { peer_identity: Option, mtls_auth_enabled: bool, allow_unauthenticated_users: bool, + operator: Option, } impl AuthGrpcRouter { @@ -972,8 +1012,14 @@ impl AuthGrpcRouter { peer_identity, mtls_auth_enabled, allow_unauthenticated_users, + operator: None, } } + + fn with_operator(mut self, operator: Option) -> Self { + self.operator = operator; + self + } } fn unauthenticated_dev_user_principal() -> Principal { @@ -1013,6 +1059,7 @@ where let peer_identity = self.peer_identity.clone(); let mtls_auth_enabled = self.mtls_auth_enabled; let allow_unauthenticated_users = self.allow_unauthenticated_users; + let operator = self.operator.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -1025,7 +1072,14 @@ where return inner.ready().await?.call(req).await; } - let principal = if let Some(chain) = chain { + let principal = if let Some(operator) = operator { + if req.headers().contains_key(http::header::AUTHORIZATION) { + return Ok(status_response(tonic::Status::unauthenticated( + "operator certificates must not be combined with authorization headers", + ))); + } + Principal::Operator(operator) + } else if let Some(chain) = chain { match chain.authenticate(req.headers(), &path).await { Ok(Some(p)) => p, Ok(None) => match (mtls_auth_enabled, peer_identity) { @@ -1056,6 +1110,13 @@ where }; match principal { + Principal::Operator(_) => { + if !crate::auth::method_authz::is_operator_callable(&path) { + return Ok(status_response(tonic::Status::permission_denied( + "operators may not call supervisor, peer, or unknown methods", + ))); + } + } Principal::User(ref user) => { if !crate::auth::method_authz::is_user_callable(&path) { return Ok(status_response(tonic::Status::permission_denied( @@ -1287,6 +1348,15 @@ where }) } +/// Fingerprint of the leaf certificate verified during the TLS handshake. +pub fn extract_peer_certificate_sha256( + stream: &tokio_rustls::server::TlsStream, +) -> Option { + use sha2::{Digest, Sha256}; + let certificate = stream.get_ref().1.peer_certificates()?.first()?; + Some(format!("{:x}", Sha256::digest(certificate.as_ref()))) +} + /// Boxed body type for uniform handling. pub struct BoxBody( http_body_util::combinators::UnsyncBoxBody>, @@ -2730,6 +2800,95 @@ mod tests { }) } + fn operator_principal() -> OperatorPrincipal { + let mut identity = mtls_identity("operator"); + identity.roles = vec!["operator".to_string()]; + OperatorPrincipal { + identity, + certificate_sha256: "verified-leaf".to_string(), + } + } + + #[tokio::test] + async fn operator_is_admin_but_not_supervisor_peer_or_unknown() { + for (method, permitted) in [ + ("GetProviderCredentials", true), + ("CreateWorkspace", true), + ("UpdateProvider", true), + ("GetSandboxConfig", true), + ("ConnectSupervisor", false), + ("PeerRelay", false), + ("IssueSandboxToken", false), + ("Unknown", false), + ] { + let (recorder, seen) = PrincipalRecorder::new(); + let mut router = AuthGrpcRouter::with_peer_identity( + recorder, + None, + Some(AuthzPolicy { + admin_role: "oidc-admin".into(), + user_role: "oidc-user".into(), + scopes_enabled: true, + }), + None, + false, + false, + ) + .with_operator(Some(operator_principal())); + let response = router + .call(empty_request(&format!("/openshell.v1.OpenShell/{method}"))) + .await + .unwrap(); + if permitted { + assert!(grpc_status(&response).is_none()); + assert!(matches!( + *seen.lock().unwrap(), + Some(Principal::Operator(_)) + )); + } else { + assert_eq!(grpc_status(&response).as_deref(), Some("7")); + } + } + } + + #[tokio::test] + async fn user_bearer_claims_and_local_dev_cannot_export_credentials() { + let mut user = mtls_identity("ordinary-user"); + user.provider = IdentityProvider::Oidc; + user.roles = vec!["operator".into(), "openshell-admin".into()]; + user.scopes = vec!["openshell:all".into()]; + for principal in [ + Principal::User(UserPrincipal { identity: user }), + sandbox_principal(), + Principal::Peer(crate::auth::principal::PeerPrincipal { + replica_id: "replica".into(), + pod_uid: "pod".into(), + }), + ] { + let chain = AuthenticatorChain::new(vec![Arc::new(MockAuthenticator::returning( + Ok(Some(principal)), + ))]); + let (recorder, _) = PrincipalRecorder::new(); + let mut router = AuthGrpcRouter::new(recorder, Some(chain), None); + let response = router + .call(empty_request( + "/openshell.v1.OpenShell/GetProviderCredentials", + )) + .await + .unwrap(); + assert_eq!(grpc_status(&response).as_deref(), Some("7")); + } + let (recorder, _) = PrincipalRecorder::new(); + let mut router = AuthGrpcRouter::new(recorder, None, None); + let response = router + .call(empty_request( + "/openshell.v1.OpenShell/GetProviderCredentials", + )) + .await + .unwrap(); + assert_eq!(grpc_status(&response).as_deref(), Some("7")); + } + #[derive(Serialize)] struct LegacySandboxClaims { sub: String, diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 1721cf7931..81c939faad 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -5,6 +5,8 @@ mod legacy_time_wire; mod postgres; +#[cfg(test)] +mod refresh_lock_tests; mod sqlite; pub use crate::storage_proto::{ @@ -312,6 +314,29 @@ impl Store { } } + /// Independent per-credential lock, shared across `PostgreSQL` replicas. + /// Callers also hold a local keyed mutex on `SQLite`. + pub(crate) async fn acquire_refresh_guard( + &self, + provider_id: &str, + credential_key: &str, + ) -> PersistenceResult { + use sha2::{Digest, Sha256}; + let mut digest = Sha256::new(); + digest.update(b"openshell-provider-refresh-lock-v1\0"); + digest.update(provider_id.as_bytes()); + digest.update(b"\0"); + digest.update(credential_key.as_bytes()); + let hash = digest.finalize(); + let key = i64::from_be_bytes(hash[..8].try_into().expect("eight digest bytes")); + match self { + Self::Postgres(store) => Ok(DistributedMutationGuard { + _postgres: Some(store.acquire_mutation_lock(key).await?), + }), + Self::Sqlite(_) => Ok(DistributedMutationGuard { _postgres: None }), + } + } + /// Connect to a persistence store based on the database URL. pub async fn connect(url: &str) -> CoreResult { if url.starts_with("postgres://") || url.starts_with("postgresql://") { diff --git a/crates/openshell-server/src/persistence/refresh_lock_tests.rs b/crates/openshell-server/src/persistence/refresh_lock_tests.rs new file mode 100644 index 0000000000..21791085c4 --- /dev/null +++ b/crates/openshell-server/src/persistence/refresh_lock_tests.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::Store; +use std::time::Duration; + +#[tokio::test] +#[ignore = "requires a disposable PostgreSQL database in OPENSHELL_REFRESH_TEST_DATABASE_URL"] +async fn refresh_locks_coordinate_independent_replica_sessions() { + let url = + std::env::var("OPENSHELL_REFRESH_TEST_DATABASE_URL").expect("disposable database URL"); + let first = Store::connect(&url).await.unwrap(); + let second = Store::connect(&url).await.unwrap(); + let provider = uuid::Uuid::new_v4().to_string(); + let guard = first + .acquire_refresh_guard(&provider, "ACCESS_TOKEN") + .await + .unwrap(); + // This bypasses the process-local mutex: independent database sessions must + // still serialize a mint, while unrelated credentials remain independent. + assert!( + tokio::time::timeout( + Duration::from_millis(100), + second.acquire_refresh_guard(&provider, "ACCESS_TOKEN") + ) + .await + .is_err() + ); + let unrelated = tokio::time::timeout( + Duration::from_secs(2), + second.acquire_refresh_guard(&provider, "OTHER_KEY"), + ) + .await + .unwrap() + .unwrap(); + drop(unrelated); + drop(guard); + let guard = tokio::time::timeout( + Duration::from_secs(2), + second.acquire_refresh_guard(&provider, "ACCESS_TOKEN"), + ) + .await + .unwrap() + .unwrap(); + drop(guard); + first.close().await; + second.close().await; +} diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index c1b5184d18..7708022f0b 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -29,6 +29,7 @@ use crate::storage_proto::{ const DEFAULT_REFRESH_BEFORE_SECONDS: i64 = 300; const EXPIRATION_PRESENT_ANNOTATION: &str = "openshell.nvidia.com/refresh-expiration-present"; +const COMPLETED_MINT_ANNOTATION: &str = "openshell.nvidia.com/refresh-completed-mint"; const DEFAULT_MAX_LIFETIME_SECONDS: i64 = 3600; const REFRESH_ERROR_RETRY_SECONDS: i64 = 60; const REFRESH_CONFIGURATION_RETRY_SECONDS: i64 = 60 * 60; @@ -845,6 +846,115 @@ fn validate_secret_material_references( ))) } +// Local waiters do not consume PostgreSQL sessions while another local mint owns +// the credential. Weak entries avoid retaining every provider ever refreshed. +static REFRESH_LOCKS: std::sync::LazyLock< + std::sync::Mutex>>>, +> = std::sync::LazyLock::new(|| std::sync::Mutex::new(HashMap::new())); +static REFRESH_EXECUTORS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(4); +static REFRESH_ADMISSIONS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(32); + +fn refresh_lock( + state: &StoredProviderCredentialRefreshState, +) -> Result>, Status> { + let key = refresh_state_name(&state.provider_id, &state.credential_key); + let mut locks = REFRESH_LOCKS + .lock() + .map_err(|_| Status::internal("refresh coordination unavailable"))?; + locks.retain(|_, lock| lock.strong_count() > 0); + let lock = locks + .get(&key) + .and_then(std::sync::Weak::upgrade) + .unwrap_or_else(|| std::sync::Arc::new(tokio::sync::Mutex::new(()))); + locks.insert(key, std::sync::Arc::downgrade(&lock)); + Ok(lock) +} + +fn completed_mint(state: &StoredProviderCredentialRefreshState) -> Option<&str> { + state + .metadata + .as_ref()? + .annotations + .get(COMPLETED_MINT_ANNOTATION) + .map(String::as_str) + .filter(|id| !id.is_empty()) +} + +/// Cleanup also mutates the mint receipt's resource version. Use the same +/// local/distributed locks as minting, then reload instead of writing a stale +/// sweep snapshot over an active mint or deleting its replacement grant. +async fn cleanup_pending_secret_deletions_coordinated( + store: &Store, + credentials: Option<&crate::credentials::CredentialRuntime>, + state: &mut StoredProviderCredentialRefreshState, +) -> Result<(), Status> { + if state.pending_secret_deletions.is_empty() { + return Ok(()); + } + let _local = tokio::time::timeout(Duration::from_secs(40), refresh_lock(state)?.lock_owned()) + .await + .map_err(|_| Status::unavailable("credential refresh is busy"))?; + let _distributed = store + .acquire_refresh_guard(&state.provider_id, &state.credential_key) + .await + .map_err(|_| Status::unavailable("credential refresh coordination unavailable"))?; + let latest = get_refresh_state( + store, + state.object_workspace(), + &state.provider_id, + &state.credential_key, + ) + .await? + .ok_or_else(|| Status::aborted("provider refresh was deleted"))?; + if latest.object_id() != state.object_id() + || effective_authorization_epoch(&latest)? != effective_authorization_epoch(state)? + || latest + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + { + return Err(Status::aborted( + "provider refresh was deleted or reconfigured", + )); + } + *state = latest; + let expected_version = state + .metadata + .as_ref() + .map_or(0, |metadata| metadata.resource_version); + cleanup_pending_secret_deletions(store, credentials, state, expected_version).await?; + Ok(()) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum RefreshTrigger { + Automatic, + ExplicitRotation, +} + +/// Automatic callers must honor the live failure schedule, not the eligibility +/// observed before they queued. Configuration/investigation failures can still +/// be retried by the worker once due; reauthorization stays parked. +fn ensure_automatic_refresh_ready( + state: &StoredProviderCredentialRefreshState, + now_ms: i64, +) -> Result<(), Status> { + if state.recovery_action == ProviderCredentialRefreshRecoveryAction::Reauthorize as i32 { + return Err(Status::failed_precondition( + "credential refresh requires recovery", + )); + } + if state.recovery_action != ProviderCredentialRefreshRecoveryAction::Unspecified as i32 + && state.next_refresh_at_ms > now_ms + { + return Err(Status::unavailable( + "credential refresh is in retry backoff", + )); + } + Ok(()) +} + +/// Cancellation-safe entry point for a separately requested manual rotation. pub async fn refresh_provider_credential( store: &Store, workspace: &str, @@ -852,6 +962,142 @@ pub async fn refresh_provider_credential( compute: Option<&crate::compute::ComputeRuntime>, provider_name: &str, credential_key: &str, +) -> Result { + let provider = store + .get_message_by_name::(workspace, provider_name) + .await + .map_err(|_| Status::internal("fetch provider failed"))? + .ok_or_else(|| Status::not_found("provider not found"))?; + let snapshot = get_refresh_state(store, workspace, provider.object_id(), credential_key) + .await? + .ok_or_else(|| Status::not_found("provider refresh state not found"))?; + refresh_from_snapshot_with_trigger( + store, + credentials, + compute, + snapshot, + RefreshTrigger::ExplicitRotation, + ) + .await +} + +/// Coalesce an automatic refresh against the caller's observed generation, not +/// a name-keyed replacement. Recheck recovery/backoff after acquiring the locks. +pub async fn refresh_from_snapshot( + store: &Store, + credentials: &crate::credentials::CredentialRuntime, + compute: Option<&crate::compute::ComputeRuntime>, + snapshot: StoredProviderCredentialRefreshState, +) -> Result { + refresh_from_snapshot_with_trigger( + store, + credentials, + compute, + snapshot, + RefreshTrigger::Automatic, + ) + .await +} + +async fn refresh_from_snapshot_with_trigger( + store: &Store, + credentials: &crate::credentials::CredentialRuntime, + compute: Option<&crate::compute::ComputeRuntime>, + snapshot: StoredProviderCredentialRefreshState, + trigger: RefreshTrigger, +) -> Result { + let admission = REFRESH_ADMISSIONS + .try_acquire() + .map_err(|_| Status::resource_exhausted("credential refresh queue is full"))?; + let lock = refresh_lock(&snapshot)?; + let store = store.clone(); + let credentials = credentials.clone(); + let compute = compute.cloned(); + // A disconnected RPC must not cancel minting after an upstream rotates its + // refresh token. Spawn before any externally observable refresh side effect. + tokio::spawn(async move { + let _admission = admission; + let _local = tokio::time::timeout(Duration::from_secs(40), lock.lock_owned()) + .await + .map_err(|_| Status::unavailable("credential refresh is busy"))?; + let _permit = tokio::time::timeout(Duration::from_secs(40), REFRESH_EXECUTORS.acquire()) + .await + .map_err(|_| Status::resource_exhausted("credential refresh workers are busy"))? + .map_err(|_| Status::unavailable("credential refresh workers stopped"))?; + let _distributed = store + .acquire_refresh_guard(&snapshot.provider_id, &snapshot.credential_key) + .await + .map_err(|_| Status::unavailable("credential refresh coordination unavailable"))?; + let latest = get_refresh_state( + &store, + snapshot.object_workspace(), + &snapshot.provider_id, + &snapshot.credential_key, + ) + .await? + .ok_or_else(|| Status::aborted("provider refresh was deleted"))?; + if latest.object_id() != snapshot.object_id() + || effective_authorization_epoch(&latest)? != effective_authorization_epoch(&snapshot)? + { + return Err(Status::aborted("provider refresh was reconfigured")); + } + if latest + .metadata + .as_ref() + .is_some_and(|metadata| metadata.deletion_time.is_some()) + { + return Err(Status::failed_precondition( + "provider refresh is being deleted", + )); + } + // A row-version change can be cleanup or other bookkeeping, not a + // successful exchange. Only a new committed mint can satisfy a waiter. + if latest.status == "refreshed" + && completed_mint(&latest).is_some() + && completed_mint(&latest) != completed_mint(&snapshot) + { + return Ok(latest); + } + let provider = store + .get_message_by_name::(snapshot.object_workspace(), &snapshot.provider_name) + .await + .map_err(|_| Status::internal("fetch provider failed"))? + .ok_or_else(|| Status::not_found("provider not found"))?; + if provider.object_id() != snapshot.provider_id { + return Err(Status::aborted("provider was replaced")); + } + if matches!( + latest.status.as_str(), + "refresh_in_progress" | "refresh_committing" + ) { + return Err(Status::failed_precondition( + "previous credential mint has an uncertain outcome; reconfigure the refresh grant", + )); + } + if trigger == RefreshTrigger::Automatic { + ensure_automatic_refresh_ready(&latest, current_time_ms())?; + } + refresh_provider_credential_locked( + &store, + snapshot.object_workspace(), + &credentials, + compute.as_ref(), + &snapshot.provider_name, + &snapshot.credential_key, + ) + .await + }) + .await + .map_err(|_| Status::internal("credential refresh worker failed"))? +} + +async fn refresh_provider_credential_locked( + store: &Store, + workspace: &str, + credentials: &crate::credentials::CredentialRuntime, + compute: Option<&crate::compute::ComputeRuntime>, + provider_name: &str, + credential_key: &str, ) -> Result { let provider = store .get_message_by_name::(workspace, provider_name) @@ -911,6 +1157,14 @@ pub async fn refresh_provider_credential( "provider credential refresh started" ); + // Persist uncertainty BEFORE contacting the issuer. A crashed process or a + // lost advisory-lock session cannot lead another replica to reuse the grant. + state.status = "refresh_in_progress".to_string(); + state.next_refresh_at_ms = i64::MAX; + state.recovery_action = ProviderCredentialRefreshRecoveryAction::Investigate as i32; + let expected_version = persist_refresh_state_if_current(store, &state, expected_version) + .await? + .ok_or_else(|| Status::aborted("provider refresh changed before minting"))?; let mint_result = match resolve_refresh_material(Some(credentials), &state).await { Ok(transient_state) => mint_credential(&transient_state).await, Err(err) => Err(err.into()), @@ -995,7 +1249,9 @@ pub async fn refresh_provider_credential( state.next_refresh_at_ms = next_refresh_at_ms(minted.expires_at_ms, state.refresh_before.as_ref()); state.last_refresh_at_ms = now_ms; - state.status = "refreshed".to_string(); + let completed_next_refresh_at_ms = state.next_refresh_at_ms; + state.status = "refresh_committing".to_string(); + state.next_refresh_at_ms = i64::MAX; state.last_error.clear(); state.recovery_action = ProviderCredentialRefreshRecoveryAction::Unspecified as i32; state.failure_code.clear(); @@ -1083,6 +1339,22 @@ pub async fn refresh_provider_credential( ); return Err(err); } + // A success snapshot must not become visible before its provider + // handles are committed. Park incomplete commits rather than minting + // again after a crash or lost database session. + state.status = "refreshed".to_string(); + state.next_refresh_at_ms = completed_next_refresh_at_ms; + if let Some(metadata) = state.metadata.as_mut() { + metadata.annotations.insert( + COMPLETED_MINT_ANNOTATION.to_string(), + uuid::Uuid::new_v4().to_string(), + ); + } + let new_version = persist_refresh_state_if_current(store, &state, new_version) + .await? + .ok_or_else(|| { + Status::aborted("provider refresh changed during credential commit") + })?; info!( provider = %state.provider_name, credential_key = %state.credential_key, @@ -1641,10 +1913,20 @@ async fn request_token( error_kind, "OAuth token endpoint request failed" ); - RefreshFailure::retryable( - Status::unavailable("token endpoint request failed"), - "oauth_token_endpoint_unavailable", - ) + if grant_kind == OAuthGrantKind::UserRefreshToken && error_kind != "connect" { + RefreshFailure::reauthorize( + Status::failed_precondition( + "refresh token exchange has an uncertain outcome; reauthorize the grant", + ), + "oauth_refresh_exchange_uncertain", + None, + ) + } else { + RefreshFailure::retryable( + Status::unavailable("token endpoint request failed"), + "oauth_token_endpoint_unavailable", + ) + } })?; let status = response.status(); if !status.is_success() { @@ -1652,12 +1934,30 @@ async fn request_token( return Err(classify_oauth_token_error(status, &body, grant_kind)); } let token = response.json::().await.map_err(|_| { + if grant_kind == OAuthGrantKind::UserRefreshToken { + return RefreshFailure::reauthorize( + Status::failed_precondition( + "refresh token exchange returned an unusable response; reauthorize the grant", + ), + "oauth_refresh_exchange_uncertain", + None, + ); + } RefreshFailure::investigate( Status::failed_precondition("token endpoint returned invalid JSON"), "oauth_invalid_success_response", ) })?; if token.access_token.trim().is_empty() { + if grant_kind == OAuthGrantKind::UserRefreshToken { + return Err(RefreshFailure::reauthorize( + Status::failed_precondition( + "refresh token exchange returned no access token; reauthorize the grant", + ), + "oauth_refresh_exchange_uncertain", + None, + )); + } return Err(RefreshFailure::investigate( Status::failed_precondition("token endpoint returned empty access_token"), "oauth_empty_access_token", @@ -2018,12 +2318,8 @@ async fn refresh_states( continue; } let mut state = state; - let expected_version = state - .metadata - .as_ref() - .map_or(0, |metadata| metadata.resource_version); if let Err(err) = - cleanup_pending_secret_deletions(store, credentials, &mut state, expected_version).await + cleanup_pending_secret_deletions_coordinated(store, credentials, &mut state).await { warn!( provider = %state.provider_name, @@ -2078,15 +2374,14 @@ async fn refresh_states( status = %state.status, "refreshing provider credential" ); - if let Err(err) = refresh_provider_credential( - store, - state.object_workspace(), - credentials, - compute, - &state.provider_name, - &state.credential_key, - ) - .await + let trigger = if rotation_requested { + RefreshTrigger::ExplicitRotation + } else { + RefreshTrigger::Automatic + }; + if let Err(err) = + refresh_from_snapshot_with_trigger(store, credentials, compute, state.clone(), trigger) + .await { warn!( provider = %state.provider_name, @@ -2107,9 +2402,9 @@ mod tests { MAX_OAUTH_ERROR_RESPONSE_BYTES, NewRefreshStateConfig, OAuthGrantKind, RefreshFailure, RefreshRetrySchedule, Status, classify_oauth_token_error, delete_refresh_state_with_credentials, effective_authorization_epoch, - enqueue_pending_secret_deletion, get_refresh_state, list_all_refresh_states, - list_refresh_states_for_provider, max_lifetime_seconds, new_refresh_state, - next_refresh_at_ms, put_refresh_state, read_bounded_oauth_error_body, + enqueue_pending_secret_deletion, ensure_automatic_refresh_ready, get_refresh_state, + list_all_refresh_states, list_refresh_states_for_provider, max_lifetime_seconds, + new_refresh_state, next_refresh_at_ms, put_refresh_state, read_bounded_oauth_error_body, refresh_has_expiration, refresh_material_scope, refresh_provider_credential, refresh_state_name, refresh_status_from_state, refresh_strategy_name, run_refresh_worker_tick, seconds_until_ms, set_refresh_expiration_presence, @@ -3236,6 +3531,158 @@ mod tests { ); } + #[tokio::test] + async fn worker_cleanup_waits_for_mint_and_preserves_rotated_grant() { + use std::time::Duration; + + let issuer = MockServer::start().await; + Mock::given(method("POST")) + .and(body_string_contains("refresh_token=old-grant")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "access_token": "new-access-token", + "refresh_token": "replacement-grant", + "expires_in": 3600 + }))) + .expect(1) + .mount(&issuer) + .await; + let store = test_store().await; + let provider = provider("cleanup-during-mint", "outlook"); + store.put_message(&provider).await.unwrap(); + let credentials = test_credentials(); + let mut state = new_refresh_state( + &provider, + "default", + "MS_GRAPH_ACCESS_TOKEN", + NewRefreshStateConfig { + strategy: ProviderCredentialRefreshStrategy::Oauth2RefreshToken, + material: HashMap::from([ + ("client_id".into(), "client".into()), + ("refresh_token".into(), "old-grant".into()), + ]), + secret_material_keys: vec!["refresh_token".into()], + expires_at_ms: 1, + token_url: issuer.uri(), + scopes: vec![], + refresh_before: None, + max_lifetime: None, + additional_output_keys: HashMap::new(), + }, + ) + .unwrap(); + let obsolete = credentials + .store_refresh_material_with_object_id( + refresh_material_scope(&state), + "obsolete-grant", + &HashMap::from([("refresh_token".into(), "obsolete".into())]), + &HashMap::new(), + ) + .await + .unwrap() + .remove("refresh_token") + .unwrap(); + enqueue_pending_secret_deletion(&mut state, "refresh_token", obsolete); + put_refresh_state(&store, &state).await.unwrap(); + let sweep_snapshot = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + // Fail pre-mint cleanup, then stop at replacement-grant storage after + // the issuer has rotated the grant but before the receipt CAS. + credentials.fail_next_delete(); + let (storing, release) = credentials.gate_next_store(); + let mint_store = store.clone(); + let mint_credentials = credentials.clone(); + let mint = tokio::spawn(async move { + refresh_provider_credential( + &mint_store, + "default", + &mint_credentials, + None, + "cleanup-during-mint", + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + }); + tokio::time::timeout(Duration::from_secs(10), storing) + .await + .unwrap() + .unwrap(); + let before_cleanup = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(before_cleanup.status, "refresh_in_progress"); + let cleanup_store = store.clone(); + let cleanup_credentials = credentials.clone(); + let mut sweep = tokio::spawn(async move { + super::refresh_states( + &cleanup_store, + Some(&cleanup_credentials), + None, + vec![sweep_snapshot], + current_time_ms(), + ) + .await; + }); + assert!( + tokio::time::timeout(Duration::from_millis(100), &mut sweep) + .await + .is_err(), + "cleanup must wait for the active mint" + ); + let during_cleanup = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(during_cleanup, before_cleanup); + release.send(()).unwrap(); + assert_eq!(mint.await.unwrap().unwrap().status, "refreshed"); + tokio::time::timeout(Duration::from_secs(10), sweep) + .await + .unwrap() + .unwrap(); + let committed = get_refresh_state( + &store, + "default", + provider.object_id(), + "MS_GRAPH_ACCESS_TOKEN", + ) + .await + .unwrap() + .unwrap(); + assert_eq!(committed.status, "refreshed"); + assert_ne!(committed.next_refresh_at_ms, i64::MAX); + assert!(committed.pending_secret_deletions.is_empty()); + assert_eq!( + credentials + .resolve_refresh_material( + refresh_material_scope(&committed), + &committed.secret_material_handles, + ) + .await + .unwrap()["refresh_token"], + "replacement-grant" + ); + assert_eq!(credentials.stored_credential_count(), Some(2)); + issuer.verify().await; + } + #[tokio::test] async fn rotated_refresh_token_store_failure_requires_reauthorization() { let mock_server = MockServer::start().await; @@ -3466,6 +3913,48 @@ mod tests { ); } + #[test] + fn automatic_refresh_honors_recovery_schedule_without_disabling_due_retries() { + let now_ms = 10_000; + for recovery in [ + ProviderCredentialRefreshRecoveryAction::Retry, + ProviderCredentialRefreshRecoveryAction::FixConfiguration, + ProviderCredentialRefreshRecoveryAction::Investigate, + ] { + let mut state = StoredProviderCredentialRefreshState { + recovery_action: recovery as i32, + next_refresh_at_ms: now_ms + 1, + ..Default::default() + }; + assert_eq!( + ensure_automatic_refresh_ready(&state, now_ms) + .unwrap_err() + .code(), + tonic::Code::Unavailable, + ); + state.next_refresh_at_ms = now_ms; + ensure_automatic_refresh_ready(&state, now_ms).unwrap(); + } + let state = StoredProviderCredentialRefreshState { + recovery_action: ProviderCredentialRefreshRecoveryAction::Reauthorize as i32, + next_refresh_at_ms: now_ms, + ..Default::default() + }; + assert_eq!( + ensure_automatic_refresh_ready(&state, now_ms) + .unwrap_err() + .code(), + tonic::Code::FailedPrecondition, + ); + // A healthy credential's proactive schedule must not block an on-read + // mint requested to meet a longer minimum remaining lifetime. + let state = StoredProviderCredentialRefreshState { + next_refresh_at_ms: now_ms + 1, + ..Default::default() + }; + ensure_automatic_refresh_ready(&state, now_ms).unwrap(); + } + #[tokio::test] async fn refresh_worker_skips_parked_reauthorization_state() { let store = test_store().await; diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 493fab2b1c..7c96f81787 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -142,8 +142,9 @@ mod tests { // profile credentials reachable from stored policies and provider profiles. // Legacy payloads decode empty owners; the gateway rebuilds their authority // from effective policy rather than trusting persisted owner stamps. + // Operator credential export adds four public messages and no durable types. const PUBLIC_RPC_SCHEMA_SHA256: &str = - "3fead4a66e57e6828072109564fa25b5b65ef541b35098d9ed7f0785387fe0aa"; + "f986011b0ced61066dd0787dd86ca50f44c621ce51abc885eef2835160a71109"; const DURABLE_SCHEMA_SHA256: &str = "96269474903e077df4d4861db0dd1004b8a7205604ffadcdaff98d0124f18147"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = @@ -555,12 +556,12 @@ mod tests { } assert_eq!( compiled_method_count, - 102 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), + 103 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), "classify every compiled RPC" ); assert_eq!( methods.len(), - 77 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), + 78 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len(), "inventory every public gateway RPC" ); assert_eq!( @@ -568,7 +569,7 @@ mod tests { .iter() .filter(|method| method.starts_with("openshell.v1.OpenShell/")) .count(), - 77 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() + 78 + PROVIDER_READINESS_RPC_SIGNATURES.len() + PEER_OWNER_RPC_SIGNATURES.len() ); assert!(methods.iter().all(|method| !method.contains(".storage."))); @@ -612,7 +613,7 @@ mod tests { overlap_hash.as_str(), ), ( - (307, 27), + (311, 27), (93, 21), (81, 21), PUBLIC_RPC_SCHEMA_SHA256, diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 414251734d..146169d7ba 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -327,6 +327,13 @@ impl OpenShell for TestOpenShell { )) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &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 de0cfee394..dfb89f18c3 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -361,6 +361,13 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + async fn get_provider_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this mock")) + } + async fn get_provider( &self, _: tonic::Request, diff --git a/crates/openshell-supervisor-network/data/sandbox-policy.rego b/crates/openshell-supervisor-network/data/sandbox-policy.rego index 3389abef37..3c98c7c2e9 100644 --- a/crates/openshell-supervisor-network/data/sandbox-policy.rego +++ b/crates/openshell-supervisor-network/data/sandbox-policy.rego @@ -1086,7 +1086,9 @@ endpoint_path_matches_request(ep, request) if { endpoint_path_matches_request(ep, request) if { path := object.get(ep, "path", "") path != "" - path_matches(request.path, path) + # Registered by the embedded engine; use the same Rust matcher as route + # selection. Rule paths retain the distinct path_matches semantics above. + openshell.endpoint_path_matches(path, request.path) } # An endpoint has extended config if it specifies an L7 protocol, allowed_ips, diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index 038f86fd56..31ac623352 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -8613,6 +8613,136 @@ network_policies: .unwrap(); } + async fn assert_endpoint_path_deny(selector: &str, method: &str, path: &str, denied: bool) { + let data = format!( + r#" +network_policies: + route_api: + endpoints: + - host: gateway.example.test + port: 443 + path: "{selector}" + protocol: rest + enforcement: enforce + rules: + - allow: {{ method: "*", path: "**" }} + deny_rules: + - {{ method: POST, path: "**" }} + - host: gateway.example.test + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: {{ method: "*", path: "**" }} + binaries: + - {{ path: /usr/bin/node }} +"#, + ); + let engine = OpaEngine::from_strings(TEST_POLICY, &data) + .expect("compatible overlapping endpoints must be accepted"); + let input = NetworkInput { + host: "gateway.example.test".into(), + port: 443, + binary_path: PathBuf::from("/usr/bin/node"), + binary_sha256: "unused".into(), + ancestors: vec![], + cmdline_paths: vec![], + }; + let (endpoints, generation) = engine + .query_endpoint_configs_with_generation(&input) + .unwrap(); + let configs: Vec<_> = endpoints + .iter() + .map(|endpoint| crate::l7::parse_l7_config(endpoint).unwrap()) + .collect(); + assert_eq!(configs.len(), 2); + if denied { + assert_eq!( + select_l7_config_for_path(&configs, path).unwrap().path, + selector + ); + } + let tunnel_engine = engine.clone_engine_for_tunnel(generation).unwrap(); + let ctx = L7EvalContext { + host: "gateway.example.test".into(), + port: 443, + request_default_port: Some(443), + policy_name: "route_api".into(), + binary_path: "/usr/bin/node".into(), + ..Default::default() + }; + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_with_route_selection( + &configs, + tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + // Respond to any forwarded request so a bypass produces an observable + // 200, rather than a timeout that could hide the authorization result. + let server = tokio::spawn(async move { + let mut request = [0u8; 8192]; + let n = upstream.read(&mut request).await.unwrap(); + if n != 0 { + upstream + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + } + n + }); + app.write_all( + format!("{method} {path} HTTP/1.1\r\nHost: gateway.example.test\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .as_bytes(), + ) + .await + .unwrap(); + let response = tokio::time::timeout( + std::time::Duration::from_secs(5), + read_http_headers(&mut app), + ) + .await + .expect("relay must answer the request"); + drop(app); + tokio::time::timeout(std::time::Duration::from_secs(5), relay) + .await + .expect("relay must finish") + .unwrap() + .unwrap(); + let forwarded_bytes = server.await.unwrap(); + let response = String::from_utf8_lossy(&response); + let status = if denied { "403 Forbidden" } else { "200 OK" }; + assert!( + response.starts_with(&format!("HTTP/1.1 {status}")), + "selector={selector}, request={method} {path}: {response}" + ); + assert_eq!(forwarded_bytes == 0, denied); + } + + #[tokio::test] + async fn endpoint_path_deny_covers_subtree_root() { + assert_endpoint_path_deny("/p/**", "POST", "/p", true).await; + } + + #[tokio::test] + async fn endpoint_path_deny_covers_multisegment_star() { + assert_endpoint_path_deny("/v1/*", "POST", "/v1/a/b", true).await; + } + + #[tokio::test] + async fn endpoint_path_deny_preserves_matching_and_nonmatching_controls() { + assert_endpoint_path_deny("/p/**", "POST", "/p/child", true).await; + assert_endpoint_path_deny("/v1/*", "POST", "/v1/a", true).await; + assert_endpoint_path_deny("/p/**", "GET", "/p", false).await; + assert_endpoint_path_deny("/p/**", "POST", "/public", false).await; + assert_endpoint_path_deny("/p/**", "POST", "/prefix", false).await; + } + /// Policy allowing GET on both `/repos/**` and `/admin/**` for the same /// host:port, so an encoded-slash denial can only come from the /// per-endpoint `allow_encoded_slash` scoping. diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 1779830585..62cf0582e5 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -6,6 +6,9 @@ //! Wraps [`regorus::Engine`] to evaluate Rego policies for sandbox network //! access decisions. The engine is loaded once at sandbox startup and queried //! on every proxy CONNECT request. +//! +//! The baked policy uses the `openshell.endpoint_path_matches` native builtin +//! registered here to share endpoint selector semantics with Rust route selection. use miette::Result; use openshell_core::host_pattern::HostSelector; @@ -36,6 +39,24 @@ mod raw_schema; /// passthroughs. They reference `data.sandbox.*` for policy data. const BAKED_POLICY_RULES: &str = include_str!("../data/sandbox-policy.rego"); +fn new_regorus_engine() -> Result { + let mut engine = regorus::Engine::new(); + engine + .add_extension( + "openshell.endpoint_path_matches".into(), + 2, + Box::new(|params: Vec| { + let pattern = params[0].as_string()?; + let path = params[1].as_string()?; + Ok(regorus::Value::from( + openshell_core::endpoint_path::matches(pattern, path), + )) + }), + ) + .map_err(|_| miette::miette!("failed to register endpoint path matcher"))?; + Ok(engine) +} + /// Maximum number of fixed categories in one policy-load error. const POLICY_VALIDATION_DIAGNOSTIC_MAX_ITEMS: usize = 8; /// Maximum byte length of one policy-load error, including its omission marker. @@ -358,7 +379,7 @@ impl OpaEngine { // original error, including its source chain, at the load boundary. let yaml_str = std::fs::read_to_string(data_path) .map_err(|_| miette::miette!("failed to read YAML policy data file"))?; - let mut engine = regorus::Engine::new(); + let mut engine = new_regorus_engine()?; engine .add_policy_from_file(policy_path) .map_err(|_| miette::miette!("failed to load Rego policy"))?; @@ -405,7 +426,7 @@ impl OpaEngine { require_binary_identity: bool, validate_middleware_config: Option<&MiddlewareConfigValidator>, ) -> Result { - let mut engine = regorus::Engine::new(); + let mut engine = new_regorus_engine()?; engine .add_policy("policy.rego".into(), policy.into()) .map_err(|_| miette::miette!("failed to load Rego policy"))?; @@ -499,7 +520,7 @@ impl OpaEngine { emit_l7_config_warnings(&expansion_warnings, "L7 access preset expansion warning"); let data_json = data.to_string(); - let mut engine = regorus::Engine::new(); + let mut engine = new_regorus_engine()?; engine .add_policy("policy.rego".into(), BAKED_POLICY_RULES.into()) .map_err(|_| miette::miette!("failed to load Rego policy"))?; @@ -5440,7 +5461,7 @@ process: } fn eval_l7_raw_data(data: serde_json::Value, input: serde_json::Value) -> bool { - let mut engine = regorus::Engine::new(); + let mut engine = new_regorus_engine().unwrap(); engine .add_policy("policy.rego".into(), TEST_POLICY.into()) .unwrap(); @@ -5457,7 +5478,7 @@ process: fn token_grant_owner_engine(policies: serde_json::Value) -> OpaEngine { // Owner identities are trusted runtime metadata, so these Rego tests // load the already-normalized data that follows policy conversion. - let mut engine = regorus::Engine::new(); + let mut engine = new_regorus_engine().expect("register runtime builtins"); engine .add_policy("policy.rego".into(), TEST_POLICY.into()) .expect("load owner admission policy"); @@ -6024,7 +6045,7 @@ network_policies: "#; let data_json: serde_json::Value = serde_yml::from_str(data).expect("fixture should parse as YAML"); - let mut rego = regorus::Engine::new(); + let mut rego = new_regorus_engine().unwrap(); rego.add_policy("policy.rego".into(), TEST_POLICY.into()) .expect("policy should load"); rego.add_data_json(&data_json.to_string()) @@ -6097,6 +6118,91 @@ network_policies: ); } + #[test] + fn endpoint_path_matching_uses_canonical_rust_semantics() { + let engine = OpaEngine::from_strings(TEST_POLICY, "network_policies: {}\n").unwrap(); + let mut rego = engine.engine.lock().unwrap(); + for (pattern, path, expected) in [ + ("", "/p", true), + ("**", "/p/a", true), + ("/**", "/p/a", true), + ("/p", "/p", true), + ("/p", "/p/a", false), + ("/p/**", "/p", true), + ("/p/**", "/p/", true), + ("/p/**", "/p/a/b", true), + ("/p/**", "/prefix", false), + ("/v1/*", "/v1/a", true), + ("/v1/*", "/v1/a/b", true), + ("/v1/*", "/v1/a.json", true), + ("/v1/*", "/v1/a.b/c.d", true), + ("/v1/*", "/v2/a", false), + ("/v?/items", "/v1/items", true), + ("/v[12]/*", "/v2/a/b", true), + // A trailing /** treats its prefix literally, including glob syntax. + ("/v*/**", "/v1/a", false), + ("/v*/**", "/v*/a", true), + // Rust glob treats braces literally and invalid patterns as exact paths. + ("/a{b,c}", "/ab", false), + ("/a{b,c}", "/a{b,c}", true), + ("/[", "/[", true), + ("/[", "/p", false), + ] { + assert_eq!( + openshell_core::endpoint_path::matches(pattern, path), + expected + ); + set_regorus_input( + &mut rego, + serde_json::json!({ + "endpoint": { "path": pattern }, + "request": { "path": path }, + }), + ) + .unwrap(); + let result = rego + .eval_query( + "data.openshell.sandbox.endpoint_path_matches_request(input.endpoint, input.request)".into(), + false, + ) + .unwrap(); + assert_eq!( + !result.result.is_empty(), + expected, + "endpoint selector {pattern:?}, request path {path:?}", + ); + } + } + + #[test] + fn endpoint_path_matching_preserves_rule_path_globs() { + let engine = OpaEngine::from_strings(TEST_POLICY, "network_policies: {}\n").unwrap(); + let mut rego = engine.engine.lock().unwrap(); + for (pattern, path, expected) in [ + ("/p/**", "/p", false), + ("/p/**", "/p/a/b", true), + ("/v1/*", "/v1/a", true), + ("/v1/*", "/v1/a/b", false), + ] { + set_regorus_input( + &mut rego, + serde_json::json!({ "pattern": pattern, "path": path }), + ) + .unwrap(); + let result = rego + .eval_query( + "data.openshell.sandbox.path_matches(input.path, input.pattern)".into(), + false, + ) + .unwrap(); + assert_eq!( + !result.result.is_empty(), + expected, + "rule pattern {pattern:?}, request path {path:?}", + ); + } + } + #[test] fn l7_method_matching_case_insensitive() { let engine = l7_engine(); diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 56c9809cbc..ac59b7e3c5 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -689,30 +689,44 @@ fn sanitize_reason_for_audit(raw: &str) -> String { } /// One-line audit description of a chunk's target: binary, host, port, and -/// L7 method/path if present. Used by both the propose and approve/reject -/// audit events so the trace can be grepped by endpoint without parsing -/// JSON. +/// L7 method/path allows for every endpoint. Used by both the propose and +/// approve/reject audit events so the trace can be grepped by endpoint +/// without parsing JSON. fn summarize_chunk_for_audit(chunk: &PolicyChunk) -> String { let Some(rule) = chunk.proposed_rule.as_ref() else { return format!("rule_name:{}", chunk.rule_name); }; - let endpoint = rule.endpoints.first().map_or_else( - || "unknown".to_string(), - |ep| format!("{}:{}", ep.host, ep.port), - ); - let l7 = rule - .endpoints - .first() - .and_then(|ep| ep.rules.first()) - .and_then(|r| r.allow.as_ref()) - .map(|a| format!(" {} {}", a.method, a.path)) - .unwrap_or_default(); + let endpoints = if rule.endpoints.is_empty() { + "unknown".to_string() + } else { + rule.endpoints + .iter() + .map(summarize_endpoint_for_audit) + .collect::>() + .join(", ") + }; let binary = if chunk.binary.is_empty() { String::new() } else { format!(" by {}", chunk.binary) }; - format!("on {endpoint}{l7}{binary}") + format!("on {endpoints}{binary}") +} + +fn summarize_endpoint_for_audit(endpoint: &NetworkEndpoint) -> String { + let allows = endpoint + .rules + .iter() + .filter_map(|r| r.allow.as_ref()) + .map(|a| format!("{} {}", a.method, a.path)) + .collect::>() + .join(", "); + let target = format!("{}:{}", endpoint.host, endpoint.port); + if allows.is_empty() { + target + } else { + format!("{target} [{allows}]") + } } /// `GET /v1/proposals/{chunk_id}` — immediate state. One gateway call, no loop. @@ -2053,6 +2067,42 @@ mod tests { assert!(summary.contains("/usr/bin/curl")); } + #[test] + fn summarize_chunk_for_audit_lists_every_endpoint_and_allow() { + let allow = |method: &str, path: &str| L7Rule { + allow: Some(L7Allow { + method: method.to_string(), + path: path.to_string(), + ..Default::default() + }), + }; + let chunk = PolicyChunk { + binary: "/usr/bin/perl".to_string(), + proposed_rule: Some(NetworkPolicyRule { + name: "two".to_string(), + endpoints: vec![ + NetworkEndpoint { + host: "ports.ubuntu.com".to_string(), + port: 80, + rules: vec![allow("GET", "/ubuntu-ports/**"), allow("HEAD", "/x")], + ..Default::default() + }, + NetworkEndpoint { + host: "169.254.169.254".to_string(), + port: 80, + ..Default::default() + }, + ], + ..Default::default() + }), + ..Default::default() + }; + assert_eq!( + summarize_chunk_for_audit(&chunk), + "on ports.ubuntu.com:80 [GET /ubuntu-ports/**, HEAD /x], 169.254.169.254:80 by /usr/bin/perl" + ); + } + // Helpers — synthetic proposed rule + policy with that rule already // merged. Both reused across reload-readiness tests. fn proposed_curl_rule_for_github() -> NetworkPolicyRule { diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index db6ac2afe3..75c7b1fa5c 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -652,14 +652,19 @@ impl russh::server::Handler for SshHandler { channel: ChannelId, session: &mut Session, ) -> Result<(), Self::Error> { - session.channel_success(channel)?; // Only allocate a PTY when the client explicitly requested one via // pty_request. VS Code Remote-SSH sends shell_request *without* a // preceding pty_request and expects pipe-based I/O with clean LF line // endings. Forcing a PTY here caused CRLF translation which made // VS Code misdetect the platform as Windows (and then try to run // `powershell`). - self.start_shell(channel, session.handle(), None).await?; + match self.start_shell(channel, session.handle(), None).await { + Ok(()) => session.channel_success(channel)?, + Err(error) => { + warn!(%error, ?channel, "failed to start boundary shell"); + session.channel_failure(channel)?; + } + } Ok(()) } @@ -669,13 +674,21 @@ impl russh::server::Handler for SshHandler { data: &[u8], session: &mut Session, ) -> Result<(), Self::Error> { - session.channel_success(channel)?; let command = String::from_utf8_lossy(data).trim().to_string(); if command.is_empty() { + session.channel_success(channel)?; return Ok(()); } - self.start_shell(channel, session.handle(), Some(command)) - .await?; + match self + .start_shell(channel, session.handle(), Some(command)) + .await + { + Ok(()) => session.channel_success(channel)?, + Err(error) => { + warn!(%error, ?channel, "failed to start boundary exec"); + session.channel_failure(channel)?; + } + } Ok(()) } diff --git a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs index 4aea1c4cde..67e04dedfa 100644 --- a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs +++ b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs @@ -12,6 +12,7 @@ use openshell_isolation_interface::contract::{ use russh::ChannelMsg; use std::collections::VecDeque; use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::time::Duration; @@ -96,6 +97,28 @@ impl BoundaryExec for TestExec { } } +struct FailThenExec { + failed: AtomicBool, + session: Mutex>, +} + +#[async_trait::async_trait] +impl BoundaryExec for FailThenExec { + async fn exec(&self, _spec: ExecSpec) -> Result { + if !self.failed.swap(true, Ordering::AcqRel) { + return Err(BackendError::Unavailable( + "test boundary admission failure".to_string(), + )); + } + Ok(self + .session + .lock() + .unwrap() + .take() + .expect("test exec session")) + } +} + struct Control { stdin: DuplexStream, stdout: DuplexStream, @@ -155,6 +178,43 @@ async fn start_exec( channel } +#[tokio::test] +async fn exec_start_failure_keeps_ssh_session_usable() { + tokio::time::timeout(DEADLINE, async { + let (session, mut control) = exec_session(); + let client = test_client( + None, + Arc::new(FailThenExec { + failed: AtomicBool::new(false), + session: Mutex::new(Some(session)), + }), + russh::client::Config::default(), + ) + .await; + + let mut rejected = client.channel_open_session().await.unwrap(); + rejected.exec(true, "first").await.unwrap(); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Failure))); + + let mut channel = start_exec(&client).await; + control.stdout.shutdown().await.unwrap(); + control.stderr.shutdown().await.unwrap(); + control + .process + .status + .send_replace(Some(BoundaryExitStatus::Exited(0))); + loop { + match channel.wait().await { + Some(ChannelMsg::ExitStatus { exit_status: 0 }) => break, + Some(_) => {} + None => panic!("successful exec closed without an exit status"), + } + } + }) + .await + .expect("a rejected exec closed the SSH session"); +} + #[tokio::test] async fn full_input_allows_output_signals_and_channel_close() { tokio::time::timeout(DEADLINE, async { diff --git a/deploy/docker/Dockerfile.konflux.e2e-odh b/deploy/docker/Dockerfile.konflux.e2e-odh index 64def8ba58..ba5e71ab0b 100644 --- a/deploy/docker/Dockerfile.konflux.e2e-odh +++ b/deploy/docker/Dockerfile.konflux.e2e-odh @@ -27,6 +27,9 @@ ARG NEXTEST_VERSION ARG TARGETARCH ARG OPENSHELL_VERSION="" +RUN dnf install -y --nodocs --setopt=install_weak_deps=0 python3.11 && \ + dnf clean all + RUN RUST_ARCH=$(case "${TARGETARCH}" in amd64) echo x86_64;; arm64) echo aarch64;; *) exit 1;; esac) && \ RUST_TRIPLE="${RUST_ARCH}-unknown-linux-gnu" && \ tar xzf "/cachi2/output/deps/generic/cargo-nextest-${NEXTEST_VERSION}-${RUST_TRIPLE}.tar.gz" \ @@ -40,6 +43,7 @@ COPY .cargo/ .cargo/ COPY .config/ .config/ COPY crates/ crates/ COPY proto/ proto/ +COPY python/ python/ COPY providers/ providers/ COPY examples/ examples/ COPY e2e/ e2e/ @@ -59,6 +63,16 @@ RUN VER="${OPENSHELL_VERSION#v}" && \ --features e2e-odh \ --archive-file /opt/openshell/e2e-odh.tar.zst +RUN UV_ARCH=$(case "${TARGETARCH}" in amd64) echo x86_64;; arm64) echo aarch64;; *) exit 1;; esac) && \ + tar xzf "/cachi2/output/deps/generic/uv-${UV_ARCH}-unknown-linux-gnu.tar.gz" \ + -C /tmp && \ + install -m 0555 "/tmp/uv-${UV_ARCH}-unknown-linux-gnu/uv" /usr/local/bin/uv +COPY odh/requirements.txt /tmp/requirements.txt +RUN uv venv --python python3.11 /opt/odh-e2e-venv && \ + uv pip install --python /opt/odh-e2e-venv/bin/python --no-index --require-hashes \ + --find-links /cachi2/output/deps/pip -r /tmp/requirements.txt +RUN /opt/odh-e2e-venv/bin/python tasks/scripts/generate_python_proto.py + FROM registry.access.redhat.com/ubi9/ubi:9.8@sha256:25a147defd01e19674714f55d17538c8dbe55d8c305fa157ecc3f9c8977b05b6 ARG OC_VERSION @@ -67,7 +81,7 @@ ARG TARGETARCH RUN dnf install -y --nodocs --setopt=install_weak_deps=0 \ ca-certificates crypto-policies-scripts \ - python3.11 libxslt util-linux openssh-clients git-core tar \ + python3.11 libxslt util-linux openssh-clients git-core tar openssl \ && dnf clean all && \ update-crypto-policies --set DEFAULT:PQ @@ -76,22 +90,29 @@ COPY --from=builder --chmod=0555 \ COPY --from=builder --chmod=0555 \ /home/odh/openshell-e2e-odh/target/release/openshell /usr/local/bin/openshell COPY --from=builder /opt/openshell/e2e-odh.tar.zst /opt/openshell/e2e-odh.tar.zst +COPY --from=builder /opt/odh-e2e-venv /opt/odh-e2e-venv RUN OC_ARCH=$(case "${TARGETARCH}" in amd64) echo amd64;; arm64) echo arm64;; *) exit 1;; esac) && \ + UV_ARCH=$(case "${TARGETARCH}" in amd64) echo x86_64;; arm64) echo aarch64;; *) exit 1;; esac) && \ mkdir -p /tmp/oc /tmp/helm && \ tar xzf "/cachi2/output/deps/generic/openshift-client-linux-${OC_ARCH}-rhel9-${OC_VERSION}.tar.gz" \ -C /tmp/oc oc kubectl && \ install -m 0555 /tmp/oc/oc /tmp/oc/kubectl /usr/local/bin/ && \ tar xzf "/cachi2/output/deps/generic/helm-v${HELM_VERSION}-linux-${OC_ARCH}.tar.gz" \ -C /tmp/helm && \ - install -m 0555 "/tmp/helm/linux-${OC_ARCH}/helm" /usr/local/bin/helm + install -m 0555 "/tmp/helm/linux-${OC_ARCH}/helm" /usr/local/bin/helm && \ + tar xzf "/cachi2/output/deps/generic/uv-${UV_ARCH}-unknown-linux-gnu.tar.gz" \ + -C /tmp && \ + install -m 0555 "/tmp/uv-${UV_ARCH}-unknown-linux-gnu/uv" /usr/local/bin/uv WORKDIR /home/odh/openshell-e2e-odh COPY --chown=1000:1000 Cargo.toml Cargo.lock ./ +COPY --chown=1000:1000 pyproject.toml uv.lock ./ COPY --chown=1000:1000 .cargo/ .cargo/ COPY --chown=1000:1000 .config/ .config/ COPY --chown=1000:1000 crates/ crates/ COPY --chown=1000:1000 proto/ proto/ +COPY --from=builder --chown=1000:1000 /home/odh/openshell-e2e-odh/python/ python/ COPY --chown=1000:1000 providers/ providers/ COPY --chown=1000:1000 examples/ examples/ COPY --chown=1000:1000 e2e/ e2e/ @@ -108,8 +129,8 @@ RUN install -d -o 1000 -g 1000 results .kube /home/odh/.config/openshell && \ # sandbox connection uses ssh, and future repository tests need git. cargo # links the system libssl, libcrypto and libz, so run it once to catch a # missing library at build time. -RUN for tool in bash oc kubectl helm openshell cargo cargo-nextest \ - python3.11 xsltproc script ssh git tar base64; do \ +RUN for tool in bash oc kubectl helm openshell cargo cargo-nextest uv \ + python3.11 xsltproc script ssh git tar base64 openssl; do \ command -v "${tool}" >/dev/null || { echo "missing e2e runtime tool: ${tool}" >&2; exit 1; }; \ done && \ cargo nextest --version >/dev/null @@ -119,7 +140,9 @@ ENV HOME=/home/odh \ OPENSHELL_BIN=/usr/local/bin/openshell \ OPENSHELL_E2E_NEXTEST_ARCHIVE=/opt/openshell/e2e-odh.tar.zst \ OPENSHELL_E2E_DEPLOY_GATEWAY=1 \ - PYTHON_BIN=python3.11 + PYTHON_BIN=python3.11 \ + UV_PROJECT_ENVIRONMENT=/opt/odh-e2e-venv \ + PYTHONPATH=/home/odh/openshell-e2e-odh/python LABEL com.redhat.component="odh-openshell-e2e-container" \ description="ODH Shift-Left e2e tests for OpenShell" \ diff --git a/deploy/docker/Dockerfile.konflux.openclaw b/deploy/docker/Dockerfile.konflux.openclaw index de0ade2326..c3f9283ff8 100644 --- a/deploy/docker/Dockerfile.konflux.openclaw +++ b/deploy/docker/Dockerfile.konflux.openclaw @@ -80,7 +80,11 @@ RUN ln -s /usr/local/lib/openclaw/node_modules/openclaw/openclaw.mjs /usr/local/ COPY --chmod=0755 deploy/konflux/openclaw/openclaw-start.sh /usr/local/bin/openclaw-start COPY --chmod=0644 deploy/konflux/openclaw/profile.sh /etc/profile.d/openclaw.sh +# SQLite picks its temp directory by probing /var/tmp before /tmp with +# access(), which Landlock does not mediate, so it chooses /var/tmp and then +# fails to open files there. TMPDIR points it, and Node, at the writable /tmp. ENV HOME=/sandbox \ + TMPDIR=/tmp \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ NPM_CONFIG_PREFIX=/sandbox/.npm-global \ OPENCLAW_NO_AUTO_UPDATE=1 \ diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index eee878e576..1df3d65ce6 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -400,14 +400,15 @@ discovery endpoint or its TLS CA. | grpcRoute.backendTLSPolicy.enabled | bool | `false` | Create a BackendTLSPolicy resource for end-to-end TLS between the Gateway proxy and the OpenShell gateway pod. The traffic flow is: client → HTTPS → Gateway (terminate) → TLS (re-encrypt) → gateway pod. Requires server.disableTls=false and server.tls.enableMtls=false. The certgen hook auto-creates the backend CA ConfigMap. | | grpcRoute.backendTLSPolicy.hostname | string | `""` | Hostname the Gateway proxy validates against the backend's TLS certificate SAN. Defaults to the service FQDN (`..svc.cluster.local`) when empty, which matches the SAN included by both cert-manager and the pkiInitJob. | | grpcRoute.enabled | bool | `false` | Create a Gateway API GRPCRoute for the gateway service. | -| grpcRoute.gateway.className | string | `"eg"` | GatewayClass to reference. Envoy Gateway installs one named "eg". | -| grpcRoute.gateway.create | bool | `false` | When true, a Gateway resource is created in the release namespace. Set to false and provide name/namespace to attach to a pre-existing Gateway. | +| grpcRoute.gateway.className | string | `"eg"` | GatewayClass for a chart-created Gateway. Set this to the name exposed by your Gateway API implementation. Envoy Gateway uses "eg" in the OpenShell examples. Ignored when gateway.create is false. | +| grpcRoute.gateway.create | bool | `false` | When true, a Gateway resource is created in the release namespace. Set to false and provide name/namespace to attach to a pre-existing Gateway. Use sectionName to select a specific listener when needed. | | grpcRoute.gateway.listener.allowedRoutes | string | `"Same"` | "Same" restricts attached routes to the release namespace; "All" allows any namespace. | | grpcRoute.gateway.listener.port | int | `80` | Listener port for the generated Gateway resource. Use 443 with protocol HTTPS. | -| grpcRoute.gateway.listener.protocol | string | `"HTTP"` | Listener protocol for the generated Gateway resource: HTTP or HTTPS. HTTPS terminates TLS at the Envoy Gateway listener; pair it with server.disableTls=true so Envoy forwards plaintext to the gateway pod, and use OIDC for client identity (the gateway never sees the client cert). | +| grpcRoute.gateway.listener.protocol | string | `"HTTP"` | Listener protocol for the generated Gateway resource: HTTP or HTTPS. HTTPS terminates TLS at the Gateway listener; pair it with server.disableTls=true so the proxy forwards plaintext to the gateway pod, and use OIDC for client identity (the gateway never sees the client certificate). | | grpcRoute.gateway.listener.tls.certificateRefs | list | `[]` | certificateRefs for the HTTPS listener. Required when protocol is HTTPS. Each entry needs a `name` pointing at a kubernetes.io/tls Secret in the Gateway's namespace. May reference a cert-manager-issued Secret or the existing openshell-server-tls Secret (its SANs must include the external hostname). | -| grpcRoute.gateway.name | string | `""` | Name of the Gateway resource. Defaults to the chart fullname. | -| grpcRoute.gateway.namespace | string | `""` | Namespace of the Gateway referenced by the GRPCRoute parentRef. Defaults to the release namespace. | +| grpcRoute.gateway.name | string | `""` | Name of the Gateway resource. Defaults to ``. Set a distinct name when the selected controller creates a proxy Service with the Gateway name. When gateway.create is false, set this to the name of the existing Gateway. | +| grpcRoute.gateway.namespace | string | `""` | Namespace of the Gateway referenced by the GRPCRoute parentRef. Defaults to the release namespace. A chart-created Gateway always resides in the release namespace. | +| grpcRoute.gateway.sectionName | string | `""` | Listener name on an existing Gateway. Leave empty to let Gateway API listener matching select compatible listeners. | | grpcRoute.hostnames | list | `[]` | Hostnames the GRPCRoute matches on. Leave empty to match all hosts. | | grpcRoute.replicaRouting.enabled | bool | `false` | Route requests carrying an `x-openshell-replica` header straight to that gateway replica. The CLI sets the header on long-lived sandbox connections (SSH, port forwards, exec) using the owner the gateway reports, so they skip the relay through a peer replica. Renders one Service per replica. Requires workload.kind=statefulset, workload.allowMultiReplicaStatefulSet=true, and at most 15 replicas. | | imagePullSecrets | list | `[]` | Image pull secrets attached to gateway and helper pods. | diff --git a/deploy/helm/openshell/templates/_helpers.tpl b/deploy/helm/openshell/templates/_helpers.tpl index 44948c4fe3..2b0c360920 100644 --- a/deploy/helm/openshell/templates/_helpers.tpl +++ b/deploy/helm/openshell/templates/_helpers.tpl @@ -24,6 +24,14 @@ Create a default fully qualified app name. {{- end }} {{- end }} +{{/* +Name of the Gateway API Gateway referenced by the GRPCRoute. Preserve explicit +names unchanged because Gateway names may be longer than label values. +*/}} +{{- define "openshell.grpcRouteGatewayName" -}} +{{- default (include "openshell.fullname" .) .Values.grpcRoute.gateway.name -}} +{{- end }} + {{/* Create chart name and version as used by the chart label. */}} diff --git a/deploy/helm/openshell/templates/gateway.yaml b/deploy/helm/openshell/templates/gateway.yaml index 73c3d76037..3c2cc91901 100644 --- a/deploy/helm/openshell/templates/gateway.yaml +++ b/deploy/helm/openshell/templates/gateway.yaml @@ -4,7 +4,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: - name: {{ default (include "openshell.fullname" .) .Values.grpcRoute.gateway.name }} + name: {{ include "openshell.grpcRouteGatewayName" . }} namespace: {{ .Release.Namespace }} labels: {{- include "openshell.labels" . | nindent 4 }} diff --git a/deploy/helm/openshell/templates/grpcroute.yaml b/deploy/helm/openshell/templates/grpcroute.yaml index aea50e1294..9ea2ecfee7 100644 --- a/deploy/helm/openshell/templates/grpcroute.yaml +++ b/deploy/helm/openshell/templates/grpcroute.yaml @@ -11,8 +11,11 @@ metadata: {{- include "openshell.labels" . | nindent 4 }} spec: parentRefs: - - name: {{ default $fullname .Values.grpcRoute.gateway.name }} - namespace: {{ default .Release.Namespace .Values.grpcRoute.gateway.namespace }} + - name: {{ include "openshell.grpcRouteGatewayName" . }} + namespace: {{ ternary .Release.Namespace (default .Release.Namespace .Values.grpcRoute.gateway.namespace) .Values.grpcRoute.gateway.create }} + {{- with .Values.grpcRoute.gateway.sectionName }} + sectionName: {{ . | quote }} + {{- end }} {{- if .Values.grpcRoute.hostnames }} hostnames: {{- toYaml .Values.grpcRoute.hostnames | nindent 4 }} diff --git a/deploy/helm/openshell/tests/gateway_api_test.yaml b/deploy/helm/openshell/tests/gateway_api_test.yaml new file mode 100644 index 0000000000..b09c21f4b3 --- /dev/null +++ b/deploy/helm/openshell/tests/gateway_api_test.yaml @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: Gateway API resources +templates: + - templates/gateway.yaml + - templates/grpcroute.yaml +release: + name: openshell + namespace: openshell + +tests: + - it: preserves the chart fullname as the default Gateway name + template: templates/gateway.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + asserts: + - equal: + path: metadata.name + value: openshell + - equal: + path: spec.gatewayClassName + value: eg + + - it: references the same default Gateway from the GRPCRoute + template: templates/grpcroute.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + asserts: + - equal: + path: spec.parentRefs[0].name + value: openshell + - equal: + path: spec.parentRefs[0].namespace + value: openshell + - notExists: + path: spec.parentRefs[0].sectionName + + - it: keeps a chart-created Gateway parent in the release namespace + template: templates/grpcroute.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + grpcRoute.gateway.namespace: another-namespace + asserts: + - equal: + path: spec.parentRefs[0].namespace + value: openshell + + - it: renders an arbitrary GatewayClass and explicit Gateway name + template: templates/gateway.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + grpcRoute.gateway.className: custom-class + grpcRoute.gateway.name: custom-ingress + asserts: + - equal: + path: metadata.name + value: custom-ingress + - equal: + path: spec.gatewayClassName + value: custom-class + + - it: preserves an explicit Gateway name longer than 63 characters + template: templates/gateway.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + grpcRoute.gateway.name: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr + asserts: + - equal: + path: metadata.name + value: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr + + - it: references an explicit Gateway name longer than 63 characters unchanged + template: templates/grpcroute.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: true + grpcRoute.gateway.name: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr + asserts: + - equal: + path: spec.parentRefs[0].name + value: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr + + - it: attaches to a listener on an existing Gateway + template: templates/grpcroute.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: false + grpcRoute.gateway.name: platform-gateway + grpcRoute.gateway.namespace: gateway-system + grpcRoute.gateway.sectionName: grpc + asserts: + - equal: + path: spec.parentRefs[0].name + value: platform-gateway + - equal: + path: spec.parentRefs[0].namespace + value: gateway-system + - equal: + path: spec.parentRefs[0].sectionName + value: grpc + + - it: does not create a Gateway when attaching to an existing Gateway + template: templates/gateway.yaml + set: + grpcRoute.enabled: true + grpcRoute.gateway.create: false + grpcRoute.gateway.name: platform-gateway + asserts: + - hasDocuments: + count: 0 diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 19484b4f87..f88b7556b8 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -689,9 +689,10 @@ certManager: serverIpAddresses: - 127.0.0.1 -# Kubernetes Gateway API - HTTPRoute and Gateway resources. -# Requires a Gateway API controller in the cluster. Install Envoy Gateway via -# the skaffold.yaml releases or independently: +# Kubernetes Gateway API - GRPCRoute and Gateway resources. +# Requires a Gateway API controller in the cluster. OpenShell tests the +# examples with Envoy Gateway, which can be installed via skaffold.yaml or +# independently: # helm install eg oci://docker.io/envoyproxy/gateway-helm \ # --version v1.4.1 -n envoy-gateway-system --create-namespace grpcRoute: @@ -701,23 +702,34 @@ grpcRoute: hostnames: [] gateway: # -- When true, a Gateway resource is created in the release namespace. - # Set to false and provide name/namespace to attach to a pre-existing Gateway. + # Set to false and provide name/namespace to attach to a pre-existing + # Gateway. Use sectionName to select a specific listener when needed. create: false - # -- GatewayClass to reference. Envoy Gateway installs one named "eg". + # -- GatewayClass for a chart-created Gateway. Set this to the name exposed + # by your Gateway API implementation. Envoy Gateway uses "eg" in the + # OpenShell examples. Ignored when gateway.create is false. className: "eg" - # -- Name of the Gateway resource. Defaults to the chart fullname. + # -- Name of the Gateway resource. Defaults to ``. Set a distinct + # name when the selected controller creates a proxy Service with the + # Gateway name. When gateway.create is false, set this to the name of the + # existing Gateway. name: "" # -- Namespace of the Gateway referenced by the GRPCRoute parentRef. - # Defaults to the release namespace. + # Defaults to the release namespace. A chart-created Gateway always resides + # in the release namespace. namespace: "" + # -- Listener name on an existing Gateway. Leave empty to let Gateway API + # listener matching select compatible listeners. + sectionName: "" # Listener settings (only used when gateway.create is true). listener: # -- Listener port for the generated Gateway resource. Use 443 with protocol HTTPS. port: 80 # -- Listener protocol for the generated Gateway resource: HTTP or HTTPS. - # HTTPS terminates TLS at the Envoy Gateway listener; pair it with - # server.disableTls=true so Envoy forwards plaintext to the gateway pod, - # and use OIDC for client identity (the gateway never sees the client cert). + # HTTPS terminates TLS at the Gateway listener; pair it with + # server.disableTls=true so the proxy forwards plaintext to the gateway + # pod, and use OIDC for client identity (the gateway never sees the client + # certificate). protocol: HTTP # -- "Same" restricts attached routes to the release namespace; "All" allows any namespace. allowedRoutes: Same diff --git a/deploy/konflux/build-local.sh b/deploy/konflux/build-local.sh index 7f843927f2..9602b628ef 100755 --- a/deploy/konflux/build-local.sh +++ b/deploy/konflux/build-local.sh @@ -101,6 +101,7 @@ build_image() { prefetch_input="[ {\"path\": \".\", \"type\": \"cargo\"}, {\"path\": \"e2e/rust\", \"type\": \"cargo\"}, + {\"path\": \"odh\", \"type\": \"pip\", \"requirements_files\": [\"requirements.txt\"], \"binary\": {\"packages\": \":all:\", \"arch\": \"x86_64,aarch64\", \"os\": \"linux\", \"py_version\": 311, \"py_impl\": \"cp\"}}, {\"path\": \"${konfig_dir}\", \"type\": \"rpm\"}, {\"path\": \"${konfig_dir}\", \"type\": \"generic\", \"lockfile\": \"generic-fetcher.yaml\"} ]" diff --git a/deploy/konflux/e2e-odh/README.md b/deploy/konflux/e2e-odh/README.md index b601246cc0..43babcebcc 100644 --- a/deploy/konflux/e2e-odh/README.md +++ b/deploy/konflux/e2e-odh/README.md @@ -1,6 +1,6 @@ # ODH E2E Image Build -The downstream ODH Konflux e2e image is a separate test artifact. Its +The ODH Konflux e2e image is a separate test artifact. Its multi-stage UBI9 build compiles the CLI and a nextest archive from two independent Cargo lockfiles on the Red Hat rust-builder image, with crates, RPMs, and cluster tools prefetched by Hermeto for a network-isolated build. The runtime image carries the compiled @@ -17,8 +17,30 @@ are forwarded through the tier runner to nextest before teardown. Extracted client mTLS material is stored in a mode `0700` directory with mode `0600` files. The Rust tests consume the deployed gateway. -Konflux onboarding must use the `linux-m2xlarge/*` builders and retain both -Cargo prefetch inputs (`.` and `e2e/rust`). The image name is -`odh-openshell-e2e`; Konflux automation generates the Tekton YAML files. +The image also carries a fully prefetched Python 3.11 test environment and +generated protobuf bindings, so later named Python test modes can execute +without runtime dependency downloads. + +Konflux onboarding must use the `linux-m2xlarge/*` builders and retain the +three Hermeto prefetch inputs: Cargo at `.` and `e2e/rust`, plus the locked +Python dependencies at `odh`: + +```json +{ + "path": "odh", + "type": "pip", + "requirements_files": ["requirements.txt"], + "binary": { + "packages": ":all:", + "arch": "x86_64,aarch64", + "os": "linux", + "py_version": 311, + "py_impl": "cp" + } +} +``` + +The image name is `odh-openshell-e2e`; Konflux automation generates the +Tekton YAML files. Before merging, validate the local hermetic build on both `linux/amd64` and `linux/arm64` using `PLATFORM` with `deploy/konflux/build-local.sh e2e-odh`. diff --git a/deploy/konflux/e2e-odh/generic-fetcher.yaml b/deploy/konflux/e2e-odh/generic-fetcher.yaml index 69d2cf347c..154b2b63c7 100644 --- a/deploy/konflux/e2e-odh/generic-fetcher.yaml +++ b/deploy/konflux/e2e-odh/generic-fetcher.yaml @@ -5,6 +5,12 @@ metadata: version: "1.0" artifacts: + - download_url: https://github.com/astral-sh/uv/releases/download/0.11.29/uv-x86_64-unknown-linux-gnu.tar.gz + checksum: "sha256:04f8b82f5d47f0512dcd32c67a4a6f16a0ea27c81537c338fd0ad6b23cebe829" + filename: uv-x86_64-unknown-linux-gnu.tar.gz + - download_url: https://github.com/astral-sh/uv/releases/download/0.11.29/uv-aarch64-unknown-linux-gnu.tar.gz + checksum: "sha256:94500fb064ae3c971a873cba64d94694c50677e0a4dbf78735c80509e7429919" + filename: uv-aarch64-unknown-linux-gnu.tar.gz - download_url: https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.20.0/openshift-client-linux-amd64-rhel9-4.20.0.tar.gz checksum: "sha256:dbc11aa32c8fa967c47c46900c524798b782b355615c9ca8ec2bbba647850420" filename: openshift-client-linux-amd64-rhel9-4.20.0.tar.gz diff --git a/deploy/konflux/openclaw/README.md b/deploy/konflux/openclaw/README.md index d21f1a81bd..4cc682b405 100644 --- a/deploy/konflux/openclaw/README.md +++ b/deploy/konflux/openclaw/README.md @@ -4,7 +4,7 @@ ## Status -This image is an unsupported reference composition. It is published only on quay.io/opendatahub, it is not RHOAI product content, and it is not the default sandbox workload image. It targets midstream main 23ac1e1c2 (v0.1.2-rhaiv.2). Read [Verified vs not verified](#verified-vs-not-verified) before relying on it. +This image is an unsupported reference composition. It is published only on quay.io/opendatahub, it is not RHOAI product content, and it is not the default sandbox workload image. It targets midstream main 4f6b3ad8 (v0.1.2-rhaiv.8 plus 18 commits). Read [Verified vs not verified](#verified-vs-not-verified) before relying on it. ## What's inside @@ -13,6 +13,7 @@ This image is an unsupported reference composition. It is published only on quay - User 1000:1000 (`sandbox`), `HOME=/sandbox`, `WORKDIR /`, no `/sandbox` directory, no ENTRYPOINT and no baked sandbox policy. OpenShell creates and owns the workspace. - The `DEFAULT:PQ` system crypto policy, so Node's TLS 1.3 prefers X25519MLKEM768. `python3` is present as a dependency of `crypto-policies-scripts`. - OpenClaw defaults `OPENCLAW_NO_AUTO_UPDATE=1`, `DO_NOT_TRACK=1`, `OPENCLAW_DISABLE_BONJOUR=1` and `OPENCLAW_OFFLINE=1`, set in the image env and again in `/etc/profile.d/openclaw.sh` for login shells. Inherited base env such as `APP_ROOT` or `NPM_RUN` is harmless. +- `TMPDIR=/tmp`, also set in both places. Without it, SQLite picks `/var/tmp`, which the sandbox filesystem policy does not grant, and OpenClaw fails with `unable to open database file`. - Not included: git, gzip, ps, Chromium and chat-channel plugins. ## Build locally @@ -66,12 +67,16 @@ openshell sandbox exec --name openclaw -- openclaw-start agent exec "Summarize R | `MODEL_BASE_URL` | OpenAI-compatible base URL. It must use exactly the host and port from the provider profile. | | `MODEL_ID` | Model name served at that URL. | | `CUSTOM_API_KEY` | Injected by the provider as a placeholder. For an endpoint without auth, create the provider with `--credential CUSTOM_API_KEY=unused`; the provider also supplies the egress rule for the model endpoint. | +| `MODEL_MAX_TOKENS` | Optional output token limit for the model. Onboarding defaults to 4096. | +| `MODEL_CONTEXT_WINDOW` | Optional context window for the model. | +| `MODEL_REASONING` | Optional `true` or `false`. Onboarding defaults to `false`. | | `OPENCLAW_*`, `DO_NOT_TRACK` | Update, telemetry, Bonjour and download defaults. Override them with `--env`. | - `openclaw-start` onboards only when `~/.openclaw/openclaw.json` is missing. Delete that file and its `.bak` to onboard again. Deleting `~/.openclaw` resets all OpenClaw state. - Start OpenClaw through `openclaw-start` or a login-shell exec. `sandbox exec --no-login-shell` skips the profile.d defaults. - With no arguments, `openclaw-start` runs `openclaw tui --local`, which needs a TTY, so don't combine it with `--detach`. When the TUI exits, the sandbox is Completed. `openshell sandbox start openclaw` restarts it with its state intact. -- Set `contextWindow` and `maxTokens` for the model under `models.providers.openshell-model` with `openclaw config set` so they match your server. +- `MODEL_MAX_TOKENS`, `MODEL_CONTEXT_WINDOW` and `MODEL_REASONING` apply only at onboarding, like the other `MODEL_*` variables. For a reasoning model, set `MODEL_REASONING=true` and raise `MODEL_MAX_TOKENS` (for example to 32768), or the model can spend the default 4096 tokens thinking and OpenClaw reports `Agent couldn't generate a response`. To change them after onboarding, use `openclaw config set 'models.providers.openshell-model.models[0].maxTokens' `. +- On every start, `openclaw-start` clears the setgid bit on directories under `~/.openclaw` and `~/.openclaw-worker`. The Kubernetes driver's pod `fsGroup` makes the workspace volume setgid, and OpenClaw rejects private directories that inherit the bit (`insecure permissions 2700`). Run OpenClaw through `openclaw-start` at least once per sandbox start so this repair happens. ## Policy @@ -86,7 +91,7 @@ Best effort, not validated. Onboarding configures OpenClaw's gateway on loopback - OpenShell sandboxes need OCP 4.19 or later (Landlock ABI 3 or newer). - On RHCOS 9 the sandbox runs in legacy read-only mode. OpenClaw's gateway then treats every client as remote, so the Control UI always needs device approval, and Python or Go servers in the sandbox fail at `accept()`. OpenClaw's embedded MCP loopback server should still work. - EDR agents such as CrowdStrike Falcon may kill OpenClaw entirely, not only its sqlite workers. That includes image builds, where any step touching the OpenClaw package path can be killed. Build on sanctioned infrastructure without such a policy, such as Konflux, and run `smoke-test.sh` on a host or CI runner without it. Nodes running such an agent may kill OpenClaw inside sandboxes too. -- OpenClaw calls `os.userInfo()` unguarded in several paths, so an SCC-assigned UID with no passwd entry may break status, session or TUI commands. This is unverified. +- OpenClaw calls `os.userInfo()` unguarded in several paths. Onboarding, `agent exec` and the TUI work as a UID with no passwd entry; `status` and session commands under an SCC-assigned UID are unverified. - `web_fetch` rejects OpenShell's synthetic DNS answers (198.18.0.0/15, fc00::/7) unless `tools.web.fetch.ssrfPolicy.allowRfc2544BenchmarkRange` and `tools.web.fetch.ssrfPolicy.allowIpv6UniqueLocalRange` are set, per OpenClaw's `docs/tools/web.md`. This is unverified. - Updating the image never touches OpenClaw state on the sandbox's persistent volume. @@ -94,18 +99,18 @@ Best effort, not validated. Onboarding configures OpenClaw's gateway on loopback Verified: -- [x] `build-local.sh openclaw` on GitHub-hosted Ubuntu 24.04 runners (podman 4.9.3), amd64 and native arm64: the hermetic build succeeds, and every `smoke-test.sh` check passes on both arches. That covers arch, config, labels, `openclaw --version`, layout, `DEFAULT:PQ` with Node negotiating X25519MLKEM768, first-run onboarding and its idempotence as UID 51234 with no passwd entry on a read-only rootfs, the login-shell env, and the missing-env exit. +- [x] `build-local.sh openclaw` on GitHub-hosted Ubuntu 24.04 runners (podman 4.9.3), amd64 and native arm64: the hermetic build succeeds, and every `smoke-test.sh` check that existed then passes on both arches. That covers arch, config, labels, `openclaw --version`, layout, `DEFAULT:PQ` with Node negotiating X25519MLKEM768, first-run onboarding and its idempotence as UID 51234 with no passwd entry on a read-only rootfs, the login-shell env, and the missing-env exit. The later `setgid-home`, `model-overrides` and `tmpdir` checks, and the `TMPDIR` assertion in `login-shell`, have only run on linux/amd64 with podman 5.8.7. - [x] Host unit tests for `openclaw-start` (with a fake OpenClaw CLI), `profile.sh` and `check-glibc.sh`. - [x] `build-local.sh`: a stub trace shows the four existing components build unchanged, and the npm files are restored even when the build fails. - [x] The glibc ceiling lint fails the build at 2.33 (`@openclaw/fs-safe-linux-x64-gnu` needs 2.34). - [x] Trivy config scan of the Dockerfile is clean, and the license check adds only `rpms.lock.yaml`. +- [x] On kind (Kubernetes 1.37, rootless podman) with the midstream gateway and Kubernetes driver from the commit in [Status](#status), and a workspace volume that honors `fsGroup` (setgid `/sandbox`), running as UID 10001 with no passwd entry: `openshell profile lint` and import of the example profile, provider credential injection and egress through the profile alone, first-run onboarding, a headless `agent exec` turn and an interactive TUI turn against a llama.cpp model and against a stub reasoning model. The private agent directory is 0700 and no directory under `~/.openclaw` keeps setgid. Not verified: -- [ ] Any OpenShell sandbox run, including provider credential injection and `openshell profile lint` of the example profile. -- [ ] Kubernetes and OpenShift (PVC seeding with no `/sandbox`, SCC arbitrary UID, exec env path) and RHCOS 9 legacy mode. -- [ ] Agent turns, the TUI and the Control UI. -- [ ] `os.userInfo()` without a passwd entry. +- [ ] OpenShift (SCC arbitrary UID, CSI-backed workspace volumes) and RHCOS 9 legacy mode. +- [ ] The Control UI. +- [ ] `os.userInfo()` in `status` and session commands without a passwd entry. Once the component is onboarded, the Konflux PR pipeline is the first full build of this Dockerfile on both arches. It runs the in-build checks (the hermetic npm and rpm install, the OpenClaw version pin check, which only reads `package.json`, the glibc ceiling lint and the permissions check) plus Konflux's scans. It does not run `smoke-test.sh`, so after each OpenClaw or base bump, re-run `build-local.sh openclaw` on a host or CI runner without such an EDR policy, or add a Konflux IntegrationTestScenario that runs `smoke-test.sh` against the built image. diff --git a/deploy/konflux/openclaw/openclaw-start.sh b/deploy/konflux/openclaw/openclaw-start.sh index 71b89ac20a..5cf66447ed 100755 --- a/deploy/konflux/openclaw/openclaw-start.sh +++ b/deploy/konflux/openclaw/openclaw-start.sh @@ -15,9 +15,41 @@ if [[ -n "${OPENCLAW_PROFILE:-}" ]]; then exit 2 fi -config="${OPENCLAW_CONFIG_PATH:-${OPENCLAW_STATE_DIR:-${OPENCLAW_HOME:-$HOME}/.openclaw}/openclaw.json}" +state_dir="${OPENCLAW_STATE_DIR:-${OPENCLAW_HOME:-$HOME}/.openclaw}" +config="${OPENCLAW_CONFIG_PATH:-${state_dir}/openclaw.json}" + +# The Kubernetes driver sets the pod fsGroup, so kubelet makes the workspace +# volume setgid and every directory created under it inherits the bit. +# OpenClaw requires its private directories to be exactly 0700 and rejects +# 2700. Create its state directories without setgid so new subdirectories +# don't inherit it, and repair directories left by earlier runs. +prepare_state_dirs() { + local dir + for dir in "${state_dir}" "${HOME}/.openclaw-worker"; do + # shellcheck disable=SC2174 # only the state directory itself must be private + mkdir -p -m 0700 "${dir}" + # Best effort: the agent workspace lives under the state directory, so + # an unreadable or vanishing directory there must not block startup. + if ! find "${dir}" -xdev -ignore_readdir_race -type d -perm -2000 -user "$(id -u)" -exec chmod g-s {} +; then + echo "openclaw-start: warning: could not clear setgid everywhere under ${dir}" >&2 + fi + # mkdir -m does not touch an existing directory's mode. + chmod 0700 "${dir}" + done +} if [[ ! -e "${config}" ]]; then + for name in MODEL_CONTEXT_WINDOW MODEL_MAX_TOKENS; do + if [[ -n "${!name:-}" && ! "${!name}" =~ ^[1-9][0-9]*$ ]]; then + echo "openclaw-start: ${name} must be a positive integer, got '${!name}'" >&2 + exit 2 + fi + done + if [[ -n "${MODEL_REASONING:-}" && "${MODEL_REASONING}" != "true" && "${MODEL_REASONING}" != "false" ]]; then + echo "openclaw-start: MODEL_REASONING must be true or false, got '${MODEL_REASONING}'" >&2 + exit 2 + fi + missing=() if [[ -z "${MODEL_BASE_URL:-}" ]]; then missing+=("MODEL_BASE_URL") @@ -48,6 +80,8 @@ if [[ ! -e "${config}" ]]; then exit 2 fi + prepare_state_dirs + if ! out=$(openclaw onboard \ --non-interactive \ --accept-risk \ @@ -75,7 +109,27 @@ if [[ ! -e "${config}" ]]; then exit 1 fi + # Onboarding writes a single model entry with maxTokens 4096 and reasoning + # off. A reasoning model can spend that whole allowance thinking and return + # no answer, so let the environment override those defaults. + model_path="models.providers.openshell-model.models[0]" + for pair in contextWindow:MODEL_CONTEXT_WINDOW maxTokens:MODEL_MAX_TOKENS reasoning:MODEL_REASONING; do + key="${pair%%:*}" + name="${pair#*:}" + if [[ -n "${!name:-}" ]]; then + if ! out=$(openclaw config set "${model_path}.${key}" "${!name}" --strict-json 2>&1); then + printf '%s\n' "${out}" >&2 + echo "openclaw-start: failed to set ${key} from ${name}" >&2 + # Remove the half-configured onboarding so the next start retries it. + rm -f "${config}" "${config}.bak" + exit 1 + fi + fi + done + echo "openclaw-start: wrote ${config} (provider openshell-model, model ${MODEL_ID})" >&2 +else + prepare_state_dirs fi if (($# == 0)); then diff --git a/deploy/konflux/openclaw/profile.sh b/deploy/konflux/openclaw/profile.sh index 363a9a8882..cf21a3d005 100755 --- a/deploy/konflux/openclaw/profile.sh +++ b/deploy/konflux/openclaw/profile.sh @@ -11,6 +11,7 @@ case ":${PATH:-}:" in *) PATH="/usr/local/bin${PATH:+:${PATH}}"; export PATH ;; esac +: "${TMPDIR:=/tmp}"; export TMPDIR : "${OPENCLAW_NO_AUTO_UPDATE:=1}"; export OPENCLAW_NO_AUTO_UPDATE : "${DO_NOT_TRACK:=1}"; export DO_NOT_TRACK : "${OPENCLAW_DISABLE_BONJOUR:=1}"; export OPENCLAW_DISABLE_BONJOUR diff --git a/deploy/konflux/openclaw/smoke-test.sh b/deploy/konflux/openclaw/smoke-test.sh index 45a0bf1412..2f7f439a0e 100755 --- a/deploy/konflux/openclaw/smoke-test.sh +++ b/deploy/konflux/openclaw/smoke-test.sh @@ -18,10 +18,12 @@ set -euo pipefail # # `openclaw-start --version` only proves onboarding plus the launcher fast # path (openclaw.mjs returns before loading config). Agent turns, `tui`, -# `doctor` and `config get/set/validate` are deliberately not run here: EDR -# on maintainers' laptops kills OpenClaw's sqlite workers. Some EDR policies -# also kill any OpenClaw process (exit 137), which fails the version and -# contract-b/c/d checks; run this on a host without such a policy. +# `doctor`, `config get/validate` and direct `config set` are deliberately +# not run here: EDR on maintainers' laptops kills OpenClaw's sqlite workers. +# `config set` runs only through openclaw-start's MODEL_* overrides in the +# model-overrides check. Some EDR policies also kill any OpenClaw process +# (exit 137), which fails the version, contract-b/c/d, setgid-home and +# model-overrides checks; run this on a host without such a policy. if [[ $# -ne 1 ]]; then echo "Usage: smoke-test.sh " >&2 @@ -284,16 +286,91 @@ for sub in a b c d e; do fi done +# --- setgid-home (K8s fsGroup makes the workspace volume setgid) ---------- +# A fresh /sandbox tmpfs per run, so onboarding happens here too. The stale +# agent directory stands in for one an earlier run left behind, at the 2700 +# OpenClaw leaves under a setgid home. +# shellcheck disable=SC2016 # runs inside the container's shell, not the host's +if setgid_out=$(podman_run --user 51234:51234 --passwd=false --read-only \ + --tmpfs /tmp:rw,mode=1777 --tmpfs /sandbox:rw,mode=2777 \ + -e HOME=/sandbox -e MODEL_BASE_URL=http://model.example.svc.cluster.local:8000/v1 \ + -e MODEL_ID=smoke-model -e CUSTOM_API_KEY=smoke-placeholder "${image}" bash -c ' +set -euo pipefail +if [[ "$(stat -c %a /sandbox)" != 2777 ]]; then echo "HOME_NOT_SETGID"; exit 1; fi +mkdir -p /sandbox/.openclaw/agents/main/agent +chmod 2700 /sandbox/.openclaw/agents/main/agent +openclaw-start --version >/tmp/out 2>&1 || { echo "START_FAILED $(tr "\n" " " /tmp/out 2>&1 || { echo "RESTART_FAILED $(tr "\n" " " &1); then + setgid_rc=0 +else + setgid_rc=$? +fi +if [[ ${setgid_rc} -eq 0 && "${setgid_out}" == "OK" ]]; then + pass "setgid-home" +else + fail "setgid-home" "rc=${setgid_rc} out='$(printf '%s' "${setgid_out}" | tr '\n' ' ' | cut -c1-400)'" +fi + +# --- model-overrides (MODEL_* limits land on the onboarded model entry) --- +# shellcheck disable=SC2016 # runs inside the container's shell, not the host's +if overrides_out=$(podman_run --user 51234:51234 --passwd=false --read-only \ + --tmpfs /tmp:rw,mode=1777 --tmpfs /sandbox:rw,mode=1777 \ + -e HOME=/sandbox -e MODEL_BASE_URL=http://model.example.svc.cluster.local:8000/v1 \ + -e MODEL_ID=smoke-model -e CUSTOM_API_KEY=smoke-placeholder "${image}" bash -c ' +set -euo pipefail +rc=0; MODEL_MAX_TOKENS=lots openclaw-start --version >/dev/null 2>/tmp/err || rc=$? +if [[ $rc -ne 2 ]] || ! grep -q MODEL_MAX_TOKENS /tmp/err; then echo "BAD_VALUE rc=$rc"; exit 1; fi +if [[ -e /sandbox/.openclaw/openclaw.json ]]; then echo "ONBOARDED_ON_BAD_VALUE"; exit 1; fi +MODEL_MAX_TOKENS=32768 MODEL_CONTEXT_WINDOW=131072 MODEL_REASONING=true \ + openclaw-start --version >/tmp/out 2>&1 || { echo "START_FAILED $(tr "\n" " " &1); then + overrides_rc=0 +else + overrides_rc=$? +fi +if [[ ${overrides_rc} -eq 0 && "${overrides_out}" == "OK" ]]; then + pass "model-overrides" +else + fail "model-overrides" "rc=${overrides_rc} out='$(printf '%s' "${overrides_out}" | tr '\n' ' ' | cut -c1-400)'" +fi + +# --- tmpdir (SQLite and Node temp files go to /tmp, not /var/tmp) --------- +got_env=$(podman image inspect "${image}" --format '{{range .Config.Env}}{{println .}}{{end}}') +if grep -qx 'TMPDIR=/tmp' <<<"${got_env}"; then + pass "tmpdir" +else + fail "tmpdir" "image env has no TMPDIR=/tmp" +fi + # --- login-shell (K8s exec-style login shell picks up profile.d) --------- # shellcheck disable=SC2016 # runs inside the container's shell, not the host's if login_out=$(podman_run --user 51234:51234 --passwd=false "${image}" \ env -i HOME=/sandbox PATH=/usr/bin:/bin bash -lc \ - 'printf "%s|%s|%s" "$OPENCLAW_NO_AUTO_UPDATE" "$DO_NOT_TRACK" "$(command -v openclaw)"' 2>/dev/null); then + 'printf "%s|%s|%s|%s" "$OPENCLAW_NO_AUTO_UPDATE" "$DO_NOT_TRACK" "$TMPDIR" "$(command -v openclaw)"' 2>/dev/null); then login_rc=0 else login_rc=$? fi -if [[ ${login_rc} -eq 0 && "${login_out}" == "1|1|/usr/local/bin/openclaw" ]]; then +if [[ ${login_rc} -eq 0 && "${login_out}" == "1|1|/tmp|/usr/local/bin/openclaw" ]]; then pass "login-shell" else fail "login-shell" "rc=${login_rc} out='${login_out}'" diff --git a/deploy/man/openshell-gateway.8.md b/deploy/man/openshell-gateway.8.md index 0d217c8a46..2584b9358c 100644 --- a/deploy/man/openshell-gateway.8.md +++ b/deploy/man/openshell-gateway.8.md @@ -93,6 +93,14 @@ TLS. the Kubernetes compute driver. Environment: **OPENSHELL_ENABLE_MTLS_AUTH**. +**--enable-operator-auth** *BOOL* +: Enable direct mTLS operators with a trusted client certificate containing + **OU=operator**. Grants user/platform-admin capabilities across all + workspaces and access to runtime credential retrieval. Defaults off. + Requires TLS and a client CA, independently of ordinary mTLS user auth + and OIDC. Do not combine an operator certificate with bearer headers. + Environment: **OPENSHELL_ENABLE_OPERATOR_AUTH**. + **--disable-tls** : Disable TLS entirely and listen on plaintext HTTP. When the bind address is **0.0.0.0** (the RPM default), disabling TLS exposes the @@ -142,7 +150,9 @@ The Debian and Ubuntu systemd user unit runs preflight before certificate generation, while retaining its EnvironmentFile and bare ExecStart behavior. The Snap wrapper replays its effective daemon arguments through preflight. It first uses a nonempty OPENSHELL_GATEWAY_CONFIG. Otherwise it passes the canonical -SNAP_COMMON/gateway.toml path whenever it exists or is a symlink. A broken symlink +service-specific gateway.toml path whenever it exists or is a symlink. The legacy +system service uses SNAP_COMMON/gateway.toml; the user service uses +SNAP_USER_COMMON/.config/openshell/gateway.toml. A broken symlink fails preflight before the gateway is started. Correct or manually migrate an operator-owned v1 file, then run preflight again before restarting the service. diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index a7ac0c47b1..36b03c943e 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -132,31 +132,104 @@ The snap installs the standalone policy prover as `openshell.prover`. The The prover reads local policy files through the `home` interface and does not connect to the gateway. -The gateway runs as a system service at `https://127.0.0.1:17670` and reads `/var/snap/openshell/common/gateway.toml`. It requires a client certificate. The install script copies that certificate to the installing user's Snap state and registers the gateway automatically. If you installed with `sudo snap install openshell`, give each trusted user the certificate and register the gateway from that user's account: +The gateway runs at `https://127.0.0.1:17670`. It reads `~/snap/openshell/common/.config/openshell/gateway.toml` when that file exists. Its database defaults to `~/snap/openshell/common/.local/state/openshell/gateway/openshell.db`, and its TLS material lives under `~/snap/openshell/common/.local/state/openshell/tls`. Register it from the same user account: ```shell +snap services openshell.user-gateway +openshell gateway add https://127.0.0.1:17670 --local --name openshell +openshell status +``` + +Existing installations upgraded from the former `openshell.gateway` service retain their state under `/var/snap/openshell/common` through `openshell.system-gateway`. Existing HTTPS registrations continue to work because the endpoint and TLS identity do not change. + +If this user still has an old HTTP registration, or has not previously been enrolled, wait for the system gateway to be active, copy its client credentials into the user's Snap state, and register its HTTPS endpoint: + +```shell +snap services openshell.system-gateway d=~/snap/openshell/common/.local/state/openshell/tls mkdir -p -m 700 "$d" "$d/client" -sudo install -o "$USER" -m 600 /var/snap/openshell/common/tls/ca.crt "$d/" -sudo install -o "$USER" -m 600 -t "$d/client" \ - /var/snap/openshell/common/tls/client/tls.crt /var/snap/openshell/common/tls/client/tls.key +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/ca.crt "$d/" +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.crt "$d/client/" +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.key "$d/client/" +openshell gateway list +old_registration="NAME_FROM_GATEWAY_LIST" +openshell gateway remove "$old_registration" openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status ``` -Keep the client key private. +Set `old_registration` to the name shown by `openshell gateway list`, and omit the variable assignment and `openshell gateway remove` when no registration exists. Older direct Snap instructions may have used `openshell-gateway`, while the install script uses `openshell`. -To install a locally built snap, connect its interfaces manually. The gateway may reach systemd's start limit before Docker is connected, so reset the failed unit and restart the gateway after connecting the interfaces: +### Change the Snap gateway mode + +The user and system gateways have separate configuration, databases, and cryptographic identities. Changing modes does not migrate gateway or sandbox state and interrupts active sandbox sessions. Back up any state you need and delete active sandboxes before changing modes. + +First disable both services and move the current user TLS directory to a timestamped backup. The directory can contain CA, server, client, and sandbox JWT private keys, so keep every `tls-backup-*` directory private: + +```shell +sudo snap set openshell gateway-mode=disable +state="$HOME/snap/openshell/common/.local/state/openshell" +tls="$state/tls" +if test -e "$tls" || test -L "$tls"; then + backup="$state/tls-backup-$(date +%s%N)" + while test -e "$backup" || test -L "$backup"; do + backup="$state/tls-backup-$(date +%s%N)" + done + mv -- "$tls" "$backup" + printf 'Previous gateway TLS state saved to: %s\n' "$backup" +fi +``` + +To use the user gateway, start it and replace the previous CLI registration after it generates new credentials: + +```shell +sudo snap set openshell gateway-mode=user +systemctl --user is-active snap.openshell.user-gateway.service +openshell gateway list +old_registration="NAME_FROM_GATEWAY_LIST" +openshell gateway remove "$old_registration" +openshell gateway add https://127.0.0.1:17670 --local --name openshell +openshell status +``` + +To use the system gateway, start it, wait for it to generate its root-owned credentials, then copy its client bundle and replace the previous CLI registration: + +```shell +sudo snap set openshell gateway-mode=system +sudo snap services openshell.system-gateway +tls="$HOME/snap/openshell/common/.local/state/openshell/tls" +mkdir -p -m 700 "$tls" "$tls/client" +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/ca.crt "$tls/" +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.crt "$tls/client/" +sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.key "$tls/client/" +openshell gateway list +old_registration="NAME_FROM_GATEWAY_LIST" +openshell gateway remove "$old_registration" +openshell gateway add https://127.0.0.1:17670 --local --name openshell +openshell status +``` + +Set `old_registration` to the name shown by `openshell gateway list`, and omit the variable assignment and remove command when no registration exists. If a transition is interrupted while both services are disabled, select `gateway-mode=user` or `gateway-mode=system` to resume it. Keep the TLS backup until the selected gateway works and can create a sandbox. + +To install a locally built snap, connect its interfaces manually. The user gateway may reach systemd's start limit before Docker is connected, so reset the failed unit and restart it from the affected user session after connecting the interfaces: ```shell sudo snap install ./openshell_*.snap --dangerous sudo snap connect openshell:log-observe sudo snap connect openshell:system-observe sudo snap connect openshell:docker :docker -sudo systemctl reset-failed snap.openshell.gateway.service -sudo snap restart openshell.gateway +systemctl --user reset-failed snap.openshell.user-gateway.service +snap start --user openshell.user-gateway ``` +For an installation upgraded from a legacy system gateway, use `sudo systemctl reset-failed snap.openshell.system-gateway.service` and `sudo snap restart openshell.system-gateway` instead. + ## Kubernetes Deploy the gateway to a cluster with the OpenShell Helm chart. See [Kubernetes Setup](/kubernetes/setup). diff --git a/docs/how-it-works/gateways/authentication.mdx b/docs/how-it-works/gateways/authentication.mdx index 2304cb7b9c..6b05e42be9 100644 --- a/docs/how-it-works/gateways/authentication.mdx +++ b/docs/how-it-works/gateways/authentication.mdx @@ -69,9 +69,35 @@ The connection flow: 5. When mTLS user authentication is enabled, the gateway maps the verified certificate subject to a user principal. 6. The gateway authorizes the gRPC method. +### mTLS Operators + +Enable `[openshell.gateway.mtls_auth] operator_enabled = true` to recognize a +trusted client certificate whose organizational unit is exactly `operator` +(`OU=operator`). This creates a distinct operator principal with user and +platform-admin capabilities across all workspaces, plus permission to call +`GetProviderCredentials`. Ordinary mTLS users and OIDC admins do not have that +permission, even with an `operator` claim or `openshell:all`. + +Operator authentication requires TLS and a configured client CA. It works +alongside OIDC and does not require ordinary mTLS user authentication. Connect +directly to the gateway with the certificate and its private key; do not send an +`Authorization` header on the same connection. Forwarded certificate headers +and edge-tunnel authentication cannot establish an operator identity. + +Anyone who can issue certificates under the trusted client CA can issue an +operator certificate. Restrict CA issuance and operator private-key access. +Normal gateway certificate generation does not grant the operator role. +Certificate validity and CA changes apply to new TLS handshakes, not requests on +already-established connections; close existing connections when removing +trust. The gateway does not provide per-certificate revocation through this +setting. + +See [Providers](/how-it-works/providers/overview#retrieve-runtime-credentials) +for the credential retrieval contract. + ### OIDC -Gateways can validate OpenID Connect access tokens on gRPC requests. Configure OIDC when you want users, operators, or automation to authenticate with an identity provider such as Keycloak, Entra ID, or Okta. +Gateways can validate OpenID Connect access tokens on gRPC requests. Configure OIDC when you want users or automation to authenticate with an identity provider such as Keycloak, Entra ID, or Okta. OIDC is application-layer authentication. TLS authenticates the gateway and protects the transport; OIDC clients do not need a TLS client certificate. The gateway validates any client certificate they present against the configured client CA. diff --git a/docs/how-it-works/gateways/configuration.mdx b/docs/how-it-works/gateways/configuration.mdx index f7f1df8b5c..21eda0a59d 100644 --- a/docs/how-it-works/gateways/configuration.mdx +++ b/docs/how-it-works/gateways/configuration.mdx @@ -65,7 +65,7 @@ Package-managed gateways use either built-in defaults or a package-seeded TOML f | Homebrew | `$XDG_CONFIG_HOME/openshell/gateway.toml` when it exists, otherwise the Homebrew prefix config such as `/opt/homebrew/var/openshell/gateway.toml`. | | Debian/Ubuntu | `$XDG_CONFIG_HOME/openshell/gateway.toml`, usually `~/.config/openshell/gateway.toml` for the systemd user service. | | Fedora/RHEL RPM | `$XDG_CONFIG_HOME/openshell/gateway.toml`, usually `~/.config/openshell/gateway.toml`; the systemd user service seeds this file from the packaged template on first start. | -| Snap | `$SNAP_COMMON/gateway.toml`, usually `/var/snap/openshell/common/gateway.toml`. | +| Snap | New installations use `$SNAP_USER_COMMON/.config/openshell/gateway.toml`, usually `~/snap/openshell/common/.config/openshell/gateway.toml`. Upgraded legacy installations retain `$SNAP_COMMON/gateway.toml`, usually `/var/snap/openshell/common/gateway.toml`. | The Fedora/RHEL RPM template leaves `[openshell.gateway].bind_address` unset. The gateway therefore uses its built-in `127.0.0.1:17670` primary listener. Host-networked Podman supervisors use that same loopback endpoint, so the primary listener does not need a wildcard address. Set `bind_address` explicitly only when clients must reach the primary multiplexed API through another interface. @@ -245,6 +245,7 @@ allow_unauthenticated_users = false [openshell.gateway.mtls_auth] enabled = false +operator_enabled = false # Opt-in: trusted OU=operator grants gateway-wide administration. # OTLP export. Omit this table entirely to disable it. [openshell.gateway.otlp] @@ -301,6 +302,16 @@ namespace = "openshell" Set `[openshell.gateway.mtls_auth] enabled = true` to map a verified client certificate to a CLI user identity. This gateway policy is independent of the compute driver and defaults on when a client CA is configured without OIDC. The listener allows bearer-only clients and validates any client certificate presented against the configured CA. Supervisors use `guest_tls_ca` to authenticate the gateway and sandbox bearer tokens to authenticate their RPCs. +Set `[openshell.gateway.mtls_auth] operator_enabled = true`, +`--enable-operator-auth true`, or `OPENSHELL_ENABLE_OPERATOR_AUTH=true` to enable +direct mTLS operators. The default is false. TLS and a client CA are required. +This setting is independent of `enabled` and OIDC. A verified certificate with +the exact `OU=operator` grants user/platform-admin capabilities across all +workspaces and access to `GetProviderCredentials`. Do not combine an operator +certificate with an `Authorization` header. See +[Authentication](/how-it-works/gateways/authentication#mtls-operators) for the +certificate trust boundary. + The client-certificate handshake policy has no `require_client_auth` TOML field. Client certificates are optional at the transport layer; gateway RPC authorization enforces the configured user or sandbox identity. `[openshell.gateway.tls]` supports optional SNI-based dual-certificate mode for deployments that need separate internal and external server certificates. Set `external_cert_path` and `external_key_path` to point at the external (e.g. ACME/publicly-trusted) certificate and key. List the hostnames that should be served with the external certificate in `external_server_names`. Connections whose TLS SNI hostname matches one of those names receive the external certificate; all other connections (including those with no SNI) receive the primary internal certificate from `cert_path`/`key_path`. Both fields must be set together — providing only one is a configuration error. On Kubernetes with the Helm chart, the external certificate is managed automatically when `certManager.serverIssuerRef.name` is set; the chart populates these fields from the cert-manager-issued external server certificate. @@ -1325,8 +1336,10 @@ Debian and Ubuntu run preflight from the systemd user unit before local certific generation. The unit still loads the `gateway.env` environment file and starts the gateway with no configuration arguments. Snap replays the exact effective daemon arguments through preflight. It gives a nonempty `OPENSHELL_GATEWAY_CONFIG` -precedence; otherwise it validates and passes its canonical -`SNAP_COMMON/gateway.toml` only when that path exists in the filesystem. A broken +precedence; otherwise it validates and passes its canonical service-specific +path only when that path exists in the filesystem. The legacy system service +uses `$SNAP_COMMON/gateway.toml`; the user service uses +`$SNAP_USER_COMMON/.config/openshell/gateway.toml`. A broken symlink is therefore rejected instead of being treated as absent. Package startup does not modify an operator-owned v1 file. Back it up, follow diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index acde16f577..85a6096f6a 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -237,3 +237,7 @@ and port that has both a REST endpoint and an endpoint without request rules also returns `unsupported`, as do REST rules that match query parameters or use `?` or bracket expressions in paths. Other request protocols, such as WebSocket, GraphQL, MCP, and JSON-RPC, return `unsupported`. + +The check also applies each endpoint's `path` selector to its allow and deny +rules, following the [endpoint matcher semantics](/how-it-works/policies/schema#matcher-semantics). +Endpoint selectors and REST rule paths use different wildcard behavior. diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index 908adf64d8..fb05acbe82 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -141,6 +141,45 @@ does not activate static credentials from a profileless provider because it cannot determine which credential definition applies. +## Retrieve Runtime Credentials + +An [mTLS operator](/how-it-works/gateways/authentication#mtls-operators) can call +`openshell.v1.OpenShell/GetProviderCredentials` through the +[Python SDK](/sdk/python#retrieve-provider-credentials), +[TypeScript SDK](/sdk/typescript#retrieve-provider-credentials), or a generated/raw +gRPC client. Select one explicit workspace, one provider by canonical name, and +between 1 and 32 runtime environment keys. For example, the request body for a +GitHub provider is: + +```json +{ + "workspace_scope": { "workspace": "default" }, + "name": "my-github", + "credential_keys": ["GITHUB_TOKEN"] +} +``` + +The response maps each selected key to its `value` and optional +`expiration_time`. The gateway returns all selected values or an error. It +reuses sufficiently valid credentials and refreshes gateway-managed credentials +when necessary. `minimum_remaining_lifetime` accepts a protobuf duration up to +24 hours; omission or zero uses a five-minute safety margin. Non-expiring static +credentials satisfy the lifetime requirement. A credential that cannot meet the +requested lifetime causes the call to fail. + +OAuth refresh-managed credentials return access tokens, never gateway-only +refresh tokens, client secrets, or signing material. Supervisor-side, +endpoint-specific, and SPIFFE-bound dynamic grants return `FAILED_PRECONDITION`. +Refresh effects can persist even if another selected key causes the request to +fail. An uncertain rotating-token exchange requires reauthorization or refresh +reconfiguration rather than an automatic retry. + + +Retrieval exports real credential values outside sandbox endpoint enforcement. +Keep them out of logs, command history, and shared output. Ordinary users and +OIDC admins cannot call this RPC. No dedicated CLI command is provided. + + ## Manage Providers List, inspect, update, and delete providers from the active gateway. diff --git a/docs/how-it-works/providers/profiles.mdx b/docs/how-it-works/providers/profiles.mdx index cbfbd476e5..da2eee03d6 100644 --- a/docs/how-it-works/providers/profiles.mdx +++ b/docs/how-it-works/providers/profiles.mdx @@ -762,13 +762,22 @@ bodies. The recovery actions are: - `retry`: a network, rate-limit, or temporary issuer failure; the worker retries automatically. - `reauthorize`: a user refresh grant is no longer usable, such as `invalid_grant`; obtain a new grant and run `provider refresh configure`. - `fix_configuration`: the OAuth client, scopes, grant type, or administrator policy must be corrected. -- `investigate`: the issuer returned an unrecognized response; the worker retries, but the operator should investigate persistent failures. +- `investigate`: an unrecognized issuer response or an interrupted mint requires investigation. The worker retries unrecognized issuer responses, but it does not retry an interrupted mint with an uncertain outcome. The gateway parks `reauthorize` and `fix_configuration` failures instead of retrying them every worker tick. A manual `provider refresh rotate` still attempts the exchange. The current access token remains available only until its recorded expiry, after which credential resolution fails closed. +The `refresh_in_progress` and `refresh_committing` statuses can indicate active +work. Check gateway health and status again; concurrent refresh requests wait +for the active mint. If a gateway interruption leaves either marker behind and +rotation reports an uncertain previous mint, investigate the issuer state and +run `provider refresh configure` with a valid grant. Obtain a new OAuth grant +first if the previous refresh token may have been consumed. Repeated rotation +or direct marker edits cannot recover it. Reconfiguration revokes previous +workload handles, so restart processes holding them. + Before OpenShell 0.1.0, refresh-state and credential-driver migrations are not supported. Upgrading from a build that stored refresh material inline requires reconfiguring the refresh grant. To change credential drivers, delete or diff --git a/docs/kubernetes/ingress.mdx b/docs/kubernetes/ingress.mdx index 56ae056889..69b18ea3f2 100644 --- a/docs/kubernetes/ingress.mdx +++ b/docs/kubernetes/ingress.mdx @@ -10,7 +10,7 @@ position: 5 By default, the OpenShell gateway is only reachable inside the cluster. To let CLI clients connect without a `kubectl port-forward`, expose the gateway through an ingress. -OpenShell uses the [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io) for ingress. The chart creates a `GRPCRoute` that routes inbound gRPC traffic to the gateway pod. You need a Gateway API implementation installed on your cluster to fulfill the `GRPCRoute`. This page uses [Envoy Gateway](https://gateway.envoyproxy.io), which the chart is tested with. +OpenShell uses the [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io) for ingress. The chart creates a `GRPCRoute` that routes inbound gRPC traffic to the gateway pod. You need a Gateway API implementation installed on your cluster to fulfill the `GRPCRoute`. The examples use [Envoy Gateway](https://gateway.envoyproxy.io), but you can select another implementation through `grpcRoute.gateway.className`. ## Install Envoy Gateway @@ -62,14 +62,53 @@ helm upgrade --install openshell \ --set grpcRoute.gateway.className=eg ``` +The generated Gateway defaults to the chart fullname. + +### Use another GatewayClass + +Set `grpcRoute.gateway.className` to any GatewayClass installed by your Gateway API implementation: + +```shell +helm upgrade --install openshell \ + oci://ghcr.io/nvidia/openshell/helm-chart \ + --version \ + --namespace openshell \ + --set grpcRoute.enabled=true \ + --set grpcRoute.gateway.create=true \ + --set grpcRoute.gateway.className= \ + --set grpcRoute.gateway.name=openshell-ingress +``` + +Some Gateway API implementations name their proxy Service after the Gateway. Set a distinct Gateway name, such as `openshell-ingress`, when the default would conflict with the OpenShell Service. For ingress, the OpenShell chart creates only the Gateway and GRPCRoute. Install and manage the selected controller, GatewayClass, and Gateway API CRDs separately. + +### Use an existing Gateway + +To attach OpenShell to a platform-managed Gateway, disable Gateway creation and set the parent Gateway name and namespace. Set `sectionName` when the route should attach to a specific listener: + +```shell +helm upgrade --install openshell \ + oci://ghcr.io/nvidia/openshell/helm-chart \ + --version \ + --namespace openshell \ + --set grpcRoute.enabled=true \ + --set grpcRoute.gateway.create=false \ + --set grpcRoute.gateway.name=platform-gateway \ + --set grpcRoute.gateway.namespace=gateway-system \ + --set grpcRoute.gateway.sectionName=grpc +``` + +The existing Gateway listener must allow routes from the OpenShell release namespace. + ## Get the external address -After the Gateway is provisioned, Envoy Gateway creates a LoadBalancer service in the `openshell` namespace. Wait for it to get an external address: +With the Envoy Gateway example above, the controller creates a LoadBalancer service in the `openshell` namespace. Wait for it to get an external address: ```shell kubectl -n openshell get svc -l gateway.envoyproxy.io/owning-gateway-name=openshell ``` +For another Gateway API implementation or an existing Gateway, follow that implementation's instructions to find the external address. + After the `EXTERNAL-IP` is assigned, register the gateway with the CLI: ```shell diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index afd1a9d399..64b1d9cb54 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -68,6 +68,47 @@ with SandboxClient.from_active_cluster(client_credentials=auth) as client: print(client.health().version) ``` +## Retrieve Provider Credentials + +Connect directly with a trusted `OU=operator` client certificate after enabling +[operator authentication](/how-it-works/gateways/authentication#mtls-operators). +Do not supply a bearer token or use an OIDC login for this connection. No sandbox +creation or provider attachment is required. + +```python +from datetime import timedelta +from pathlib import Path + +from openshell import SandboxClient, TlsConfig + +tls = TlsConfig( + ca_path=Path("ca.crt"), + cert_path=Path("operator.crt"), + key_path=Path("operator.key"), +) +with SandboxClient("gateway.example.com:443", tls=tls) as client: + credentials = client.providers().get_credentials( + "my-graph", + ["MS_GRAPH_ACCESS_TOKEN"], + workspace="production", + minimum_remaining_lifetime=timedelta(minutes=5), + ) + token = credentials["MS_GRAPH_ACCESS_TOKEN"].value +``` + +`ProviderClient` also accepts an existing `grpc.Channel`. Its +`get_credentials()` method requires an explicit workspace and key selection. +It returns a dictionary of `ProviderCredentialValue` objects with `value` and +optional `expiration_time`, a UTC `datetime`. Pass a `timedelta` for +`minimum_remaining_lifetime`; omission or zero uses the gateway default. +The helper uses the connection's timeout and does not retry or cache exports. +Gateway failures raise `GatewayError`; invalid local selections raise `ValueError`. + +The value is excluded from the object's `repr`, but serialization still exposes +it. Keep exported secrets out of logs and shared output. See +[Retrieve Runtime Credentials](/how-it-works/providers/overview#retrieve-runtime-credentials) +for freshness, supported credentials, and the export security boundary. + ## Create and Use a Sandbox Python SDK methods take an explicit workspace. This example creates a sandbox, diff --git a/docs/sdk/typescript.mdx b/docs/sdk/typescript.mdx index 96a093fb3f..312bc32a46 100644 --- a/docs/sdk/typescript.mdx +++ b/docs/sdk/typescript.mdx @@ -65,6 +65,44 @@ const client = await OpenShellClient.connect({ The provider retains credentials and tokens in memory and renews the access token before expiry. +## Retrieve Provider Credentials + +Connect directly with a trusted `OU=operator` client certificate after enabling +[operator authentication](/how-it-works/gateways/authentication#mtls-operators). +Do not supply an OIDC or edge token for this connection. No sandbox creation or +provider attachment is required. + +```ts +import { readFileSync } from 'node:fs' +import { OpenShellClient } from '@nvidia/openshell-sdk' + +const operator = await OpenShellClient.connect({ + gateway: 'https://gateway.example.com', + caCert: readFileSync('ca.crt'), + clientCert: readFileSync('operator.crt'), + clientKey: readFileSync('operator.key'), +}) +const credentials = await operator.providers.getCredentials( + 'my-graph', + ['MS_GRAPH_ACCESS_TOKEN'], + { workspace: 'production', minimumRemainingLifetimeSecs: 300 }, +) +const token = credentials.MS_GRAPH_ACCESS_TOKEN!.value +``` + +`ProviderClient.connect()` provides the same API as a standalone client. Unlike +sandbox operations, `getCredentials()` requires an explicit workspace. It returns +a record of `ProviderCredentialValue` objects with `value` and optional +`expirationTime`, a `Date`. Sub-millisecond expiration precision is truncated. +`minimumRemainingLifetimeSecs` accepts fractional seconds; omission or zero uses +the gateway default. Pass `signal` or `timeoutMs` in the operation options to +bound delivery. The helper does not retry or cache exports, and canceled delivery +does not roll back a gateway refresh that has already started. + +Keep returned values out of logs and shared output. See +[Retrieve Runtime Credentials](/how-it-works/providers/overview#retrieve-runtime-credentials) +for freshness, supported credentials, and the export security boundary. + ## Create and Use a Sandbox The curated client uses the `default` workspace unless you pass `workspace` in diff --git a/e2e/rust/Cargo.lock b/e2e/rust/Cargo.lock index 018ae54912..b5403e2ac3 100644 --- a/e2e/rust/Cargo.lock +++ b/e2e/rust/Cargo.lock @@ -831,16 +831,15 @@ dependencies = [ [[package]] name = "noyalib" -version = "0.0.28" +version = "0.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" dependencies = [ "hashbrown", "indexmap", "libm", "memchr", "rustc-hash", - "serde", "serde_core", "smallvec", ] diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 284114a99d..77e85d214f 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -266,7 +266,7 @@ rustls = { version = "0.23", default-features = false, features = ["std", "loggi rustls-pemfile = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -serde_yml = { package = "noyalib", version = "0.0.28", default-features = false, features = ["std", "compat-serde-yaml"] } +serde_yml = { package = "noyalib", version = "0.0.53", default-features = false, features = ["std", "compat-serde-yaml"] } tonic = { version = "0.14", features = ["transport"] } tonic-prost = "0.14" tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "aws_lc_rs"] } diff --git a/e2e/rust/tests/transparent_tcp.rs b/e2e/rust/tests/transparent_tcp.rs index 5a7e95db7e..3c9192d31c 100644 --- a/e2e/rust/tests/transparent_tcp.rs +++ b/e2e/rust/tests/transparent_tcp.rs @@ -17,6 +17,7 @@ use tempfile::NamedTempFile; // Use a qualified policy hostname so runtime-provided resolver search domains // (for example Podman's `dns.podman`) cannot rewrite the policy identity. const FIXTURE_ALIAS: &str = "transparent-tcp-fixture.openshell.test"; +const UNLISTED_ALIAS: &str = "unlisted-exec-recovery.openshell.test"; const MUSL_FIXTURE_ALIAS: &str = "transparent-tcp-musl.openshell.test"; const FIXTURE_PORT: u16 = 5432; const TCP_DNS_PORT: u16 = 53; @@ -378,6 +379,7 @@ def denied(host, port): assert denied({host:?}, {wrong_port}) assert denied({real_ip:?}, {port}) assert denied({real_ip:?}, {transparent_port}) +assert denied({unlisted_host:?}, {port}) print('transparent-tcp-e2e-ok') "#, host = policy_host, @@ -386,6 +388,7 @@ print('transparent-tcp-e2e-ok') wrong_port = wrong_port, real_ip = real_ip, transparent_port = transparent_port, + unlisted_host = UNLISTED_ALIAS, ); let output = match sandbox.exec(&["python3", "-c", &script]).await { Ok(output) => output, @@ -407,9 +410,17 @@ print('transparent-tcp-e2e-ok') }; assert!(output.contains("transparent-tcp-e2e-ok"), "{output}"); + let recovery = sandbox + .exec(&["sh", "-c", "printf post-denial-exec-ok"]) + .await + .expect("fresh exec should succeed after an unlisted-host denial"); + assert_eq!(recovery, "post-denial-exec-ok"); + let logs = wait_for_sandbox_logs(&sandbox.name, |logs| { logs.contains(&format!("-> {policy_host}:{fixture_port}")) && logs.contains("Denied staged transparent connection") + && logs.contains(UNLISTED_ALIAS) + && logs.contains("policy_dns_ineligible") }) .await .expect("wait for sandbox logs"); @@ -421,6 +432,8 @@ print('transparent-tcp-e2e-ok') logs.contains("Denied staged transparent connection"), "{logs}" ); + assert!(logs.contains(UNLISTED_ALIAS), "{logs}"); + assert!(logs.contains("policy_dns_ineligible"), "{logs}"); sandbox.cleanup().await; } diff --git a/e2e/with-kube-gateway.sh b/e2e/with-kube-gateway.sh index c7c832b1c9..ee755b8760 100755 --- a/e2e/with-kube-gateway.sh +++ b/e2e/with-kube-gateway.sh @@ -265,18 +265,29 @@ install_envoy_gateway() { } wait_for_envoy_service() { + local gateway_name="" local svc_ref="" local svc_namespace="" + gateway_name="$(kctl -n "${NAMESPACE}" get grpcroute \ + -l "app.kubernetes.io/instance=${RELEASE_NAME}" \ + -o jsonpath='{.items[0].spec.parentRefs[0].name}' \ + 2>/dev/null || true)" + if [ -z "${gateway_name}" ]; then + echo "ERROR: could not determine the Gateway referenced by the OpenShell GRPCRoute." >&2 + kctl -n "${NAMESPACE}" get gateway,grpcroute -o wide >&2 || true + return 1 + fi + for _ in $(seq 1 60); do svc_ref="$(kctl get svc -A \ - -l "gateway.envoyproxy.io/owning-gateway-name=${RELEASE_NAME},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ + -l "gateway.envoyproxy.io/owning-gateway-name=${gateway_name},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ -o jsonpath='{range .items[0]}{.metadata.namespace}{"/"}{.metadata.name}{end}' \ 2>/dev/null || true)" if [ -n "${svc_ref}" ]; then svc_namespace="${svc_ref%%/*}" if kctl -n "${svc_namespace}" wait --for=condition=Ready pod \ - -l "gateway.envoyproxy.io/owning-gateway-name=${RELEASE_NAME},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ + -l "gateway.envoyproxy.io/owning-gateway-name=${gateway_name},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ --timeout=5s >/dev/null 2>&1; then printf '%s\n' "${svc_ref}" return 0 @@ -285,13 +296,13 @@ wait_for_envoy_service() { sleep 2 done - echo "ERROR: Envoy proxy Service for Gateway ${RELEASE_NAME} was not ready." >&2 + echo "ERROR: Envoy proxy Service for Gateway ${gateway_name} was not ready." >&2 kctl -n "${NAMESPACE}" get gateway,grpcroute -o wide >&2 || true kctl get svc -A \ - -l "gateway.envoyproxy.io/owning-gateway-name=${RELEASE_NAME},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ + -l "gateway.envoyproxy.io/owning-gateway-name=${gateway_name},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ -o wide >&2 || true kctl get pods -A \ - -l "gateway.envoyproxy.io/owning-gateway-name=${RELEASE_NAME},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ + -l "gateway.envoyproxy.io/owning-gateway-name=${gateway_name},gateway.envoyproxy.io/owning-gateway-namespace=${NAMESPACE}" \ -o wide >&2 || true return 1 } diff --git a/examples/governance-interceptor/Cargo.lock b/examples/governance-interceptor/Cargo.lock index f116601386..a673129a47 100644 --- a/examples/governance-interceptor/Cargo.lock +++ b/examples/governance-interceptor/Cargo.lock @@ -989,16 +989,15 @@ dependencies = [ [[package]] name = "noyalib" -version = "0.0.28" +version = "0.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" dependencies = [ "hashbrown 0.17.1", "indexmap", "libm", "memchr", "rustc-hash", - "serde", "serde_core", "smallvec", ] diff --git a/examples/governance-interceptor/Cargo.toml b/examples/governance-interceptor/Cargo.toml index 890ef67dc7..23dbd5037b 100644 --- a/examples/governance-interceptor/Cargo.toml +++ b/examples/governance-interceptor/Cargo.toml @@ -21,7 +21,7 @@ prost-types = "0.14" rcgen = { version = "0.13", default-features = false, features = ["crypto", "pem", "aws_lc_rs"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -serde_yml = { package = "noyalib", version = "0.0.28", default-features = false, features = ["std", "compat-serde-yaml"] } +serde_yml = { package = "noyalib", version = "0.0.53", default-features = false, features = ["std", "compat-serde-yaml"] } sha2 = "0.10" tokio = { version = "1.43", features = ["macros", "rt-multi-thread", "fs", "signal"] } tonic = { version = "0.14", features = ["transport"] } diff --git a/examples/supervisor-middleware-content-guard/Cargo.lock b/examples/supervisor-middleware-content-guard/Cargo.lock index 07b3e92e7b..c90a6dd3dc 100644 --- a/examples/supervisor-middleware-content-guard/Cargo.lock +++ b/examples/supervisor-middleware-content-guard/Cargo.lock @@ -1106,16 +1106,15 @@ dependencies = [ [[package]] name = "noyalib" -version = "0.0.28" +version = "0.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" dependencies = [ "hashbrown 0.17.1", "indexmap", "libm", "memchr", "rustc-hash", - "serde", "serde_core", "smallvec", ] diff --git a/install.sh b/install.sh index 7cc8637f6e..d58dd81a9e 100755 --- a/install.sh +++ b/install.sh @@ -981,13 +981,18 @@ dump_snap_gateway_diagnostics() { info "OpenShell snap service status:" as_root snap services openshell >&2 || true + info "OpenShell snap user service status for ${TARGET_USER}:" + as_target_user snap services openshell.user-gateway >&2 || true info "OpenShell snap connections:" - as_root snap connections openshell >&2 || true + snap connections openshell >&2 || true if has_cmd journalctl; then - info "last ${_lines} lines from the OpenShell snap gateway journal:" - as_root journalctl -b -u snap.openshell.gateway.service --no-pager -n "$_lines" >&2 || true + info "last ${_lines} lines from the legacy OpenShell snap gateway journal:" + journalctl -b -u snap.openshell.system-gateway.service --no-pager -n "$_lines" >&2 || true + info "last ${_lines} lines from the OpenShell snap user gateway journal:" + as_target_user journalctl --user -u snap.openshell.user-gateway --no-pager -n "$_lines" >&2 || true fi - as_root snap logs openshell.gateway -n="$_lines" >&2 || true + as_root snap logs openshell.system-gateway -n="$_lines" >&2 || true + as_target_user snap logs openshell.user-gateway -n="$_lines" >&2 || true } dump_homebrew_gateway_diagnostics() { @@ -1259,10 +1264,10 @@ wait_for_docker_daemon() { _elapsed=0 _last_output="" - info "waiting for Docker daemon to become reachable..." + info "waiting for Docker daemon at /var/run/docker.sock to become reachable..." while [ "$_elapsed" -lt "$_timeout" ]; do - if _last_output="$(as_root docker info 2>&1)"; then - info "Docker daemon is reachable" + if _last_output="$(as_root env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info 2>&1)"; then + info "Docker daemon at /var/run/docker.sock is reachable" return 0 fi sleep 1 @@ -1271,10 +1276,39 @@ wait_for_docker_daemon() { [ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2 if snap list docker >/dev/null 2>&1; then - as_root snap services docker >&2 || true - as_root snap changes >&2 || true + snap services docker >&2 || true + snap changes >&2 || true fi - error "Docker daemon did not become reachable within ${_timeout}s" + error "Docker daemon at /var/run/docker.sock did not become reachable within ${_timeout}s" +} + +wait_for_user_docker_daemon() { + _timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}" + _elapsed=0 + _last_output="" + + info "waiting for Docker daemon at /var/run/docker.sock to become reachable as ${TARGET_USER}..." + while [ "$_elapsed" -lt "$_timeout" ]; do + if _last_output="$(as_target_user env -u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info 2>&1)"; then + info "Docker daemon at /var/run/docker.sock is reachable as ${TARGET_USER}" + return 0 + fi + sleep 1 + _elapsed=$((_elapsed + 1)) + done + + [ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2 + error "Docker daemon at /var/run/docker.sock did not become reachable as ${TARGET_USER} within ${_timeout}s. Ensure ${TARGET_USER} can access the Docker socket, then log out and back in after changing group membership." +} + +snap_gateway_mode() { + _mode="$(as_root snap get openshell gateway-mode 2>/dev/null || true)" + case "$_mode" in + user | system) printf '%s\n' "$_mode" ;; + disable) error "OpenShell snap gateway mode is disabled. Complete the gateway transition with 'sudo snap set openshell gateway-mode=user' or 'sudo snap set openshell gateway-mode=system', then rerun the installer." ;; + '') error "OpenShell snap gateway mode was not initialized" ;; + *) error "unsupported OpenShell snap gateway mode: ${_mode}" ;; + esac } # Copy the snap gateway's client bundle into the target user's snap state @@ -1299,7 +1333,7 @@ snap_gateway_uses_mtls() { [ -e "${OPENSHELL_SNAP_DIR:-/snap/openshell/current}/meta/hooks/post-refresh" ] } -register_snap_gateway() { +register_system_snap_gateway() { _register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}" if snap_gateway_uses_mtls; then @@ -1331,9 +1365,33 @@ register_snap_gateway() { esac } +register_user_snap_gateway() { + _register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}" + _endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}" + + if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then + [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" + return 0 + else + _add_status=$? + fi + + case "$_add_output" in + *"already exists"*) + info "local gateway already exists; removing and re-adding it..." + remove_snap_gateway_registration + as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell + ;; + *) + printf '%s\n' "$_add_output" >&2 + return "$_add_status" + ;; + esac +} + # The mTLS gateway rejects TLS handshakes without a client certificate, so # probe it with the root-owned client bundle. -wait_for_snap_gateway_listener() { +wait_for_system_snap_gateway_listener() { _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" @@ -1365,10 +1423,48 @@ wait_for_snap_gateway_listener() { error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" } +wait_for_user_snap_gateway_listener() { + _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" + _elapsed=0 + _last_output="" + _tls_dir="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls" + _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + + info "waiting for local gateway listener to become reachable..." + while [ "$_elapsed" -lt "$_timeout" ]; do + if _last_output="$(as_target_user curl -sS --max-time 2 \ + --cacert "${_tls_dir}/ca.crt" \ + --cert "${_tls_dir}/client/tls.crt" \ + --key "${_tls_dir}/client/tls.key" \ + -o /dev/null "$_probe_url" 2>&1)"; then + info "local gateway listener is reachable" + return 0 + fi + sleep 1 + _elapsed=$((_elapsed + 1)) + done + + [ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2 + dump_local_gateway_diagnostics + error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" +} + install_linux_snap() { require_cmd snap set_linux_target_runtime_dir + if snap list openshell >/dev/null 2>&1; then + _existing_snap=true + _existing_mode="$(as_root snap get openshell gateway-mode 2>/dev/null || true)" + else + _existing_snap=false + _existing_mode=user + fi + + if [ "$_existing_mode" = disable ]; then + error "OpenShell snap gateway mode is disabled. Complete the gateway transition with 'sudo snap set openshell gateway-mode=user' or 'sudo snap set openshell gateway-mode=system', then rerun the installer." + fi + if snap list docker >/dev/null 2>&1; then error "the Docker snap is not currently compatible with OpenShell because its AppArmor confinement prevents OpenShell's hardened containers from starting. Remove the Docker snap and install Docker Engine from a system package or Docker's package repository, then rerun this installer." @@ -1378,24 +1474,41 @@ Remove the Docker snap and install Docker Engine from a system package or Docker Install Docker Engine from a system package or Docker's package repository, then rerun this installer. The Docker snap is not currently compatible with OpenShell." fi info "using existing Docker installation" - wait_for_docker_daemon + if [ "$_existing_mode" = user ]; then + wait_for_user_docker_daemon + else + wait_for_docker_daemon + fi _channel="$(openshell_snap_channel)" - if snap list openshell >/dev/null 2>&1; then + if [ "$_existing_snap" = true ]; then info "refreshing OpenShell snap from ${_channel}..." as_root snap refresh openshell --channel="$_channel" - warn "restarting the OpenShell gateway to use the refreshed snap; active sandbox sessions will be interrupted" else info "installing OpenShell snap from ${_channel}..." as_root snap install openshell --channel="$_channel" fi - as_root snap restart openshell.gateway + _gateway_mode="$(snap_gateway_mode)" + if [ "$_gateway_mode" = system ]; then + warn "restarting the OpenShell gateway to use the refreshed snap; active sandbox sessions will be interrupted" + as_root snap restart openshell.system-gateway + elif [ "$_existing_snap" = true ]; then + warn "restarting the OpenShell user gateway to use the refreshed snap; active sandbox sessions will be interrupted" + # as root, snap restart automatically restarts the given user services for + # all active users (there should just be one), and is forwards-compatible + # with planned changes to the snap user daemon commands. + as_root snap restart openshell.user-gateway + fi - info "installed OpenShell snap from ${_channel}" - wait_for_snap_gateway_listener info "registering local gateway as ${TARGET_USER}..." - register_snap_gateway + if [ "$_gateway_mode" = system ]; then + wait_for_system_snap_gateway_listener + register_system_snap_gateway + else + wait_for_user_snap_gateway_listener + register_user_snap_gateway + fi OPENSHELL_REGISTER_BIN="/snap/bin/openshell" wait_for_local_gateway_status } diff --git a/nix/test-guest/README.md b/nix/test-guest/README.md index 9a9378abee..b5340b37f2 100644 --- a/nix/test-guest/README.md +++ b/nix/test-guest/README.md @@ -296,44 +296,6 @@ nix run .#test-guest -- \ -- openshell --version ``` -## Reproduce Snap installation - -Copy the repository installer and reproduction script into a prepared Ubuntu -guest. The script follows both Release Canary Snap flows: it runs `install.sh` -with `OPENSHELL_VERSION=dev`, checks the `latest/edge` channel and Docker -interface, and exercises a sandbox. Repeated attempts also cover the installer's -idempotent Snap refresh path. On each failure the script prints snapd, Docker, -and gateway diagnostics. - -Reuse system Docker and verify that the Docker snap is not installed: - -```shell -nix run .#test-guest -- \ - --distro ubuntu-24-04 \ - --with docker \ - --with snapd \ - --keep \ - --copy ./install.sh:/tmp/install.sh \ - --copy ./nix/test-guest/scripts/snap-gateway-repro.sh:/usr/local/bin/snap-gateway-repro \ - -- /usr/local/bin/snap-gateway-repro /tmp/install.sh system-docker 10 -``` - -Start without Docker and verify that `install.sh` rejects the Snap installation: - -```shell -nix run .#test-guest -- \ - --distro ubuntu-24-04 \ - --with snapd \ - --keep \ - --copy ./install.sh:/tmp/install.sh \ - --copy ./nix/test-guest/scripts/snap-gateway-repro.sh:/usr/local/bin/snap-gateway-repro \ - -- /usr/local/bin/snap-gateway-repro /tmp/install.sh missing-docker 10 -``` - -`--keep` retains the overlay and serial log when diagnosing a failure. The -runner prints their location after shutdown. - - The destination must be an absolute guest path. Use bare octal permission bits from `000` through `777` for explicit modes. diff --git a/nix/test-guest/scripts/snap-gateway-repro.sh b/nix/test-guest/scripts/snap-gateway-repro.sh deleted file mode 100755 index 8dd09fc206..0000000000 --- a/nix/test-guest/scripts/snap-gateway-repro.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Reproduce the Release Canary Snap lifecycle with install.sh. Run this as the -# test user inside an Ubuntu guest prepared with --with snapd and, for the -# system-docker mode, --with docker. - -set -uo pipefail - -usage() { - cat <<'EOF' -Usage: snap-gateway-repro.sh INSTALL_SCRIPT MODE [ATTEMPTS] - -MODE is system-docker, missing-docker, or docker-snap. The system-docker mode requires an -existing non-Snap Docker daemon, verifies that install.sh does not install the -Docker snap, and exercises a sandbox. The missing-docker mode requires Docker -to be absent and verifies that install.sh fails before installing either snap. -The docker-snap mode requires the Docker snap and verifies that install.sh -rejects it before installing OpenShell. ATTEMPTS defaults to 1; repeated -system-docker attempts exercise idempotent OpenShell Snap refreshes. Every -failed attempt prints service, connection, snap-change, journal, gateway-log, -and listener diagnostics. -EOF -} - -if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then - usage >&2 - exit 2 -fi - -install_script=$1 -mode=$2 -attempts=${3:-1} -if [ ! -f "${install_script}" ]; then - echo "Install script does not exist: ${install_script}" >&2 - exit 2 -fi -if [[ ! ${attempts} =~ ^[1-9][0-9]*$ ]]; then - echo "ATTEMPTS must be a positive integer: ${attempts}" >&2 - exit 2 -fi -case "${mode}" in -system-docker) - if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then - echo "system-docker mode requires a running preinstalled Docker daemon" >&2 - exit 2 - fi - if sudo snap list docker >/dev/null 2>&1; then - echo "system-docker mode does not support the Docker snap" >&2 - exit 2 - fi - ;; -missing-docker) - if command -v docker >/dev/null 2>&1; then - echo "missing-docker mode requires Docker to be absent" >&2 - exit 2 - fi - ;; -docker-snap) - if ! sudo snap list docker >/dev/null 2>&1; then - echo "docker-snap mode requires the Docker snap" >&2 - exit 2 - fi - ;; -*) - echo "MODE must be system-docker, missing-docker, or docker-snap: ${mode}" >&2 - exit 2 - ;; -esac - -docker_is_ready() { - docker info >/dev/null -} - -diagnostics() { - local attempt=$1 - echo "========== Snap diagnostics (attempt ${attempt}) ==========" >&2 - if sudo snap list docker >/dev/null 2>&1; then - sudo snap services docker >&2 || true - sudo systemctl status snap.docker.dockerd.service --no-pager >&2 || true - sudo journalctl -b -u snap.docker.dockerd.service --no-pager -n 300 >&2 || true - else - sudo systemctl status docker.service --no-pager >&2 || true - sudo journalctl -b -u docker.service --no-pager -n 300 >&2 || true - fi - sudo snap services openshell >&2 || true - sudo snap connections openshell >&2 || true - sudo snap changes >&2 || true - sudo systemctl status snap.openshell.gateway.service --no-pager >&2 || true - sudo journalctl -b -u snap.openshell.gateway.service --no-pager -n 300 >&2 || true - sudo journalctl -b -u snapd.service --no-pager -n 300 >&2 || true - sudo snap logs openshell.gateway -n=300 >&2 || true - sudo ss -ltnp '( sport = :17670 )' >&2 || true -} - -failures=0 -for attempt in $(seq 1 "${attempts}"); do - echo "==> install.sh Snap ${mode} reproduction attempt ${attempt}/${attempts}" - if [ "${mode}" != system-docker ]; then - output=$(mktemp) - if OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}" >"${output}" 2>&1; then - echo "install.sh unexpectedly succeeded in ${mode} mode" >&2 - cat "${output}" >&2 - rm -f "${output}" - diagnostics "${attempt}" - failures=$((failures + 1)) - continue - fi - case "${mode}" in - missing-docker) expected="Docker is required before installing the OpenShell snap" ;; - docker-snap) expected="the Docker snap is not currently compatible with OpenShell" ;; - esac - if ! grep -Fq "${expected}" "${output}" || - sudo snap list openshell >/dev/null 2>&1 || - { [ "${mode}" = missing-docker ] && sudo snap list docker >/dev/null 2>&1; }; then - echo "install.sh did not fail safely in ${mode} mode" >&2 - cat "${output}" >&2 - rm -f "${output}" - diagnostics "${attempt}" - failures=$((failures + 1)) - continue - fi - cat "${output}" - rm -f "${output}" - continue - fi - - sandbox="snap-${attempt}-$$" - prover_dir=$(mktemp -d "$HOME/openshell-prover-repro.XXXXXX") - cat >"${prover_dir}/boundary.yaml" <<'EOF' -version: 1 -filesystem_policy: - read_only: - - /usr - - /etc -EOF - cat >"${prover_dir}/candidate.yaml" <<'EOF' -version: 1 -filesystem_policy: - read_only: - - /usr -EOF - if ! OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}" || - ! sudo snap list openshell >/dev/null || - ! snap info openshell | grep -Eq '^tracking: +latest/edge$' || - ! docker_is_ready || - ! sudo snap connections openshell | grep -Eq '^docker +openshell:docker +:docker +' || - ! /snap/bin/openshell status || - ! /snap/bin/openshell.prover --version || - ! /snap/bin/openshell.prover check "${prover_dir}/candidate.yaml" \ - --boundary "${prover_dir}/boundary.yaml" | grep -q '^result: within_boundary$' || - ! /snap/bin/openshell sandbox create --name "${sandbox}" --detach || - ! /snap/bin/openshell sandbox exec --name "${sandbox}" --no-tty -- true || - ! /snap/bin/openshell sandbox delete "${sandbox}"; then - echo "install.sh Snap reproduction failed" >&2 - diagnostics "${attempt}" - failures=$((failures + 1)) - rm -rf "${prover_dir}" - continue - fi - rm -rf "${prover_dir}" - - if sudo snap list docker >/dev/null 2>&1; then - echo "install.sh unexpectedly installed the Docker snap" >&2 - diagnostics "${attempt}" - failures=$((failures + 1)) - fi -done - -if [ "${failures}" -gt 0 ]; then - echo "${failures}/${attempts} attempt(s) failed" >&2 - exit 1 -fi - -echo "All ${attempts} attempt(s) passed" diff --git a/odh/requirements.in b/odh/requirements.in new file mode 100644 index 0000000000..bb4fb62a41 --- /dev/null +++ b/odh/requirements.in @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Fork-local Python runtime and test dependencies for the ODH E2E image. +# Regenerate requirements.txt with: +# uv pip compile --universal --generate-hashes --python-version 3.11 \ +# --output-file odh/requirements.txt odh/requirements.in +cloudpickle>=3.0 +googleapis-common-protos>=1.63 +grpcio>=1.60 +grpcio-tools>=1.60 +httpx>=0.27 +protobuf>=4.25 +pytest>=8.0 diff --git a/odh/requirements.txt b/odh/requirements.txt new file mode 100644 index 0000000000..e4d1baf543 --- /dev/null +++ b/odh/requirements.txt @@ -0,0 +1,173 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --python-version 3.11 --output-file odh/requirements.txt odh/requirements.in +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f + # via httpx +certifi==2026.5.20 \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d + # via + # httpcore + # httpx +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via -r odh/requirements.in +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via pytest +googleapis-common-protos==1.75.3 \ + --hash=sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a \ + --hash=sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2 + # via -r odh/requirements.in +grpcio==1.78.0 \ + --hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \ + --hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \ + --hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \ + --hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \ + --hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \ + --hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \ + --hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \ + --hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \ + --hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \ + --hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \ + --hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \ + --hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \ + --hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \ + --hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \ + --hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \ + --hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \ + --hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \ + --hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \ + --hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \ + --hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \ + --hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \ + --hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \ + --hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \ + --hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \ + --hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \ + --hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \ + --hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \ + --hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \ + --hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \ + --hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \ + --hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \ + --hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \ + --hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \ + --hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \ + --hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \ + --hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \ + --hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \ + --hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \ + --hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \ + --hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \ + --hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce + # via + # -r odh/requirements.in + # grpcio-tools +grpcio-tools==1.78.0 \ + --hash=sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2 \ + --hash=sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599 \ + --hash=sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094 \ + --hash=sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6 \ + --hash=sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5 \ + --hash=sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d \ + --hash=sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577 \ + --hash=sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812 \ + --hash=sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456 \ + --hash=sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb \ + --hash=sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58 \ + --hash=sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4 \ + --hash=sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72 \ + --hash=sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d \ + --hash=sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b \ + --hash=sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26 \ + --hash=sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771 \ + --hash=sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec \ + --hash=sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8 \ + --hash=sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585 \ + --hash=sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af \ + --hash=sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57 \ + --hash=sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229 \ + --hash=sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9 \ + --hash=sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79 \ + --hash=sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51 \ + --hash=sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525 \ + --hash=sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f \ + --hash=sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790 \ + --hash=sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4 \ + --hash=sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63 \ + --hash=sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a \ + --hash=sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796 \ + --hash=sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10 \ + --hash=sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608 \ + --hash=sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f \ + --hash=sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831 \ + --hash=sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45 \ + --hash=sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822 \ + --hash=sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c \ + --hash=sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6 + # via -r odh/requirements.in +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via -r odh/requirements.in +idna==3.17 \ + --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \ + --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f + # via + # anyio + # httpx +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +packaging==26.0 \ + --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ + --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 + # via pytest +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +protobuf==6.33.5 \ + --hash=sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c \ + --hash=sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02 \ + --hash=sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c \ + --hash=sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd \ + --hash=sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190 \ + --hash=sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5 \ + --hash=sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0 \ + --hash=sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b + # via + # -r odh/requirements.in + # googleapis-common-protos + # grpcio-tools +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via pytest +pytest==9.0.2 \ + --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \ + --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 + # via -r odh/requirements.in +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via grpcio-tools +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # anyio + # grpcio diff --git a/proto/openshell.proto b/proto/openshell.proto index df5769086e..2aef9595c2 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -296,6 +296,13 @@ service OpenShell { }; } + // Export selected usable runtime credentials. Only directly authenticated + // mTLS operators may call this method; refresh material is never returned. + rpc GetProviderCredentials(GetProviderCredentialsRequest) + returns (GetProviderCredentialsResponse) { + option (openshell.options.v1.authorization) = { auth_mode: "operator" }; + } + // List providers. rpc ListProviders(ListProvidersRequest) returns (ListProvidersResponse) { option (openshell.options.v1.authorization) = { @@ -2136,6 +2143,31 @@ message GetProviderRequest { string name = 1; } +// Retrieve selected runtime credentials from one provider in one workspace. +// Refresh side effects can persist even when the complete request fails. +message GetProviderCredentialsRequest { + // Required named workspace selection; all_workspaces is not accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 1; + // Provider canonical name. + string name = 2; + // Nonempty selection of runtime environment keys (maximum 32). + repeated string credential_keys = 3; + // Omitted/zero uses a five-minute safety margin. Maximum is 24 hours. + google.protobuf.Duration minimum_remaining_lifetime = 4; +} + +// Usable runtime value, never gateway-only minting material. +message ProviderCredentialValue { + string value = 1 [(openshell.options.v1.secret) = true]; + // Absent for credentials with no declared expiration. + google.protobuf.Timestamp expiration_time = 2; +} + +message GetProviderCredentialsResponse { + // All requested values or an error; no partial response. + map credentials = 1; +} + // List providers request. message ListProvidersRequest { // Workspace scope. Named and all-workspaces selections are accepted. diff --git a/proto/options.proto b/proto/options.proto index 7669e2fe1f..6f127cb281 100644 --- a/proto/options.proto +++ b/proto/options.proto @@ -10,7 +10,8 @@ import "google/protobuf/descriptor.proto"; // Per-method authorization rule. Consumed at runtime by the gateway's // descriptor-pool-based auth table to enforce auth mode, role, and scope. message AuthorizationRule { - // Authentication mode: "bearer", "sandbox", "dual", or "unauthenticated". + // Authentication mode: "bearer", "sandbox", "dual", "peer", "operator", + // or "unauthenticated". Operator requires a verified mTLS operator certificate. string auth_mode = 1; // Minimum workspace-level role required (checked by handler via // authorize_workspace): "user" or "admin". Mutually exclusive with diff --git a/python/openshell/__init__.py b/python/openshell/__init__.py index 109fe09521..1b2f276673 100644 --- a/python/openshell/__init__.py +++ b/python/openshell/__init__.py @@ -7,6 +7,7 @@ from .errors import ErrorInfo, FieldViolation, GatewayError, from_grpc_error from .mutations import DeletionOutcome, DeletionResult +from .providers import ProviderClient, ProviderCredentialValue from .sandbox import ( ClientCredentialsAuth, ExecChunk, @@ -46,6 +47,8 @@ "GatewayError", "Page", "Pager", + "ProviderClient", + "ProviderCredentialValue", "Sandbox", "SandboxClient", "SandboxError", diff --git a/python/openshell/providers.py b/python/openshell/providers.py new file mode 100644 index 0000000000..f3312f5407 --- /dev/null +++ b/python/openshell/providers.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Privileged provider credential retrieval over the caller's gRPC channel.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING + +from ._proto import datamodel_pb2, openshell_pb2, openshell_pb2_grpc +from .errors import _error_mapping_channel + +if TYPE_CHECKING: + from collections.abc import Sequence + + import grpc + + from .sandbox import SandboxClient + + +@dataclass(frozen=True) +class ProviderCredentialValue: + """An exported runtime secret and its optional, UTC expiration. + + The value is omitted from repr, but explicit serialization still exposes it. + Do not log or persist exported credentials in shared output. + """ + + value: str = field(repr=False) + expiration_time: datetime | None = None + + +class ProviderClient: + """Provider credential operations on a shared gRPC channel. + + Retrieval requires direct gateway mTLS with a trusted OU=operator certificate + and no bearer authentication. The gateway must enable operator authentication. + This client does not own or close the supplied channel. + """ + + def __init__(self, channel: grpc.Channel, *, timeout: float = 30.0) -> None: + self._stub = openshell_pb2_grpc.OpenShellStub(_error_mapping_channel(channel)) + self._timeout = timeout + + @classmethod + def from_sandbox_client(cls, client: SandboxClient) -> ProviderClient: + """Reuse a client's transport and timeout; no sandbox is required.""" + return client.providers() + + def get_credentials( + self, + name: str, + credential_keys: Sequence[str], + *, + workspace: str, + minimum_remaining_lifetime: timedelta | None = None, + ) -> dict[str, ProviderCredentialValue]: + """Export exactly the selected runtime keys, or raise without a result. + + Omitted or zero lifetime uses the gateway's five-minute margin; the + maximum is 24 hours. The gateway refreshes eligible missing or short-lived + outputs. Refresh effects can persist even when delivery fails. This + method does not retry or cache secrets and never exports refresh inputs. + """ + if not workspace or workspace.strip() != workspace: + raise ValueError("an explicit canonical workspace is required") + if not name or name.strip() != name or len(name.encode("utf-8")) > 253: + raise ValueError("a canonical provider name is required") + # Copy the selection so caller mutation cannot change response validation. + if isinstance(credential_keys, str): + raise ValueError("select between 1 and 32 unique runtime environment keys") + keys = list(credential_keys) + if ( + not 1 <= len(keys) <= 32 + or any( + len(key) > 256 or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key) is None + for key in keys + ) + or len(set(keys)) != len(keys) + ): + raise ValueError("select between 1 and 32 unique runtime environment keys") + if minimum_remaining_lifetime is not None and not ( + timedelta(0) <= minimum_remaining_lifetime <= timedelta(days=1) + ): + raise ValueError( + "minimum_remaining_lifetime must be between zero and 24 hours" + ) + request = openshell_pb2.GetProviderCredentialsRequest( + workspace_scope=datamodel_pb2.WorkspaceSelector(workspace=workspace), + name=name, + credential_keys=keys, + ) + if minimum_remaining_lifetime is not None: + request.minimum_remaining_lifetime.FromTimedelta(minimum_remaining_lifetime) + response = self._stub.GetProviderCredentials(request, timeout=self._timeout) + if set(response.credentials) != set(keys): + raise ValueError("gateway returned an unexpected credential selection") + return { + key: ProviderCredentialValue( + value=credential.value, + expiration_time=( + credential.expiration_time.ToDatetime(tzinfo=UTC) + if credential.HasField("expiration_time") + else None + ), + ) + for key, credential in response.credentials.items() + } diff --git a/python/openshell/providers_test.py b/python/openshell/providers_test.py new file mode 100644 index 0000000000..b8a9b20152 --- /dev/null +++ b/python/openshell/providers_test.py @@ -0,0 +1,230 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Curated provider retrieval through generated stubs and real gRPC error mapping.""" + +from concurrent import futures +from datetime import UTC, datetime, timedelta +from types import SimpleNamespace + +import grpc +import pytest + +from openshell import ProviderClient, ProviderCredentialValue, SandboxClient, TlsConfig +from openshell._proto import openshell_pb2, openshell_pb2_grpc +from openshell.errors import GatewayError + + +@pytest.fixture +def provider_client(): + state = SimpleNamespace(calls=[], deadlines=[], code=None, response=None) + + def retrieve(request, context): + state.calls.append(request) + state.deadlines.append(context.time_remaining()) + if state.code is not None: + context.set_trailing_metadata((("request-id", "provider-export"),)) + context.abort(state.code, "runtime credential retrieval failed") + if state.response is not None: + return state.response + response = openshell_pb2.GetProviderCredentialsResponse() + for key in request.credential_keys: + response.credentials[key].value = f"secret-{key}" + if key == "ACCESS_TOKEN": + response.credentials[key].expiration_time.FromDatetime( + datetime(2030, 1, 1, tzinfo=UTC) + ) + elif key == "EPOCH_KEY": + response.credentials[key].expiration_time.FromNanoseconds(123_456_789) + return response + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + server.add_generic_rpc_handlers( + ( + grpc.method_handlers_generic_handler( + "openshell.v1.OpenShell", + { + "GetProviderCredentials": grpc.unary_unary_rpc_method_handler( + retrieve, + request_deserializer=openshell_pb2.GetProviderCredentialsRequest.FromString, + response_serializer=openshell_pb2.GetProviderCredentialsResponse.SerializeToString, + ), + }, + ), + ) + ) + port = server.add_insecure_port("127.0.0.1:0") + server.start() + with SandboxClient(f"127.0.0.1:{port}", timeout=3) as connection: + try: + yield ProviderClient.from_sandbox_client(connection), state + finally: + server.stop(0).wait() + + +def test_selected_values_and_expiration_are_curated_and_repr_safe( + provider_client, caplog +): + client, state = provider_client + values = client.get_credentials( + "my-provider", + ["ACCESS_TOKEN", "STATIC_KEY", "EPOCH_KEY"], + workspace="production", + minimum_remaining_lifetime=timedelta(seconds=300, microseconds=123456), + ) + assert set(values) == {"ACCESS_TOKEN", "STATIC_KEY", "EPOCH_KEY"} + assert isinstance(values["ACCESS_TOKEN"], ProviderCredentialValue) + assert values["ACCESS_TOKEN"].value == "secret-ACCESS_TOKEN" + assert values["ACCESS_TOKEN"].expiration_time == datetime(2030, 1, 1, tzinfo=UTC) + assert values["STATIC_KEY"].expiration_time is None + assert values["EPOCH_KEY"].expiration_time == datetime( + 1970, 1, 1, microsecond=123456, tzinfo=UTC + ) + assert "secret-" not in repr(values) + assert "secret-" not in caplog.text + request = state.calls[0] + assert request.name == "my-provider" + assert request.workspace_scope.WhichOneof("selection") == "workspace" + assert request.workspace_scope.workspace == "production" + assert list(request.credential_keys) == ["ACCESS_TOKEN", "STATIC_KEY", "EPOCH_KEY"] + assert request.minimum_remaining_lifetime.seconds == 300 + assert request.minimum_remaining_lifetime.nanos == 123456000 + assert 0 < state.deadlines[0] <= 3.1 + + +@pytest.mark.parametrize("lifetime", [None, timedelta(0), timedelta(days=1)]) +def test_lifetime_default_zero_and_maximum(provider_client, lifetime): + client, state = provider_client + client.get_credentials( + "provider", + ["STATIC_KEY"], + workspace="default", + minimum_remaining_lifetime=lifetime, + ) + request = state.calls[0] + assert request.HasField("minimum_remaining_lifetime") == (lifetime is not None) + if lifetime is not None: + assert request.minimum_remaining_lifetime.ToTimedelta() == lifetime + + +@pytest.mark.parametrize( + "name,keys,workspace,lifetime", + [ + ("", ["KEY"], "default", None), + (" provider", ["KEY"], "default", None), + ("x" * 254, ["KEY"], "default", None), + ("provider", [], "default", None), + ("provider", ["KEY", "KEY"], "default", None), + ("provider", [f"KEY_{i}" for i in range(33)], "default", None), + ("provider", ["KEY=secret-input"], "default", None), + ("provider", ["1KEY"], "default", None), + ("provider", ["é"], "default", None), + ("provider", ["K" * 257], "default", None), + ("provider", "KEY", "default", None), + ("provider", ["KEY"], "", None), + ("provider", ["KEY"], " default", None), + ("provider", ["KEY"], "default", timedelta(microseconds=-1)), + ("provider", ["KEY"], "default", timedelta(days=1, microseconds=1)), + ], +) +def test_invalid_requests_never_contact_gateway( + provider_client, name, keys, workspace, lifetime +): + client, state = provider_client + with pytest.raises(ValueError) as error: + client.get_credentials( + name, keys, workspace=workspace, minimum_remaining_lifetime=lifetime + ) + assert "secret-input" not in str(error.value) + assert not state.calls + + +@pytest.mark.parametrize( + "code", + [ + grpc.StatusCode.UNAUTHENTICATED, + grpc.StatusCode.PERMISSION_DENIED, + grpc.StatusCode.NOT_FOUND, + grpc.StatusCode.FAILED_PRECONDITION, + grpc.StatusCode.UNAVAILABLE, + grpc.StatusCode.ABORTED, + grpc.StatusCode.DEADLINE_EXCEEDED, + ], +) +def test_errors_preserve_status_and_metadata_without_retry(provider_client, code): + client, state = provider_client + state.code = code + with pytest.raises(GatewayError) as error: + client.get_credentials( + "provider", ["ACCESS_TOKEN", "STATIC_KEY"], workspace="other" + ) + assert error.value.code() == code + assert ("request-id", "provider-export") in error.value.trailing_metadata() + assert len(state.calls) == 1 + assert "secret-" not in str(error.value) + + +@pytest.mark.parametrize( + "keys", [["ACCESS_TOKEN"], ["ACCESS_TOKEN", "STATIC_KEY", "PRIVATE_KEY"]] +) +def test_unexpected_or_partial_responses_are_not_delivered(provider_client, keys): + client, state = provider_client + state.response = openshell_pb2.GetProviderCredentialsResponse( + credentials={ + key: openshell_pb2.ProviderCredentialValue(value="must-not-deliver") + for key in keys + } + ) + with pytest.raises(ValueError, match="unexpected credential selection") as error: + client.get_credentials( + "provider", ["ACCESS_TOKEN", "STATIC_KEY"], workspace="other" + ) + assert "must-not-deliver" not in str(error.value) + + +def test_shared_client_preserves_mtls_and_does_not_attach_bearer(monkeypatch, tmp_path): + ca = tmp_path / "ca.crt" + cert = tmp_path / "operator.crt" + key = tmp_path / "operator.key" + for path, value in [(ca, b"ca"), (cert, b"operator-cert"), (key, b"operator-key")]: + path.write_bytes(value) + seen = {} + + def credentials(**kwargs): + seen.update(kwargs) + return "tls-credentials" + + channel = SimpleNamespace(close=lambda: None) + monkeypatch.setattr(grpc, "ssl_channel_credentials", credentials) + + def secure_channel(endpoint, creds): + assert endpoint == "gateway.example.com:443" + assert creds == "tls-credentials" + return channel + + monkeypatch.setattr(grpc, "secure_channel", secure_channel) + monkeypatch.setattr("openshell.sandbox._error_mapping_channel", lambda c: c) + monkeypatch.setattr("openshell.providers._error_mapping_channel", lambda c: c) + stub_channels = [] + + def stub(c): + stub_channels.append(c) + return SimpleNamespace() + + monkeypatch.setattr(openshell_pb2_grpc, "OpenShellStub", stub) + + def reject_bearer(*_args): + pytest.fail("operator connection must not attach a bearer interceptor") + + monkeypatch.setattr(grpc, "intercept_channel", reject_bearer) + with SandboxClient( + "gateway.example.com:443", + tls=TlsConfig(ca_path=ca, cert_path=cert, key_path=key), + ) as connection: + assert isinstance(connection.providers(), ProviderClient) + assert stub_channels == [channel, channel] + assert seen == { + "root_certificates": b"ca", + "private_key": b"operator-key", + "certificate_chain": b"operator-cert", + } diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py index b845c83090..3c34956d07 100644 --- a/python/openshell/release_formula_test.py +++ b/python/openshell/release_formula_test.py @@ -151,19 +151,12 @@ def test_snap_wrapper_uses_optional_gateway_config_without_generating_toml() -> ) assert "init-gateway-config.sh" not in wrapper - assert ( - 'export OPENSHELL_DB_URL="${OPENSHELL_DB_URL:-sqlite:${SNAP_COMMON}/gateway.db?mode=rwc}"' - in wrapper - ) + assert "CANONICAL_CONFIG_FILE" not in wrapper + assert '[ -z "${OPENSHELL_GATEWAY_CONFIG:-}" ]' in wrapper + assert 'export OPENSHELL_GATEWAY_CONFIG="$OPENSHELL_SNAP_CONFIG_FILE"' in wrapper + assert "export OPENSHELL_DB_URL=" not in wrapper assert "OPENSHELL_DISABLE_TLS" not in wrapper - assert ( - 'export OPENSHELL_LOCAL_TLS_DIR="${OPENSHELL_LOCAL_TLS_DIR:-${SNAP_COMMON}/tls}"' - in wrapper - ) - assert ( - 'exec "${SNAP}/bin/openshell-gateway" --config "$CANONICAL_CONFIG_FILE" "$@"' - in wrapper - ) + assert 'export OPENSHELL_LOCAL_TLS_DIR="${XDG_STATE_HOME}/openshell/tls"' in wrapper assert 'exec "${SNAP}/bin/openshell-gateway" "$@"' in wrapper @@ -249,17 +242,8 @@ def test_schema_v2_debian_and_snap_preflight_wiring() -> None: assert "ExecStart=/usr/bin/openshell-gateway" in unit assert "$src_dir/openshell-gateway.service" in package_deb assert "$pkgroot/usr/lib/systemd/user/openshell-gateway.service" in package_deb - assert 'if [ -n "${OPENSHELL_GATEWAY_CONFIG:-}" ]; then' in wrapper - assert ( - 'elif [ -e "$CANONICAL_CONFIG_FILE" ] || [ -L "$CANONICAL_CONFIG_FILE" ]; then' - in wrapper - ) - assert wrapper.count('"${SNAP}/bin/openshell-gateway" config preflight') == 4 + assert '[ -z "${OPENSHELL_GATEWAY_CONFIG:-}" ]' in wrapper + assert 'export OPENSHELL_GATEWAY_CONFIG="$OPENSHELL_SNAP_CONFIG_FILE"' in wrapper + assert wrapper.count('"${SNAP}/bin/openshell-gateway" config preflight') == 1 assert 'config preflight -- "$@"' in wrapper - assert 'config preflight -- --config "$CANONICAL_CONFIG_FILE" "$@"' in wrapper - assert ( - 'exec "${SNAP}/bin/openshell-gateway" --config "$CANONICAL_CONFIG_FILE" "$@"' - in wrapper - ) - assert wrapper.count('exec "${SNAP}/bin/openshell-gateway" "$@"') == 3 - assert '[ -f "$CANONICAL_CONFIG_FILE" ]' not in wrapper + assert wrapper.count('exec "${SNAP}/bin/openshell-gateway" "$@"') == 1 diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 745c31e81a..20ce99263e 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -135,6 +135,8 @@ class _ClientCallDetails(_ClientCallDetailsBase, grpc.ClientCallDetails): import builtins from collections.abc import Callable, Iterator, Mapping, Sequence + from .providers import ProviderClient + @dataclass(frozen=True) class TlsConfig: @@ -929,6 +931,12 @@ def create_session_from_template( def sandbox_templates(self) -> SandboxTemplateClient: return SandboxTemplateClient(self._channel, timeout=self._timeout) + def providers(self) -> ProviderClient: + """Reuse this transport for provider operations; no sandbox is required.""" + from .providers import ProviderClient + + return ProviderClient(self._channel, timeout=self._timeout) + def get(self, name: str, *, workspace: str) -> SandboxRef: response = self._stub.GetSandbox( openshell_pb2.GetSandboxRequest( diff --git a/scripts/lint-mermaid/package-lock.json b/scripts/lint-mermaid/package-lock.json index fcb8c01d19..4a36094898 100644 --- a/scripts/lint-mermaid/package-lock.json +++ b/scripts/lint-mermaid/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "jsdom": "^25.0.1", - "mermaid": "^11.4.0" + "mermaid": "^11.16.1" } }, "node_modules/@antfu/install-pkg": { @@ -1489,9 +1489,10 @@ } }, "node_modules/mermaid": { - "version": "11.16.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", - "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz", + "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==", + "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", diff --git a/scripts/lint-mermaid/package.json b/scripts/lint-mermaid/package.json index 271bb08043..ba43f51753 100644 --- a/scripts/lint-mermaid/package.json +++ b/scripts/lint-mermaid/package.json @@ -6,7 +6,7 @@ "type": "module", "dependencies": { "jsdom": "^25.0.1", - "mermaid": "^11.4.0" + "mermaid": "^11.16.1" }, "overrides": { "dompurify": "3.4.15" diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 9575a8b8ef..a8cf975097 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -8240,6 +8240,179 @@ func (x *GetProviderRequest) GetName() string { return "" } +// Retrieve selected runtime credentials from one provider in one workspace. +// Refresh side effects can persist even when the complete request fails. +type GetProviderCredentialsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required named workspace selection; all_workspaces is not accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,1,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Provider canonical name. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Nonempty selection of runtime environment keys (maximum 32). + CredentialKeys []string `protobuf:"bytes,3,rep,name=credential_keys,json=credentialKeys,proto3" json:"credential_keys,omitempty"` + // Omitted/zero uses a five-minute safety margin. Maximum is 24 hours. + MinimumRemainingLifetime *durationpb.Duration `protobuf:"bytes,4,opt,name=minimum_remaining_lifetime,json=minimumRemainingLifetime,proto3" json:"minimum_remaining_lifetime,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderCredentialsRequest) Reset() { + *x = GetProviderCredentialsRequest{} + mi := &file_openshell_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderCredentialsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderCredentialsRequest) ProtoMessage() {} + +func (x *GetProviderCredentialsRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[95] + 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 GetProviderCredentialsRequest.ProtoReflect.Descriptor instead. +func (*GetProviderCredentialsRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{95} +} + +func (x *GetProviderCredentialsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetProviderCredentialsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetProviderCredentialsRequest) GetCredentialKeys() []string { + if x != nil { + return x.CredentialKeys + } + return nil +} + +func (x *GetProviderCredentialsRequest) GetMinimumRemainingLifetime() *durationpb.Duration { + if x != nil { + return x.MinimumRemainingLifetime + } + return nil +} + +// Usable runtime value, never gateway-only minting material. +type ProviderCredentialValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + // Absent for credentials with no declared expiration. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialValue) Reset() { + *x = ProviderCredentialValue{} + mi := &file_openshell_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialValue) ProtoMessage() {} + +func (x *ProviderCredentialValue) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[96] + 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 ProviderCredentialValue.ProtoReflect.Descriptor instead. +func (*ProviderCredentialValue) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{96} +} + +func (x *ProviderCredentialValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *ProviderCredentialValue) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +type GetProviderCredentialsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // All requested values or an error; no partial response. + Credentials map[string]*ProviderCredentialValue `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderCredentialsResponse) Reset() { + *x = GetProviderCredentialsResponse{} + mi := &file_openshell_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderCredentialsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderCredentialsResponse) ProtoMessage() {} + +func (x *GetProviderCredentialsResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[97] + 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 GetProviderCredentialsResponse.ProtoReflect.Descriptor instead. +func (*GetProviderCredentialsResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{97} +} + +func (x *GetProviderCredentialsResponse) GetCredentials() map[string]*ProviderCredentialValue { + if x != nil { + return x.Credentials + } + return nil +} + // List providers request. type ListProvidersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -8257,7 +8430,7 @@ type ListProvidersRequest struct { func (x *ListProvidersRequest) Reset() { *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8269,7 +8442,7 @@ func (x *ListProvidersRequest) String() string { func (*ListProvidersRequest) ProtoMessage() {} func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8282,7 +8455,7 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{98} } func (x *ListProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8327,7 +8500,7 @@ type UpdateProviderRequest struct { func (x *UpdateProviderRequest) Reset() { *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8339,7 +8512,7 @@ func (x *UpdateProviderRequest) String() string { func (*UpdateProviderRequest) ProtoMessage() {} func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8352,7 +8525,7 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{99} } func (x *UpdateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8406,7 +8579,7 @@ type DeleteProviderRequest struct { func (x *DeleteProviderRequest) Reset() { *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8418,7 +8591,7 @@ func (x *DeleteProviderRequest) String() string { func (*DeleteProviderRequest) ProtoMessage() {} func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8431,7 +8604,7 @@ func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{100} } func (x *DeleteProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8477,7 +8650,7 @@ type ProviderResponse struct { func (x *ProviderResponse) Reset() { *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8489,7 +8662,7 @@ func (x *ProviderResponse) String() string { func (*ProviderResponse) ProtoMessage() {} func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8502,7 +8675,7 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { @@ -8538,7 +8711,7 @@ type ListProvidersResponse struct { func (x *ListProvidersResponse) Reset() { *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8550,7 +8723,7 @@ func (x *ListProvidersResponse) String() string { func (*ListProvidersResponse) ProtoMessage() {} func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8563,7 +8736,7 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { @@ -8597,7 +8770,7 @@ type ListProviderProfilesRequest struct { func (x *ListProviderProfilesRequest) Reset() { *x = ListProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8609,7 +8782,7 @@ func (x *ListProviderProfilesRequest) String() string { func (*ListProviderProfilesRequest) ProtoMessage() {} func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8622,7 +8795,7 @@ func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *ListProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8658,7 +8831,7 @@ type GetProviderProfileRequest struct { func (x *GetProviderProfileRequest) Reset() { *x = GetProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8670,7 +8843,7 @@ func (x *GetProviderProfileRequest) String() string { func (*GetProviderProfileRequest) ProtoMessage() {} func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8683,7 +8856,7 @@ func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *GetProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -8711,7 +8884,7 @@ type ProviderProfileImportItem struct { func (x *ProviderProfileImportItem) Reset() { *x = ProviderProfileImportItem{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8723,7 +8896,7 @@ func (x *ProviderProfileImportItem) String() string { func (*ProviderProfileImportItem) ProtoMessage() {} func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8736,7 +8909,7 @@ func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { @@ -8767,7 +8940,7 @@ type ProviderProfileDiagnostic struct { func (x *ProviderProfileDiagnostic) Reset() { *x = ProviderProfileDiagnostic{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8779,7 +8952,7 @@ func (x *ProviderProfileDiagnostic) String() string { func (*ProviderProfileDiagnostic) ProtoMessage() {} func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8792,7 +8965,7 @@ func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *ProviderProfileDiagnostic) GetSource() string { @@ -8849,7 +9022,7 @@ type ProviderCredentialTokenGrantAudienceOverride struct { func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { *x = ProviderCredentialTokenGrantAudienceOverride{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8861,7 +9034,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8874,7 +9047,7 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protorefle // Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { @@ -8928,7 +9101,7 @@ type ProviderCredentialTokenGrantSubjectToken struct { func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { *x = ProviderCredentialTokenGrantSubjectToken{} - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8940,7 +9113,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) String() string { func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8953,7 +9126,7 @@ func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.M // Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { @@ -9009,7 +9182,7 @@ type ProviderCredentialTokenGrant struct { func (x *ProviderCredentialTokenGrant) Reset() { *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9021,7 +9194,7 @@ func (x *ProviderCredentialTokenGrant) String() string { func (*ProviderCredentialTokenGrant) ProtoMessage() {} func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9034,7 +9207,7 @@ func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { @@ -9132,7 +9305,7 @@ type ProviderProfileCredential struct { func (x *ProviderProfileCredential) Reset() { *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9144,7 +9317,7 @@ func (x *ProviderProfileCredential) String() string { func (*ProviderProfileCredential) ProtoMessage() {} func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9157,7 +9330,7 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *ProviderProfileCredential) GetName() string { @@ -9249,7 +9422,7 @@ type ProviderCredentialRefreshMaterial struct { func (x *ProviderCredentialRefreshMaterial) Reset() { *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9261,7 +9434,7 @@ func (x *ProviderCredentialRefreshMaterial) String() string { func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9274,7 +9447,7 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message // Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *ProviderCredentialRefreshMaterial) GetName() string { @@ -9319,7 +9492,7 @@ type ProviderCredentialRefreshOutput struct { func (x *ProviderCredentialRefreshOutput) Reset() { *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9331,7 +9504,7 @@ func (x *ProviderCredentialRefreshOutput) String() string { func (*ProviderCredentialRefreshOutput) ProtoMessage() {} func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9344,7 +9517,7 @@ func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *ProviderCredentialRefreshOutput) GetOutput() string { @@ -9376,7 +9549,7 @@ type ProviderCredentialRefresh struct { func (x *ProviderCredentialRefresh) Reset() { *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9388,7 +9561,7 @@ func (x *ProviderCredentialRefresh) String() string { func (*ProviderCredentialRefresh) ProtoMessage() {} func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9401,7 +9574,7 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { @@ -9482,7 +9655,7 @@ type ProviderCredentialRefreshStatus struct { func (x *ProviderCredentialRefreshStatus) Reset() { *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9494,7 +9667,7 @@ func (x *ProviderCredentialRefreshStatus) String() string { func (*ProviderCredentialRefreshStatus) ProtoMessage() {} func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9507,7 +9680,7 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *ProviderCredentialRefreshStatus) GetProvider() string { @@ -9612,7 +9785,7 @@ type ProviderProfileDiscovery struct { func (x *ProviderProfileDiscovery) Reset() { *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9624,7 +9797,7 @@ func (x *ProviderProfileDiscovery) String() string { func (*ProviderProfileDiscovery) ProtoMessage() {} func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9637,7 +9810,7 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *ProviderProfileDiscovery) GetCredentials() []string { @@ -9659,7 +9832,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9671,7 +9844,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9684,7 +9857,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *GetProviderRefreshStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -9717,7 +9890,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9729,7 +9902,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9742,7 +9915,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -9774,7 +9947,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9786,7 +9959,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9799,7 +9972,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *ConfigureProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -9867,7 +10040,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9879,7 +10052,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9892,7 +10065,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -9917,7 +10090,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9929,7 +10102,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9942,7 +10115,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *RotateProviderCredentialRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -9982,7 +10155,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9994,7 +10167,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10007,7 +10180,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{121} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -10033,7 +10206,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10045,7 +10218,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10058,7 +10231,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *DeleteProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10105,7 +10278,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10117,7 +10290,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10130,7 +10303,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *DeleteProviderRefreshResponse) GetOutcome() DeletionOutcome { @@ -10173,7 +10346,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10185,7 +10358,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10198,7 +10371,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *ProviderProfile) GetId() string { @@ -10314,7 +10487,7 @@ type ProviderProfileFile struct { func (x *ProviderProfileFile) Reset() { *x = ProviderProfileFile{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10326,7 +10499,7 @@ func (x *ProviderProfileFile) String() string { func (*ProviderProfileFile) ProtoMessage() {} func (x *ProviderProfileFile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10339,7 +10512,7 @@ func (x *ProviderProfileFile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileFile.ProtoReflect.Descriptor instead. func (*ProviderProfileFile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{125} } func (x *ProviderProfileFile) GetPath() string { @@ -10373,7 +10546,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10385,7 +10558,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10398,7 +10571,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -10420,7 +10593,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10432,7 +10605,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10445,7 +10618,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -10477,7 +10650,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10489,7 +10662,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10502,7 +10675,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *ImportProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10538,7 +10711,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10550,7 +10723,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10563,7 +10736,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -10609,7 +10782,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10621,7 +10794,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10634,7 +10807,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *UpdateProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10684,7 +10857,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10696,7 +10869,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10709,7 +10882,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -10745,7 +10918,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10757,7 +10930,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10770,7 +10943,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *LintProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10798,7 +10971,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10810,7 +10983,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10823,7 +10996,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{133} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -10850,7 +11023,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10862,7 +11035,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10875,7 +11048,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *DeleteProviderResponse) GetOutcome() DeletionOutcome { @@ -10901,7 +11074,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10913,7 +11086,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10926,7 +11099,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{135} } func (x *DeleteProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -10967,7 +11140,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10979,7 +11152,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10992,7 +11165,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *DeleteProviderProfileResponse) GetOutcome() DeletionOutcome { @@ -11017,7 +11190,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11029,7 +11202,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11042,7 +11215,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -11071,7 +11244,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11083,7 +11256,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11096,7 +11269,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{138} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -11140,7 +11313,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11152,7 +11325,7 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11165,7 +11338,7 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { @@ -11224,7 +11397,7 @@ type GetSandboxProviderEnvironmentResponse struct { func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11236,7 +11409,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11249,7 +11422,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -11339,7 +11512,7 @@ type ExchangeProviderSubjectTokenRequest struct { func (x *ExchangeProviderSubjectTokenRequest) Reset() { *x = ExchangeProviderSubjectTokenRequest{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11351,7 +11524,7 @@ func (x *ExchangeProviderSubjectTokenRequest) String() string { func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11364,7 +11537,7 @@ func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { @@ -11406,7 +11579,7 @@ type ExchangeProviderSubjectTokenResponse struct { func (x *ExchangeProviderSubjectTokenResponse) Reset() { *x = ExchangeProviderSubjectTokenResponse{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11418,7 +11591,7 @@ func (x *ExchangeProviderSubjectTokenResponse) String() string { func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11431,7 +11604,7 @@ func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{142} } func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { @@ -11504,7 +11677,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11516,7 +11689,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11529,7 +11702,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *UpdateConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -11626,7 +11799,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11638,7 +11811,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11651,7 +11824,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -11765,7 +11938,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11777,7 +11950,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11790,7 +11963,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *AddNetworkRule) GetRuleName() string { @@ -11818,7 +11991,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11830,7 +12003,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11843,7 +12016,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -11876,7 +12049,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11888,7 +12061,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11901,7 +12074,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -11930,7 +12103,7 @@ type L7RuleTarget struct { func (x *L7RuleTarget) Reset() { *x = L7RuleTarget{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11942,7 +12115,7 @@ func (x *L7RuleTarget) String() string { func (*L7RuleTarget) ProtoMessage() {} func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11955,7 +12128,7 @@ func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RuleTarget.ProtoReflect.Descriptor instead. func (*L7RuleTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *L7RuleTarget) GetRuleName() string { @@ -12010,7 +12183,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12022,7 +12195,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12035,7 +12208,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *AddDenyRules) GetDenyRules() []*sandboxv1.L7DenyRule { @@ -12062,7 +12235,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12074,7 +12247,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12087,7 +12260,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *AddAllowRules) GetRules() []*sandboxv1.L7Rule { @@ -12114,7 +12287,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12126,7 +12299,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12139,7 +12312,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -12175,7 +12348,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12187,7 +12360,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12200,7 +12373,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -12254,7 +12427,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12266,7 +12439,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12279,7 +12452,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12323,7 +12496,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12335,7 +12508,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12348,7 +12521,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{154} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -12385,7 +12558,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12397,7 +12570,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12410,7 +12583,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -12462,7 +12635,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12474,7 +12647,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12487,7 +12660,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{156} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -12521,7 +12694,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12533,7 +12706,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12546,7 +12719,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -12586,7 +12759,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12598,7 +12771,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12611,7 +12784,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{158} } type SandboxConfigurationAdmission struct { @@ -12629,7 +12802,7 @@ type SandboxConfigurationAdmission struct { func (x *SandboxConfigurationAdmission) Reset() { *x = SandboxConfigurationAdmission{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12641,7 +12814,7 @@ func (x *SandboxConfigurationAdmission) String() string { func (*SandboxConfigurationAdmission) ProtoMessage() {} func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12654,7 +12827,7 @@ func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxConfigurationAdmission.ProtoReflect.Descriptor instead. func (*SandboxConfigurationAdmission) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *SandboxConfigurationAdmission) GetInstanceId() string { @@ -12718,7 +12891,7 @@ type ReportSandboxConfigurationRequest struct { func (x *ReportSandboxConfigurationRequest) Reset() { *x = ReportSandboxConfigurationRequest{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12730,7 +12903,7 @@ func (x *ReportSandboxConfigurationRequest) String() string { func (*ReportSandboxConfigurationRequest) ProtoMessage() {} func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12743,7 +12916,7 @@ func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationRequest.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *ReportSandboxConfigurationRequest) GetSandboxId() string { @@ -12775,7 +12948,7 @@ type ReportSandboxConfigurationResponse struct { func (x *ReportSandboxConfigurationResponse) Reset() { *x = ReportSandboxConfigurationResponse{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12787,7 +12960,7 @@ func (x *ReportSandboxConfigurationResponse) String() string { func (*ReportSandboxConfigurationResponse) ProtoMessage() {} func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12800,7 +12973,7 @@ func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ReportSandboxConfigurationResponse.ProtoReflect.Descriptor instead. func (*ReportSandboxConfigurationResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{161} } // A versioned policy revision with metadata. @@ -12833,7 +13006,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12845,7 +13018,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12858,7 +13031,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -12938,7 +13111,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12950,7 +13123,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12963,7 +13136,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -13021,7 +13194,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13033,7 +13206,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13046,7 +13219,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -13072,7 +13245,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13084,7 +13257,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13097,7 +13270,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{165} } // Get sandbox logs response. @@ -13113,7 +13286,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13125,7 +13298,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13138,7 +13311,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -13171,7 +13344,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13183,7 +13356,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13196,7 +13369,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -13288,7 +13461,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13300,7 +13473,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13313,7 +13486,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -13443,7 +13616,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13455,7 +13628,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13468,7 +13641,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *SupervisorHello) GetSandboxId() string { @@ -13526,7 +13699,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13538,7 +13711,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13551,7 +13724,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *SessionAccepted) GetSessionId() string { @@ -13579,7 +13752,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13591,7 +13764,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13604,7 +13777,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *SessionRejected) GetReason() string { @@ -13633,7 +13806,7 @@ type SessionRedirect struct { func (x *SessionRedirect) Reset() { *x = SessionRedirect{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13645,7 +13818,7 @@ func (x *SessionRedirect) String() string { func (*SessionRedirect) ProtoMessage() {} func (x *SessionRedirect) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13658,7 +13831,7 @@ func (x *SessionRedirect) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRedirect.ProtoReflect.Descriptor instead. func (*SessionRedirect) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *SessionRedirect) GetPeerEndpoint() string { @@ -13684,7 +13857,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13696,7 +13869,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13709,7 +13882,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{173} } // Gateway heartbeat. @@ -13721,7 +13894,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13733,7 +13906,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13746,7 +13919,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{174} } // Terminal result reported before the supervisor shuts down. A successful RPC @@ -13763,7 +13936,7 @@ type ReportMainProcessExitRequest struct { func (x *ReportMainProcessExitRequest) Reset() { *x = ReportMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13775,7 +13948,7 @@ func (x *ReportMainProcessExitRequest) String() string { func (*ReportMainProcessExitRequest) ProtoMessage() {} func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13788,7 +13961,7 @@ func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *ReportMainProcessExitRequest) GetSandboxId() string { @@ -13820,7 +13993,7 @@ type ReportMainProcessExitResponse struct { func (x *ReportMainProcessExitResponse) Reset() { *x = ReportMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13832,7 +14005,7 @@ func (x *ReportMainProcessExitResponse) String() string { func (*ReportMainProcessExitResponse) ProtoMessage() {} func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13845,7 +14018,7 @@ func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{176} } // Terminal-delivery completion reported after all expected foreground SSH @@ -13860,7 +14033,7 @@ type FinalizeMainProcessExitRequest struct { func (x *FinalizeMainProcessExitRequest) Reset() { *x = FinalizeMainProcessExitRequest{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13872,7 +14045,7 @@ func (x *FinalizeMainProcessExitRequest) String() string { func (*FinalizeMainProcessExitRequest) ProtoMessage() {} func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13885,7 +14058,7 @@ func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { @@ -13910,7 +14083,7 @@ type FinalizeMainProcessExitResponse struct { func (x *FinalizeMainProcessExitResponse) Reset() { *x = FinalizeMainProcessExitResponse{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13922,7 +14095,7 @@ func (x *FinalizeMainProcessExitResponse) String() string { func (*FinalizeMainProcessExitResponse) ProtoMessage() {} func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13935,7 +14108,7 @@ func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{178} } // Gateway requests the supervisor to open a relay channel. @@ -13964,7 +14137,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13976,7 +14149,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13989,7 +14162,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *RelayOpen) GetChannelId() string { @@ -14056,7 +14229,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14068,7 +14241,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14081,7 +14254,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{180} } // TCP target dialed by the supervisor from inside the sandbox. @@ -14097,7 +14270,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14109,7 +14282,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14122,7 +14295,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *TcpRelayTarget) GetHost() string { @@ -14150,7 +14323,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14162,7 +14335,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14175,7 +14348,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *RelayInit) GetChannelId() string { @@ -14202,7 +14375,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14214,7 +14387,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14227,7 +14400,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -14287,7 +14460,7 @@ type PeerRelayInit struct { func (x *PeerRelayInit) Reset() { *x = PeerRelayInit{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14299,7 +14472,7 @@ func (x *PeerRelayInit) String() string { func (*PeerRelayInit) ProtoMessage() {} func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14312,7 +14485,7 @@ func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayInit.ProtoReflect.Descriptor instead. func (*PeerRelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *PeerRelayInit) GetSandboxId() string { @@ -14350,7 +14523,7 @@ type PeerRelayFrame struct { func (x *PeerRelayFrame) Reset() { *x = PeerRelayFrame{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14362,7 +14535,7 @@ func (x *PeerRelayFrame) String() string { func (*PeerRelayFrame) ProtoMessage() {} func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14375,7 +14548,7 @@ func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRelayFrame.ProtoReflect.Descriptor instead. func (*PeerRelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *PeerRelayFrame) GetPayload() isPeerRelayFrame_Payload { @@ -14434,7 +14607,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14446,7 +14619,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14459,7 +14632,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *RelayOpenResult) GetChannelId() string { @@ -14496,7 +14669,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14508,7 +14681,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14521,7 +14694,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *RelayClose) GetChannelId() string { @@ -14555,7 +14728,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14567,7 +14740,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14580,7 +14753,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *L7RequestSample) GetMethod() string { @@ -14654,7 +14827,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14666,7 +14839,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14679,7 +14852,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{189} } func (x *DenialSummary) GetSandboxId() string { @@ -14814,7 +14987,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14826,7 +14999,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14839,7 +15012,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -14872,7 +15045,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14884,7 +15057,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14897,7 +15070,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -14985,7 +15158,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14997,7 +15170,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15010,7 +15183,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *PolicyChunk) GetId() string { @@ -15198,7 +15371,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15210,7 +15383,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15223,7 +15396,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -15282,7 +15455,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15294,7 +15467,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15307,7 +15480,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *SubmitPolicyAnalysisRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -15370,7 +15543,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15382,7 +15555,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[192] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15395,7 +15568,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{192} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -15440,7 +15613,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15452,7 +15625,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[193] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15465,7 +15638,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{193} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -15505,7 +15678,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15517,7 +15690,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[194] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15530,7 +15703,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{194} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -15581,7 +15754,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15593,7 +15766,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[195] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15606,7 +15779,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{195} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -15656,7 +15829,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15668,7 +15841,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[196] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15681,7 +15854,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{196} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -15717,7 +15890,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15729,7 +15902,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[197] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15742,7 +15915,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{197} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -15788,7 +15961,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15800,7 +15973,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[198] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15813,7 +15986,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{198} + return file_openshell_proto_rawDescGZIP(), []int{201} } // Approve all pending chunks. @@ -15827,7 +16000,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15839,7 +16012,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[199] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15852,7 +16025,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{199} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *DraftChunkApproval) GetChunkId() string { @@ -15888,7 +16061,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15900,7 +16073,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[200] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15913,7 +16086,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{200} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -15968,7 +16141,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15980,7 +16153,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[201] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15993,7 +16166,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{201} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -16043,7 +16216,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16055,7 +16228,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[202] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16068,7 +16241,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{202} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16114,7 +16287,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16126,7 +16299,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[203] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16139,7 +16312,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{203} + return file_openshell_proto_rawDescGZIP(), []int{206} } // Reverse an approval (remove merged rule from active policy). @@ -16159,7 +16332,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16171,7 +16344,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[204] + mi := &file_openshell_proto_msgTypes[207] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16184,7 +16357,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{204} + return file_openshell_proto_rawDescGZIP(), []int{207} } func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16227,7 +16400,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16239,7 +16412,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[205] + mi := &file_openshell_proto_msgTypes[208] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16252,7 +16425,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{205} + return file_openshell_proto_rawDescGZIP(), []int{208} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -16284,7 +16457,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16296,7 +16469,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[206] + mi := &file_openshell_proto_msgTypes[209] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16309,7 +16482,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{206} + return file_openshell_proto_rawDescGZIP(), []int{209} } func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16343,7 +16516,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16355,7 +16528,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[207] + mi := &file_openshell_proto_msgTypes[210] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16368,7 +16541,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{207} + return file_openshell_proto_rawDescGZIP(), []int{210} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -16390,7 +16563,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16402,7 +16575,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[208] + mi := &file_openshell_proto_msgTypes[211] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16415,7 +16588,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{208} + return file_openshell_proto_rawDescGZIP(), []int{211} } func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -16449,7 +16622,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16461,7 +16634,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[209] + mi := &file_openshell_proto_msgTypes[212] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16474,7 +16647,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{209} + return file_openshell_proto_rawDescGZIP(), []int{212} } func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { @@ -16515,7 +16688,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16527,7 +16700,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[210] + mi := &file_openshell_proto_msgTypes[213] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16540,7 +16713,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{210} + return file_openshell_proto_rawDescGZIP(), []int{213} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -16565,7 +16738,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16577,7 +16750,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[211] + mi := &file_openshell_proto_msgTypes[214] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16590,7 +16763,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{211} + return file_openshell_proto_rawDescGZIP(), []int{214} } func (x *CreateWorkspaceRequest) GetName() string { @@ -16624,7 +16797,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16636,7 +16809,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[212] + mi := &file_openshell_proto_msgTypes[215] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16649,7 +16822,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{212} + return file_openshell_proto_rawDescGZIP(), []int{215} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -16670,7 +16843,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16682,7 +16855,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[213] + mi := &file_openshell_proto_msgTypes[216] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16695,7 +16868,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{213} + return file_openshell_proto_rawDescGZIP(), []int{216} } func (x *GetWorkspaceRequest) GetName() string { @@ -16715,7 +16888,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16727,7 +16900,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[214] + mi := &file_openshell_proto_msgTypes[217] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16740,7 +16913,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{214} + return file_openshell_proto_rawDescGZIP(), []int{217} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -16767,7 +16940,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16779,7 +16952,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[215] + mi := &file_openshell_proto_msgTypes[218] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16792,7 +16965,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{215} + return file_openshell_proto_rawDescGZIP(), []int{218} } func (x *ListWorkspacesRequest) GetPageSize() int32 { @@ -16828,7 +17001,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16840,7 +17013,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[216] + mi := &file_openshell_proto_msgTypes[219] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16853,7 +17026,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{216} + return file_openshell_proto_rawDescGZIP(), []int{219} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -16884,7 +17057,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16896,7 +17069,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[217] + mi := &file_openshell_proto_msgTypes[220] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16909,7 +17082,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{217} + return file_openshell_proto_rawDescGZIP(), []int{220} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -16943,7 +17116,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16955,7 +17128,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[218] + mi := &file_openshell_proto_msgTypes[221] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16968,7 +17141,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{218} + return file_openshell_proto_rawDescGZIP(), []int{221} } func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { @@ -16992,7 +17165,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17004,7 +17177,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[219] + mi := &file_openshell_proto_msgTypes[222] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17017,7 +17190,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{219} + return file_openshell_proto_rawDescGZIP(), []int{222} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -17058,7 +17231,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17070,7 +17243,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[220] + mi := &file_openshell_proto_msgTypes[223] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17083,7 +17256,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{220} + return file_openshell_proto_rawDescGZIP(), []int{223} } func (x *AddWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17124,7 +17297,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17136,7 +17309,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[221] + mi := &file_openshell_proto_msgTypes[224] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17149,7 +17322,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{221} + return file_openshell_proto_rawDescGZIP(), []int{224} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -17175,7 +17348,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17187,7 +17360,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[222] + mi := &file_openshell_proto_msgTypes[225] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17200,7 +17373,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{222} + return file_openshell_proto_rawDescGZIP(), []int{225} } func (x *RemoveWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17241,7 +17414,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[226] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17253,7 +17426,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[223] + mi := &file_openshell_proto_msgTypes[226] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17266,7 +17439,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{223} + return file_openshell_proto_rawDescGZIP(), []int{226} } func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { @@ -17293,7 +17466,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[227] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17305,7 +17478,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[224] + mi := &file_openshell_proto_msgTypes[227] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17318,7 +17491,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{224} + return file_openshell_proto_rawDescGZIP(), []int{227} } func (x *ListWorkspaceMembersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { @@ -17354,7 +17527,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[228] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17366,7 +17539,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[225] + mi := &file_openshell_proto_msgTypes[228] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17379,7 +17552,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{225} + return file_openshell_proto_rawDescGZIP(), []int{228} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -17414,7 +17587,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[229] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17426,7 +17599,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[226] + mi := &file_openshell_proto_msgTypes[229] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17439,7 +17612,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{226} + return file_openshell_proto_rawDescGZIP(), []int{229} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -17476,7 +17649,7 @@ type EndpointObservation struct { func (x *EndpointObservation) Reset() { *x = EndpointObservation{} - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[230] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17488,7 +17661,7 @@ func (x *EndpointObservation) String() string { func (*EndpointObservation) ProtoMessage() {} func (x *EndpointObservation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[227] + mi := &file_openshell_proto_msgTypes[230] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17501,7 +17674,7 @@ func (x *EndpointObservation) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointObservation.ProtoReflect.Descriptor instead. func (*EndpointObservation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{227} + return file_openshell_proto_rawDescGZIP(), []int{230} } func (x *EndpointObservation) GetEndpointId() string { @@ -17544,7 +17717,7 @@ type ReportEndpointStatusRequest struct { func (x *ReportEndpointStatusRequest) Reset() { *x = ReportEndpointStatusRequest{} - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[231] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17556,7 +17729,7 @@ func (x *ReportEndpointStatusRequest) String() string { func (*ReportEndpointStatusRequest) ProtoMessage() {} func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[228] + mi := &file_openshell_proto_msgTypes[231] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17569,7 +17742,7 @@ func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{228} + return file_openshell_proto_rawDescGZIP(), []int{231} } func (x *ReportEndpointStatusRequest) GetSandboxId() string { @@ -17630,7 +17803,7 @@ type ReportEndpointStatusResponse struct { func (x *ReportEndpointStatusResponse) Reset() { *x = ReportEndpointStatusResponse{} - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[232] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17642,7 +17815,7 @@ func (x *ReportEndpointStatusResponse) String() string { func (*ReportEndpointStatusResponse) ProtoMessage() {} func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[229] + mi := &file_openshell_proto_msgTypes[232] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17655,7 +17828,7 @@ func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{229} + return file_openshell_proto_rawDescGZIP(), []int{232} } // A configured endpoint and its last accepted network result in one record. @@ -17684,7 +17857,7 @@ type EndpointStatus struct { func (x *EndpointStatus) Reset() { *x = EndpointStatus{} - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[233] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17696,7 +17869,7 @@ func (x *EndpointStatus) String() string { func (*EndpointStatus) ProtoMessage() {} func (x *EndpointStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[230] + mi := &file_openshell_proto_msgTypes[233] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17709,7 +17882,7 @@ func (x *EndpointStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use EndpointStatus.ProtoReflect.Descriptor instead. func (*EndpointStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{230} + return file_openshell_proto_rawDescGZIP(), []int{233} } func (x *EndpointStatus) GetEndpointId() string { @@ -17797,7 +17970,7 @@ type SandboxProvisioning struct { func (x *SandboxProvisioning) Reset() { *x = SandboxProvisioning{} - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[234] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17809,7 +17982,7 @@ func (x *SandboxProvisioning) String() string { func (*SandboxProvisioning) ProtoMessage() {} func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[231] + mi := &file_openshell_proto_msgTypes[234] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17822,7 +17995,7 @@ func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxProvisioning.ProtoReflect.Descriptor instead. func (*SandboxProvisioning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{231} + return file_openshell_proto_rawDescGZIP(), []int{234} } func (x *SandboxProvisioning) GetAttemptId() string { @@ -17945,7 +18118,7 @@ type SandboxServiceExposure struct { func (x *SandboxServiceExposure) Reset() { *x = SandboxServiceExposure{} - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[235] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -17957,7 +18130,7 @@ func (x *SandboxServiceExposure) String() string { func (*SandboxServiceExposure) ProtoMessage() {} func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[232] + mi := &file_openshell_proto_msgTypes[235] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -17970,7 +18143,7 @@ func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxServiceExposure.ProtoReflect.Descriptor instead. func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{232} + return file_openshell_proto_rawDescGZIP(), []int{235} } func (x *SandboxServiceExposure) GetService() string { @@ -18555,7 +18728,20 @@ const file_openshell_proto_rawDesc = "" + "request_id\x18\x03 \x01(\tR\trequestId\"|\n" + "\x12GetProviderRequest\x12R\n" + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"\xa6\x01\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x89\x02\n" + + "\x1dGetProviderCredentialsRequest\x12R\n" + + "\x0fworkspace_scope\x18\x01 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12'\n" + + "\x0fcredential_keys\x18\x03 \x03(\tR\x0ecredentialKeys\x12W\n" + + "\x1aminimum_remaining_lifetime\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\x18minimumRemainingLifetime\"z\n" + + "\x17ProviderCredentialValue\x12\x1a\n" + + "\x05value\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05value\x12C\n" + + "\x0fexpiration_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\"\xe8\x01\n" + + "\x1eGetProviderCredentialsResponse\x12_\n" + + "\vcredentials\x18\x01 \x03(\v2=.openshell.v1.GetProviderCredentialsResponse.CredentialsEntryR\vcredentials\x1ae\n" + + "\x10CredentialsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12;\n" + + "\x05value\x18\x02 \x01(\v2%.openshell.v1.ProviderCredentialValueR\x05value:\x028\x01\"\xa6\x01\n" + "\x14ListProvidersRequest\x12R\n" + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + @@ -19488,7 +19674,7 @@ const file_openshell_proto_rawDesc = "" + "\x18ServiceAuthorizationMode\x12*\n" + "&SERVICE_AUTHORIZATION_MODE_UNSPECIFIED\x10\x00\x12$\n" + " SERVICE_AUTHORIZATION_MODE_STRIP\x10\x01\x121\n" + - "-SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH\x10\x022\xafU\n" + + "-SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH\x10\x022\xb5V\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -19550,7 +19736,10 @@ const file_openshell_proto_rawDesc = "" + "\x0eCreateProvider\x12#.openshell.v1.CreateProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\"#\x82\xb5\x18\x1f\n" + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12r\n" + "\vGetProvider\x12 .openshell.v1.GetProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\"!\x82\xb5\x18\x1d\n" + - "\x06bearer\x12\x04user\"\rprovider:read\x12{\n" + + "\x06bearer\x12\x04user\"\rprovider:read\x12\x83\x01\n" + + "\x16GetProviderCredentials\x12+.openshell.v1.GetProviderCredentialsRequest\x1a,.openshell.v1.GetProviderCredentialsResponse\"\x0e\x82\xb5\x18\n" + + "\n" + + "\boperator\x12{\n" + "\rListProviders\x12\".openshell.v1.ListProvidersRequest\x1a#.openshell.v1.ListProvidersResponse\"!\x82\xb5\x18\x1d\n" + "\x06bearer\x12\x04user\"\rprovider:read\x12\x90\x01\n" + "\x14ListProviderProfiles\x12).openshell.v1.ListProviderProfilesRequest\x1a*.openshell.v1.ListProviderProfilesResponse\"!\x82\xb5\x18\x1d\n" + @@ -19675,7 +19864,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 20) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 256) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 260) var file_openshell_proto_goTypes = []any{ (ExtensionKind)(0), // 0: openshell.v1.ExtensionKind (SandboxPhase)(0), // 1: openshell.v1.SandboxPhase @@ -19792,192 +19981,196 @@ var file_openshell_proto_goTypes = []any{ (*SandboxStreamWarning)(nil), // 112: openshell.v1.SandboxStreamWarning (*CreateProviderRequest)(nil), // 113: openshell.v1.CreateProviderRequest (*GetProviderRequest)(nil), // 114: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 115: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 116: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 117: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 118: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 119: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 120: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 121: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 122: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 123: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 124: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrantSubjectToken)(nil), // 125: openshell.v1.ProviderCredentialTokenGrantSubjectToken - (*ProviderCredentialTokenGrant)(nil), // 126: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 127: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 128: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 129: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 130: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 131: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 132: openshell.v1.ProviderProfileDiscovery - (*GetProviderRefreshStatusRequest)(nil), // 133: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 134: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 135: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 136: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 137: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 138: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 139: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 140: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 141: openshell.v1.ProviderProfile - (*ProviderProfileFile)(nil), // 142: openshell.v1.ProviderProfileFile - (*ProviderProfileResponse)(nil), // 143: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 144: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 145: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 146: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 147: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 148: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 149: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 150: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 151: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 152: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 153: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 154: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 155: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 156: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 157: openshell.v1.GetSandboxProviderEnvironmentResponse - (*ExchangeProviderSubjectTokenRequest)(nil), // 158: openshell.v1.ExchangeProviderSubjectTokenRequest - (*ExchangeProviderSubjectTokenResponse)(nil), // 159: openshell.v1.ExchangeProviderSubjectTokenResponse - (*UpdateConfigRequest)(nil), // 160: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 161: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 162: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 163: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 164: openshell.v1.RemoveNetworkRule - (*L7RuleTarget)(nil), // 165: openshell.v1.L7RuleTarget - (*AddDenyRules)(nil), // 166: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 167: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 168: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 169: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 170: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 171: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 172: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 173: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 174: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 175: openshell.v1.ReportPolicyStatusResponse - (*SandboxConfigurationAdmission)(nil), // 176: openshell.v1.SandboxConfigurationAdmission - (*ReportSandboxConfigurationRequest)(nil), // 177: openshell.v1.ReportSandboxConfigurationRequest - (*ReportSandboxConfigurationResponse)(nil), // 178: openshell.v1.ReportSandboxConfigurationResponse - (*SandboxPolicyRevision)(nil), // 179: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 180: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 181: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 182: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 183: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 184: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 185: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 186: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 187: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 188: openshell.v1.SessionRejected - (*SessionRedirect)(nil), // 189: openshell.v1.SessionRedirect - (*SupervisorHeartbeat)(nil), // 190: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 191: openshell.v1.GatewayHeartbeat - (*ReportMainProcessExitRequest)(nil), // 192: openshell.v1.ReportMainProcessExitRequest - (*ReportMainProcessExitResponse)(nil), // 193: openshell.v1.ReportMainProcessExitResponse - (*FinalizeMainProcessExitRequest)(nil), // 194: openshell.v1.FinalizeMainProcessExitRequest - (*FinalizeMainProcessExitResponse)(nil), // 195: openshell.v1.FinalizeMainProcessExitResponse - (*RelayOpen)(nil), // 196: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 197: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 198: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 199: openshell.v1.RelayInit - (*RelayFrame)(nil), // 200: openshell.v1.RelayFrame - (*PeerRelayInit)(nil), // 201: openshell.v1.PeerRelayInit - (*PeerRelayFrame)(nil), // 202: openshell.v1.PeerRelayFrame - (*RelayOpenResult)(nil), // 203: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 204: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 205: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 206: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 207: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 208: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 209: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 210: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 211: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 212: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 213: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 214: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 215: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 216: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 217: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 218: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 219: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 220: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 221: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 222: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 223: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 224: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 225: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 226: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 227: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 228: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 229: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 230: openshell.v1.GetDraftHistoryResponse - (*CreateWorkspaceRequest)(nil), // 231: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 232: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 233: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 234: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 235: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 236: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 237: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 238: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 239: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 240: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 241: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 242: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 243: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 244: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 245: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 246: openshell.v1.ExtensionServiceCredential - (*EndpointObservation)(nil), // 247: openshell.v1.EndpointObservation - (*ReportEndpointStatusRequest)(nil), // 248: openshell.v1.ReportEndpointStatusRequest - (*ReportEndpointStatusResponse)(nil), // 249: openshell.v1.ReportEndpointStatusResponse - (*EndpointStatus)(nil), // 250: openshell.v1.EndpointStatus - (*SandboxProvisioning)(nil), // 251: openshell.v1.SandboxProvisioning - (*SandboxServiceExposure)(nil), // 252: openshell.v1.SandboxServiceExposure - nil, // 253: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 254: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 255: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 256: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 257: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry - nil, // 258: openshell.v1.PlatformEvent.MetadataEntry - nil, // 259: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 260: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 261: openshell.v1.SandboxResponse.ServiceUrlsEntry - nil, // 262: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 263: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 264: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - nil, // 265: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 266: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 267: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 268: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - nil, // 269: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 270: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 271: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - nil, // 272: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 273: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 274: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 275: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*timestamppb.Timestamp)(nil), // 276: google.protobuf.Timestamp - (*datamodelv1.ObjectMeta)(nil), // 277: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 278: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 279: google.protobuf.Struct - (*durationpb.Duration)(nil), // 280: google.protobuf.Duration - (*datamodelv1.WorkspaceSelector)(nil), // 281: openshell.datamodel.v1.WorkspaceSelector - (*datamodelv1.Provider)(nil), // 282: openshell.datamodel.v1.Provider - (sandboxv1.PolicySource)(0), // 283: openshell.sandbox.v1.PolicySource - (*sandboxv1.NetworkEndpoint)(nil), // 284: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 285: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 286: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 287: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 288: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 289: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 290: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 291: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 292: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 293: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 294: openshell.sandbox.v1.GetGatewayConfigResponse + (*GetProviderCredentialsRequest)(nil), // 115: openshell.v1.GetProviderCredentialsRequest + (*ProviderCredentialValue)(nil), // 116: openshell.v1.ProviderCredentialValue + (*GetProviderCredentialsResponse)(nil), // 117: openshell.v1.GetProviderCredentialsResponse + (*ListProvidersRequest)(nil), // 118: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 119: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 120: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 121: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 122: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 123: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 124: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 125: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 126: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 127: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 128: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 129: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 130: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 131: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 132: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 133: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 134: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 135: openshell.v1.ProviderProfileDiscovery + (*GetProviderRefreshStatusRequest)(nil), // 136: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 137: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 138: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 139: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 140: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 141: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 142: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 143: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 144: openshell.v1.ProviderProfile + (*ProviderProfileFile)(nil), // 145: openshell.v1.ProviderProfileFile + (*ProviderProfileResponse)(nil), // 146: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 147: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 148: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 149: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 150: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 151: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 152: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 153: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 154: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 155: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 156: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 157: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 158: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 159: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 160: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 161: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 162: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 163: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 164: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 165: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 166: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 167: openshell.v1.RemoveNetworkRule + (*L7RuleTarget)(nil), // 168: openshell.v1.L7RuleTarget + (*AddDenyRules)(nil), // 169: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 170: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 171: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 172: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 173: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 174: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 175: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 176: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 177: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 178: openshell.v1.ReportPolicyStatusResponse + (*SandboxConfigurationAdmission)(nil), // 179: openshell.v1.SandboxConfigurationAdmission + (*ReportSandboxConfigurationRequest)(nil), // 180: openshell.v1.ReportSandboxConfigurationRequest + (*ReportSandboxConfigurationResponse)(nil), // 181: openshell.v1.ReportSandboxConfigurationResponse + (*SandboxPolicyRevision)(nil), // 182: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 183: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 184: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 185: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 186: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 187: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 188: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 189: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 190: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 191: openshell.v1.SessionRejected + (*SessionRedirect)(nil), // 192: openshell.v1.SessionRedirect + (*SupervisorHeartbeat)(nil), // 193: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 194: openshell.v1.GatewayHeartbeat + (*ReportMainProcessExitRequest)(nil), // 195: openshell.v1.ReportMainProcessExitRequest + (*ReportMainProcessExitResponse)(nil), // 196: openshell.v1.ReportMainProcessExitResponse + (*FinalizeMainProcessExitRequest)(nil), // 197: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 198: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 199: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 200: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 201: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 202: openshell.v1.RelayInit + (*RelayFrame)(nil), // 203: openshell.v1.RelayFrame + (*PeerRelayInit)(nil), // 204: openshell.v1.PeerRelayInit + (*PeerRelayFrame)(nil), // 205: openshell.v1.PeerRelayFrame + (*RelayOpenResult)(nil), // 206: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 207: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 208: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 209: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 210: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 211: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 212: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 213: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 214: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 215: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 216: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 217: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 218: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 219: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 220: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 221: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 222: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 223: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 224: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 225: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 226: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 227: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 228: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 229: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 230: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 231: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 232: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 233: openshell.v1.GetDraftHistoryResponse + (*CreateWorkspaceRequest)(nil), // 234: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 235: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 236: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 237: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 238: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 239: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 240: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 241: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 242: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 243: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 244: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 245: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 246: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 247: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 248: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 249: openshell.v1.ExtensionServiceCredential + (*EndpointObservation)(nil), // 250: openshell.v1.EndpointObservation + (*ReportEndpointStatusRequest)(nil), // 251: openshell.v1.ReportEndpointStatusRequest + (*ReportEndpointStatusResponse)(nil), // 252: openshell.v1.ReportEndpointStatusResponse + (*EndpointStatus)(nil), // 253: openshell.v1.EndpointStatus + (*SandboxProvisioning)(nil), // 254: openshell.v1.SandboxProvisioning + (*SandboxServiceExposure)(nil), // 255: openshell.v1.SandboxServiceExposure + nil, // 256: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 257: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 258: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 259: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 260: openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + nil, // 261: openshell.v1.PlatformEvent.MetadataEntry + nil, // 262: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 263: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 264: openshell.v1.SandboxResponse.ServiceUrlsEntry + nil, // 265: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 266: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 267: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry + nil, // 268: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + nil, // 269: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 270: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 271: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 272: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + nil, // 273: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 274: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 275: openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + nil, // 276: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 277: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 278: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 279: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*timestamppb.Timestamp)(nil), // 280: google.protobuf.Timestamp + (*datamodelv1.ObjectMeta)(nil), // 281: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 282: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 283: google.protobuf.Struct + (*durationpb.Duration)(nil), // 284: google.protobuf.Duration + (*datamodelv1.WorkspaceSelector)(nil), // 285: openshell.datamodel.v1.WorkspaceSelector + (*datamodelv1.Provider)(nil), // 286: openshell.datamodel.v1.Provider + (sandboxv1.PolicySource)(0), // 287: openshell.sandbox.v1.PolicySource + (*sandboxv1.NetworkEndpoint)(nil), // 288: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 289: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 290: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 291: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 292: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 293: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 294: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 295: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 296: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 297: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 298: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 276, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 276, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp - 246, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 276, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp + 280, // 0: openshell.v1.IssueSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 280, // 1: openshell.v1.RefreshSandboxTokenResponse.expiration_time:type_name -> google.protobuf.Timestamp + 249, // 2: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 280, // 3: openshell.v1.RefreshSandboxTokenResponse.sandbox_expiration_time:type_name -> google.protobuf.Timestamp 13, // 4: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 13, // 5: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 31, // 6: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo @@ -19988,488 +20181,495 @@ var file_openshell_proto_depIdxs = []int32{ 34, // 11: openshell.v1.ResourceCapabilities.cpu:type_name -> openshell.v1.CpuResourceCapabilities 35, // 12: openshell.v1.ResourceCapabilities.memory:type_name -> openshell.v1.MemoryResourceCapabilities 36, // 13: openshell.v1.ResourceCapabilities.gpu:type_name -> openshell.v1.GpuResourceCapabilities - 277, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 281, // 14: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 38, // 15: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 49, // 16: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus 48, // 17: openshell.v1.Sandbox.created_from_workload_template:type_name -> openshell.v1.SandboxWorkloadTemplateProvenance - 253, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 256, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 41, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 278, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 282, // 20: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 39, // 21: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 16, // 22: openshell.v1.SandboxSpec.restart_policy:type_name -> openshell.v1.SandboxRestartPolicy 40, // 23: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 254, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 255, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 256, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 279, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 279, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 277, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 257, // 24: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 258, // 25: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 259, // 26: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 283, // 27: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 283, // 28: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 281, // 29: openshell.v1.SandboxWorkloadTemplate.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 43, // 30: openshell.v1.SandboxWorkloadTemplate.spec:type_name -> openshell.v1.SandboxWorkloadTemplateSpec 44, // 31: openshell.v1.SandboxWorkloadTemplateSpec.workload:type_name -> openshell.v1.SandboxWorkloadConfig - 279, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct + 283, // 32: openshell.v1.SandboxWorkloadTemplateSpec.driver_config:type_name -> google.protobuf.Struct 46, // 33: openshell.v1.SandboxWorkloadTemplateSpec.desired_service_level:type_name -> openshell.v1.SandboxServiceLevel - 257, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry + 260, // 34: openshell.v1.SandboxWorkloadConfig.environment:type_name -> openshell.v1.SandboxWorkloadConfig.EnvironmentEntry 45, // 35: openshell.v1.SandboxWorkloadConfig.resources:type_name -> openshell.v1.SandboxResources 40, // 36: openshell.v1.SandboxResources.gpu:type_name -> openshell.v1.GpuResourceRequirements 47, // 37: openshell.v1.SandboxServiceLevel.startup:type_name -> openshell.v1.SandboxStartup - 280, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration + 284, // 38: openshell.v1.SandboxStartup.ready_within:type_name -> google.protobuf.Duration 50, // 39: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 1, // 40: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 250, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus - 176, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 251, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning - 276, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp - 276, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp - 276, // 46: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp - 276, // 47: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp - 258, // 48: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 281, // 49: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 253, // 41: openshell.v1.SandboxStatus.endpoint_statuses:type_name -> openshell.v1.EndpointStatus + 179, // 42: openshell.v1.SandboxStatus.configuration_admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 254, // 43: openshell.v1.SandboxStatus.provisioning:type_name -> openshell.v1.SandboxProvisioning + 280, // 44: openshell.v1.SandboxStatus.next_restart_time:type_name -> google.protobuf.Timestamp + 280, // 45: openshell.v1.SandboxStatus.main_process_started_time:type_name -> google.protobuf.Timestamp + 280, // 46: openshell.v1.SandboxCondition.transition_time:type_name -> google.protobuf.Timestamp + 280, // 47: openshell.v1.PlatformEvent.event_time:type_name -> google.protobuf.Timestamp + 261, // 48: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 285, // 49: openshell.v1.CreateSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 38, // 50: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 259, // 51: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 260, // 52: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 252, // 53: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure - 281, // 54: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 262, // 51: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 263, // 52: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 255, // 53: openshell.v1.CreateSandboxRequest.service_exposures:type_name -> openshell.v1.SandboxServiceExposure + 285, // 54: openshell.v1.CreateSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 42, // 55: openshell.v1.CreateSandboxTemplateRequest.template:type_name -> openshell.v1.SandboxWorkloadTemplate - 281, // 56: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 57: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 58: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 56: openshell.v1.GetSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 57: openshell.v1.ListSandboxTemplatesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 58: openshell.v1.DeleteSandboxTemplateRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 42, // 59: openshell.v1.SandboxTemplateResponse.template:type_name -> openshell.v1.SandboxWorkloadTemplate 42, // 60: openshell.v1.ListSandboxTemplatesResponse.templates:type_name -> openshell.v1.SandboxWorkloadTemplate 17, // 61: openshell.v1.DeleteSandboxTemplateResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 281, // 62: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 63: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp - 281, // 64: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 65: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 66: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 67: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 68: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 69: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 70: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 71: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 62: openshell.v1.BeginRootfsTarStagingRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 280, // 63: openshell.v1.BeginRootfsTarStagingResponse.expiration_time:type_name -> google.protobuf.Timestamp + 285, // 64: openshell.v1.GetSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 65: openshell.v1.ListSandboxesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 66: openshell.v1.ListSandboxProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 67: openshell.v1.AttachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 68: openshell.v1.DetachSandboxProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 69: openshell.v1.DeleteSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 70: openshell.v1.StopSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 71: openshell.v1.StartSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 37, // 72: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 261, // 73: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 264, // 73: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry 37, // 74: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 282, // 75: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 286, // 75: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 37, // 76: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 79, // 77: openshell.v1.AttachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt 37, // 78: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 79, // 79: openshell.v1.DetachSandboxProviderResponse.receipt:type_name -> openshell.v1.ProviderMutationReceipt 77, // 80: openshell.v1.ConfigSnapshotRevision.sandbox_config:type_name -> openshell.v1.SandboxConfigRevision 75, // 81: openshell.v1.ConfigSnapshotRevision.provider_target:type_name -> openshell.v1.ProviderDesiredIdentity - 283, // 82: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 287, // 82: openshell.v1.SandboxConfigRevision.policy_source:type_name -> openshell.sandbox.v1.PolicySource 5, // 83: openshell.v1.ConfigUpdateOperation.component:type_name -> openshell.v1.ConfigComponent 76, // 84: openshell.v1.ConfigUpdateOperation.target_revision:type_name -> openshell.v1.ConfigSnapshotRevision 7, // 85: openshell.v1.ConfigUpdateOperation.state:type_name -> openshell.v1.ConfigUpdateOperationState 6, // 86: openshell.v1.ConfigUpdateOperation.outcome:type_name -> openshell.v1.ConfigApplyOutcome - 276, // 87: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp - 276, // 88: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp - 276, // 89: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp + 280, // 87: openshell.v1.ConfigUpdateOperation.created_time:type_name -> google.protobuf.Timestamp + 280, // 88: openshell.v1.ConfigUpdateOperation.updated_time:type_name -> google.protobuf.Timestamp + 280, // 89: openshell.v1.ConfigUpdateOperation.completed_time:type_name -> google.protobuf.Timestamp 2, // 90: openshell.v1.ProviderMutationReceipt.kind:type_name -> openshell.v1.ProviderMutationKind 75, // 91: openshell.v1.ProviderMutationReceipt.desired:type_name -> openshell.v1.ProviderDesiredIdentity - 276, // 92: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp + 280, // 92: openshell.v1.ProviderMutationReceipt.persisted_time:type_name -> google.protobuf.Timestamp 4, // 93: openshell.v1.ProviderReadinessObservation.reason:type_name -> openshell.v1.ProviderReadinessReason 79, // 94: openshell.v1.ProviderReadinessStatus.receipt:type_name -> openshell.v1.ProviderMutationReceipt 3, // 95: openshell.v1.ProviderReadinessStatus.state:type_name -> openshell.v1.ProviderReadinessState 4, // 96: openshell.v1.ProviderReadinessStatus.reason:type_name -> openshell.v1.ProviderReadinessReason 80, // 97: openshell.v1.ProviderReadinessStatus.observed:type_name -> openshell.v1.ProviderReadinessObservation - 276, // 98: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp - 276, // 99: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp + 280, // 98: openshell.v1.ProviderReadinessStatus.observed_time:type_name -> google.protobuf.Timestamp + 280, // 99: openshell.v1.ProviderReadinessStatus.evaluated_time:type_name -> google.protobuf.Timestamp 78, // 100: openshell.v1.ProviderReadinessStatus.operation:type_name -> openshell.v1.ConfigUpdateOperation - 281, // 101: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 101: openshell.v1.GetSandboxProviderStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 81, // 102: openshell.v1.GetSandboxProviderStatusResponse.status:type_name -> openshell.v1.ProviderReadinessStatus 80, // 103: openshell.v1.ReportProviderReadinessRequest.observation:type_name -> openshell.v1.ProviderReadinessObservation - 280, // 104: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration - 280, // 105: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration + 284, // 104: openshell.v1.ReportProviderReadinessResponse.report_interval:type_name -> google.protobuf.Duration + 284, // 105: openshell.v1.ReportProviderReadinessResponse.observation_ttl:type_name -> google.protobuf.Duration 17, // 106: openshell.v1.DeleteSandboxResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 281, // 107: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 108: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp - 281, // 109: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 107: openshell.v1.CreateSshSessionRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 280, // 108: openshell.v1.CreateSshSessionResponse.expiration_time:type_name -> google.protobuf.Timestamp + 285, // 109: openshell.v1.ExposeServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 19, // 110: openshell.v1.ExposeServiceRequest.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 281, // 111: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 112: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 111: openshell.v1.GetServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 112: openshell.v1.ListServicesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 96, // 113: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 281, // 114: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 114: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector 17, // 115: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 277, // 116: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 281, // 116: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 19, // 117: openshell.v1.ServiceEndpoint.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode 95, // 118: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint 17, // 119: openshell.v1.RevokeSshSessionResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 281, // 120: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 262, // 121: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 280, // 122: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration + 285, // 120: openshell.v1.ExecSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 265, // 121: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 284, // 122: openshell.v1.ExecSandboxRequest.execution_timeout:type_name -> google.protobuf.Duration 100, // 123: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout 101, // 124: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr 102, // 125: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 197, // 126: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 198, // 127: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 200, // 126: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 201, // 127: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget 104, // 128: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit 99, // 129: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest 107, // 130: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 277, // 131: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 276, // 132: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp - 281, // 133: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 134: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp + 281, // 131: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 280, // 132: openshell.v1.SshSession.expiration_time:type_name -> google.protobuf.Timestamp + 285, // 133: openshell.v1.WatchSandboxRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 280, // 134: openshell.v1.WatchSandboxRequest.since_time:type_name -> google.protobuf.Timestamp 37, // 135: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox 111, // 136: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine 51, // 137: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent 112, // 138: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 210, // 139: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 276, // 140: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp - 263, // 141: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 281, // 142: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 282, // 143: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 281, // 144: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 145: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 146: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 282, // 147: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 264, // 148: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry - 281, // 149: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 282, // 150: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 79, // 151: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt - 282, // 152: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 281, // 153: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 154: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 141, // 155: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 280, // 156: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration - 124, // 157: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 8, // 158: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType - 125, // 159: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken - 130, // 160: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 126, // 161: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 9, // 162: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 280, // 163: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration - 280, // 164: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration - 128, // 165: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 129, // 166: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 9, // 167: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 276, // 168: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp - 276, // 169: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp - 276, // 170: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp - 15, // 171: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 276, // 172: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp - 281, // 173: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 131, // 174: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 281, // 175: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 9, // 176: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 265, // 177: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 276, // 178: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp - 131, // 179: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 281, // 180: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 131, // 181: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 281, // 182: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 183: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 10, // 184: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 127, // 185: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 284, // 186: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 285, // 187: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 132, // 188: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 266, // 189: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 142, // 190: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile - 141, // 191: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 141, // 192: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 281, // 193: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 122, // 194: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 123, // 195: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 141, // 196: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 281, // 197: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 122, // 198: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 123, // 199: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 141, // 200: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 281, // 201: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 122, // 202: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 123, // 203: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 17, // 204: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 281, // 205: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 206: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 155, // 207: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 267, // 208: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 268, // 209: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry - 269, // 210: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 270, // 211: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 4, // 212: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason - 271, // 213: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry - 280, // 214: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration - 281, // 215: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 278, // 216: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 286, // 217: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 161, // 218: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 272, // 219: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 162, // 220: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 163, // 221: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 164, // 222: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 166, // 223: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 167, // 224: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 168, // 225: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 287, // 226: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 285, // 227: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 288, // 228: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 165, // 229: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget - 289, // 230: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 165, // 231: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget - 273, // 232: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 281, // 233: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 179, // 234: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 281, // 235: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 179, // 236: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 12, // 237: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 11, // 238: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState - 176, // 239: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission - 12, // 240: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 276, // 241: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp - 276, // 242: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp - 278, // 243: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 274, // 244: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 281, // 245: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 246: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp - 111, // 247: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 111, // 248: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 186, // 249: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 190, // 250: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 203, // 251: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 204, // 252: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 187, // 253: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 188, // 254: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 191, // 255: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 196, // 256: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 204, // 257: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 189, // 258: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect - 280, // 259: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration - 197, // 260: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 198, // 261: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 199, // 262: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 196, // 263: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen - 201, // 264: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit - 276, // 265: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp - 276, // 266: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp - 205, // 267: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 207, // 268: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 287, // 269: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 276, // 270: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp - 276, // 271: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp - 276, // 272: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp - 276, // 273: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp - 278, // 274: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 278, // 275: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 281, // 276: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 206, // 277: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 209, // 278: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 208, // 279: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 281, // 280: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 209, // 281: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 276, // 282: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp - 281, // 283: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 284: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 285: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 219, // 286: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 281, // 287: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 287, // 288: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 281, // 289: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 290: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 281, // 291: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 276, // 292: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp - 229, // 293: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 275, // 294: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 290, // 295: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 290, // 296: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 290, // 297: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 17, // 298: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 277, // 299: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 14, // 300: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 281, // 301: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 14, // 302: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 239, // 303: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 281, // 304: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 17, // 305: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome - 281, // 306: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector - 239, // 307: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 276, // 308: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp - 18, // 309: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult - 247, // 310: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation - 18, // 311: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult - 276, // 312: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp - 276, // 313: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp - 276, // 314: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp - 276, // 315: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp - 276, // 316: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp - 276, // 317: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp - 276, // 318: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp - 276, // 319: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp - 276, // 320: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp - 276, // 321: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp - 19, // 322: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode - 276, // 323: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 276, // 324: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp - 127, // 325: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 156, // 326: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 24, // 327: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 26, // 328: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 28, // 329: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 52, // 330: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 60, // 331: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest - 62, // 332: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 63, // 333: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 53, // 334: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest - 54, // 335: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest - 55, // 336: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest - 56, // 337: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest - 64, // 338: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 65, // 339: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 66, // 340: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 82, // 341: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 67, // 342: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 68, // 343: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 69, // 344: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 87, // 345: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 89, // 346: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 90, // 347: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 91, // 348: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 93, // 349: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 97, // 350: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 99, // 351: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 105, // 352: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 106, // 353: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 113, // 354: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 114, // 355: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 115, // 356: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 120, // 357: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 121, // 358: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 145, // 359: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 147, // 360: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 149, // 361: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 116, // 362: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 133, // 363: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 135, // 364: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 137, // 365: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 139, // 366: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 117, // 367: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 152, // 368: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 291, // 369: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 292, // 370: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 160, // 371: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 170, // 372: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 172, // 373: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 174, // 374: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 248, // 375: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 84, // 376: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 177, // 377: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest - 154, // 378: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 158, // 379: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest - 180, // 380: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 181, // 381: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 184, // 382: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 192, // 383: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 194, // 384: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest - 200, // 385: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 202, // 386: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame - 84, // 387: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest - 248, // 388: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest - 82, // 389: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest - 109, // 390: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 211, // 391: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 213, // 392: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 215, // 393: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 217, // 394: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 220, // 395: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 222, // 396: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 224, // 397: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 226, // 398: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 228, // 399: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 20, // 400: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 22, // 401: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 231, // 402: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 233, // 403: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 235, // 404: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 237, // 405: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 240, // 406: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 242, // 407: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 244, // 408: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 25, // 409: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 27, // 410: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 29, // 411: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 70, // 412: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 61, // 413: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse - 70, // 414: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 71, // 415: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 57, // 416: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 57, // 417: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse - 58, // 418: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse - 59, // 419: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse - 72, // 420: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 73, // 421: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 74, // 422: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 83, // 423: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 86, // 424: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 70, // 425: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 70, // 426: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 88, // 427: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 96, // 428: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 96, // 429: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 92, // 430: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 94, // 431: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 98, // 432: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 103, // 433: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 105, // 434: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 103, // 435: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 118, // 436: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 118, // 437: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 119, // 438: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 144, // 439: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 143, // 440: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 146, // 441: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 148, // 442: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 150, // 443: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 118, // 444: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 134, // 445: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 136, // 446: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 138, // 447: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 140, // 448: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 151, // 449: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 153, // 450: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 293, // 451: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 294, // 452: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 169, // 453: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 171, // 454: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 173, // 455: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 175, // 456: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 249, // 457: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 85, // 458: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 178, // 459: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse - 157, // 460: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 159, // 461: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 183, // 462: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 182, // 463: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 185, // 464: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 193, // 465: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 195, // 466: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse - 200, // 467: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 202, // 468: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame - 85, // 469: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse - 249, // 470: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse - 83, // 471: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse - 110, // 472: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 212, // 473: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 214, // 474: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 216, // 475: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 218, // 476: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 221, // 477: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 223, // 478: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 225, // 479: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 227, // 480: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 230, // 481: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 21, // 482: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 23, // 483: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 232, // 484: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 234, // 485: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 236, // 486: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 238, // 487: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 241, // 488: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 243, // 489: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 245, // 490: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 409, // [409:491] is the sub-list for method output_type - 327, // [327:409] is the sub-list for method input_type - 327, // [327:327] is the sub-list for extension type_name - 327, // [327:327] is the sub-list for extension extendee - 0, // [0:327] is the sub-list for field type_name + 213, // 139: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 280, // 140: openshell.v1.SandboxLogLine.event_time:type_name -> google.protobuf.Timestamp + 266, // 141: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 285, // 142: openshell.v1.CreateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 286, // 143: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 285, // 144: openshell.v1.GetProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 145: openshell.v1.GetProviderCredentialsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 284, // 146: openshell.v1.GetProviderCredentialsRequest.minimum_remaining_lifetime:type_name -> google.protobuf.Duration + 280, // 147: openshell.v1.ProviderCredentialValue.expiration_time:type_name -> google.protobuf.Timestamp + 267, // 148: openshell.v1.GetProviderCredentialsResponse.credentials:type_name -> openshell.v1.GetProviderCredentialsResponse.CredentialsEntry + 285, // 149: openshell.v1.ListProvidersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 150: openshell.v1.UpdateProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 286, // 151: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 268, // 152: openshell.v1.UpdateProviderRequest.credential_expiration_times:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry + 285, // 153: openshell.v1.DeleteProviderRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 286, // 154: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 79, // 155: openshell.v1.ProviderResponse.target_receipts:type_name -> openshell.v1.ProviderMutationReceipt + 286, // 156: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 285, // 157: openshell.v1.ListProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 158: openshell.v1.GetProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 144, // 159: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 284, // 160: openshell.v1.ProviderCredentialTokenGrant.cache_ttl:type_name -> google.protobuf.Duration + 127, // 161: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 8, // 162: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 128, // 163: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 133, // 164: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 129, // 165: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 9, // 166: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 284, // 167: openshell.v1.ProviderCredentialRefresh.refresh_before:type_name -> google.protobuf.Duration + 284, // 168: openshell.v1.ProviderCredentialRefresh.max_lifetime:type_name -> google.protobuf.Duration + 131, // 169: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 132, // 170: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 9, // 171: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 280, // 172: openshell.v1.ProviderCredentialRefreshStatus.expiration_time:type_name -> google.protobuf.Timestamp + 280, // 173: openshell.v1.ProviderCredentialRefreshStatus.next_refresh_time:type_name -> google.protobuf.Timestamp + 280, // 174: openshell.v1.ProviderCredentialRefreshStatus.last_refresh_time:type_name -> google.protobuf.Timestamp + 15, // 175: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction + 280, // 176: openshell.v1.ProviderCredentialRefreshStatus.last_error_time:type_name -> google.protobuf.Timestamp + 285, // 177: openshell.v1.GetProviderRefreshStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 134, // 178: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 285, // 179: openshell.v1.ConfigureProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 9, // 180: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 269, // 181: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 280, // 182: openshell.v1.ConfigureProviderRefreshRequest.expiration_time:type_name -> google.protobuf.Timestamp + 134, // 183: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 285, // 184: openshell.v1.RotateProviderCredentialRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 134, // 185: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 285, // 186: openshell.v1.DeleteProviderRefreshRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 17, // 187: openshell.v1.DeleteProviderRefreshResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 10, // 188: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 130, // 189: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 288, // 190: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 289, // 191: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 135, // 192: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 270, // 193: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 145, // 194: openshell.v1.ProviderProfile.files:type_name -> openshell.v1.ProviderProfileFile + 144, // 195: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 144, // 196: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 285, // 197: openshell.v1.ImportProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 198: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 126, // 199: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 144, // 200: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 285, // 201: openshell.v1.UpdateProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 202: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 126, // 203: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 144, // 204: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 285, // 205: openshell.v1.LintProviderProfilesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 125, // 206: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 126, // 207: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 17, // 208: openshell.v1.DeleteProviderResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 285, // 209: openshell.v1.DeleteProviderProfileRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 17, // 210: openshell.v1.DeleteProviderProfileResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 158, // 211: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 271, // 212: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 272, // 213: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expiration_times:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry + 273, // 214: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 274, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 4, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.readiness_reason:type_name -> openshell.v1.ProviderReadinessReason + 275, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.files:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntry + 284, // 218: openshell.v1.ExchangeProviderSubjectTokenResponse.expires_after:type_name -> google.protobuf.Duration + 285, // 219: openshell.v1.UpdateConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 282, // 220: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 290, // 221: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 164, // 222: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 276, // 223: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 165, // 224: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 166, // 225: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 167, // 226: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 169, // 227: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 170, // 228: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 171, // 229: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 291, // 230: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 289, // 231: openshell.v1.L7RuleTarget.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 292, // 232: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 168, // 233: openshell.v1.AddDenyRules.target:type_name -> openshell.v1.L7RuleTarget + 293, // 234: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 168, // 235: openshell.v1.AddAllowRules.target:type_name -> openshell.v1.L7RuleTarget + 277, // 236: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 285, // 237: openshell.v1.GetSandboxPolicyStatusRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 182, // 238: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 285, // 239: openshell.v1.ListSandboxPoliciesRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 182, // 240: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 12, // 241: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 11, // 242: openshell.v1.SandboxConfigurationAdmission.state:type_name -> openshell.v1.ConfigurationAdmissionState + 179, // 243: openshell.v1.ReportSandboxConfigurationRequest.admission:type_name -> openshell.v1.SandboxConfigurationAdmission + 12, // 244: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 280, // 245: openshell.v1.SandboxPolicyRevision.created_time:type_name -> google.protobuf.Timestamp + 280, // 246: openshell.v1.SandboxPolicyRevision.loaded_time:type_name -> google.protobuf.Timestamp + 282, // 247: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 278, // 248: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 285, // 249: openshell.v1.GetSandboxLogsRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 280, // 250: openshell.v1.GetSandboxLogsRequest.since_time:type_name -> google.protobuf.Timestamp + 111, // 251: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 111, // 252: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 189, // 253: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 193, // 254: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 206, // 255: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 207, // 256: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 190, // 257: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 191, // 258: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 194, // 259: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 199, // 260: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 207, // 261: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 192, // 262: openshell.v1.GatewayMessage.session_redirect:type_name -> openshell.v1.SessionRedirect + 284, // 263: openshell.v1.SessionAccepted.heartbeat_interval:type_name -> google.protobuf.Duration + 200, // 264: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 201, // 265: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 202, // 266: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 199, // 267: openshell.v1.PeerRelayInit.relay_open:type_name -> openshell.v1.RelayOpen + 204, // 268: openshell.v1.PeerRelayFrame.init:type_name -> openshell.v1.PeerRelayInit + 280, // 269: openshell.v1.DenialSummary.first_seen_time:type_name -> google.protobuf.Timestamp + 280, // 270: openshell.v1.DenialSummary.last_seen_time:type_name -> google.protobuf.Timestamp + 208, // 271: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 210, // 272: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 291, // 273: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 280, // 274: openshell.v1.PolicyChunk.created_time:type_name -> google.protobuf.Timestamp + 280, // 275: openshell.v1.PolicyChunk.decided_time:type_name -> google.protobuf.Timestamp + 280, // 276: openshell.v1.PolicyChunk.first_seen_time:type_name -> google.protobuf.Timestamp + 280, // 277: openshell.v1.PolicyChunk.last_seen_time:type_name -> google.protobuf.Timestamp + 282, // 278: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 282, // 279: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 285, // 280: openshell.v1.SubmitPolicyAnalysisRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 209, // 281: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 212, // 282: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 211, // 283: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 285, // 284: openshell.v1.GetDraftPolicyRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 212, // 285: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 280, // 286: openshell.v1.GetDraftPolicyResponse.last_analyzed_time:type_name -> google.protobuf.Timestamp + 285, // 287: openshell.v1.ApproveDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 288: openshell.v1.RejectDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 289: openshell.v1.ApproveAllDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 222, // 290: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 285, // 291: openshell.v1.EditDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 291, // 292: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 285, // 293: openshell.v1.UndoDraftChunkRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 294: openshell.v1.ClearDraftChunksRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 285, // 295: openshell.v1.GetDraftHistoryRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 280, // 296: openshell.v1.DraftHistoryEntry.event_time:type_name -> google.protobuf.Timestamp + 232, // 297: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 279, // 298: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 294, // 299: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 294, // 300: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 294, // 301: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 17, // 302: openshell.v1.DeleteWorkspaceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 281, // 303: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 14, // 304: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 285, // 305: openshell.v1.AddWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 14, // 306: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 242, // 307: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 285, // 308: openshell.v1.RemoveWorkspaceMemberRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 17, // 309: openshell.v1.RemoveWorkspaceMemberResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 285, // 310: openshell.v1.ListWorkspaceMembersRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 242, // 311: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 280, // 312: openshell.v1.ExtensionServiceCredential.expiration_time:type_name -> google.protobuf.Timestamp + 18, // 313: openshell.v1.EndpointObservation.result:type_name -> openshell.v1.EndpointResult + 250, // 314: openshell.v1.ReportEndpointStatusRequest.observations:type_name -> openshell.v1.EndpointObservation + 18, // 315: openshell.v1.EndpointStatus.last_result:type_name -> openshell.v1.EndpointResult + 280, // 316: openshell.v1.EndpointStatus.last_reported_time:type_name -> google.protobuf.Timestamp + 280, // 317: openshell.v1.SandboxProvisioning.configuration_change_time:type_name -> google.protobuf.Timestamp + 280, // 318: openshell.v1.SandboxProvisioning.first_rejection_time:type_name -> google.protobuf.Timestamp + 280, // 319: openshell.v1.SandboxProvisioning.deadline:type_name -> google.protobuf.Timestamp + 280, // 320: openshell.v1.SandboxProvisioning.timeout_time:type_name -> google.protobuf.Timestamp + 280, // 321: openshell.v1.SandboxProvisioning.cleanup_completed_time:type_name -> google.protobuf.Timestamp + 280, // 322: openshell.v1.SandboxProvisioning.cleanup_retry_time:type_name -> google.protobuf.Timestamp + 280, // 323: openshell.v1.SandboxProvisioning.attachment_change_time:type_name -> google.protobuf.Timestamp + 280, // 324: openshell.v1.SandboxProvisioning.preparation_deadline:type_name -> google.protobuf.Timestamp + 280, // 325: openshell.v1.SandboxProvisioning.admission_start_time:type_name -> google.protobuf.Timestamp + 19, // 326: openshell.v1.SandboxServiceExposure.authorization_mode:type_name -> openshell.v1.ServiceAuthorizationMode + 116, // 327: openshell.v1.GetProviderCredentialsResponse.CredentialsEntry.value:type_name -> openshell.v1.ProviderCredentialValue + 280, // 328: openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 280, // 329: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 130, // 330: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 159, // 331: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 24, // 332: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 26, // 333: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 28, // 334: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 52, // 335: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 60, // 336: openshell.v1.OpenShell.BeginRootfsTarStaging:input_type -> openshell.v1.BeginRootfsTarStagingRequest + 62, // 337: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 63, // 338: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 53, // 339: openshell.v1.OpenShell.CreateSandboxTemplate:input_type -> openshell.v1.CreateSandboxTemplateRequest + 54, // 340: openshell.v1.OpenShell.GetSandboxTemplate:input_type -> openshell.v1.GetSandboxTemplateRequest + 55, // 341: openshell.v1.OpenShell.ListSandboxTemplates:input_type -> openshell.v1.ListSandboxTemplatesRequest + 56, // 342: openshell.v1.OpenShell.DeleteSandboxTemplate:input_type -> openshell.v1.DeleteSandboxTemplateRequest + 64, // 343: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 65, // 344: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 66, // 345: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 82, // 346: openshell.v1.OpenShell.GetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 67, // 347: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 68, // 348: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 69, // 349: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 87, // 350: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 89, // 351: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 90, // 352: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 91, // 353: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 93, // 354: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 97, // 355: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 99, // 356: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 105, // 357: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 106, // 358: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 113, // 359: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 114, // 360: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 115, // 361: openshell.v1.OpenShell.GetProviderCredentials:input_type -> openshell.v1.GetProviderCredentialsRequest + 118, // 362: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 123, // 363: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 124, // 364: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 148, // 365: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 150, // 366: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 152, // 367: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 119, // 368: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 136, // 369: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 138, // 370: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 140, // 371: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 142, // 372: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 120, // 373: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 155, // 374: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 295, // 375: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 296, // 376: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 163, // 377: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 173, // 378: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 175, // 379: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 177, // 380: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 251, // 381: openshell.v1.OpenShell.ReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 84, // 382: openshell.v1.OpenShell.ReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 180, // 383: openshell.v1.OpenShell.ReportSandboxConfiguration:input_type -> openshell.v1.ReportSandboxConfigurationRequest + 157, // 384: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 161, // 385: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 183, // 386: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 184, // 387: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 187, // 388: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 195, // 389: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest + 197, // 390: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 203, // 391: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 205, // 392: openshell.v1.OpenShell.PeerRelay:input_type -> openshell.v1.PeerRelayFrame + 84, // 393: openshell.v1.OpenShell.PeerReportProviderReadiness:input_type -> openshell.v1.ReportProviderReadinessRequest + 251, // 394: openshell.v1.OpenShell.PeerReportEndpointStatus:input_type -> openshell.v1.ReportEndpointStatusRequest + 82, // 395: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:input_type -> openshell.v1.GetSandboxProviderStatusRequest + 109, // 396: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 214, // 397: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 216, // 398: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 218, // 399: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 220, // 400: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 223, // 401: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 225, // 402: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 227, // 403: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 229, // 404: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 231, // 405: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 20, // 406: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 22, // 407: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 234, // 408: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 236, // 409: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 238, // 410: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 240, // 411: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 243, // 412: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 245, // 413: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 247, // 414: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 25, // 415: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 27, // 416: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 29, // 417: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 70, // 418: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 61, // 419: openshell.v1.OpenShell.BeginRootfsTarStaging:output_type -> openshell.v1.BeginRootfsTarStagingResponse + 70, // 420: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 71, // 421: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 57, // 422: openshell.v1.OpenShell.CreateSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 57, // 423: openshell.v1.OpenShell.GetSandboxTemplate:output_type -> openshell.v1.SandboxTemplateResponse + 58, // 424: openshell.v1.OpenShell.ListSandboxTemplates:output_type -> openshell.v1.ListSandboxTemplatesResponse + 59, // 425: openshell.v1.OpenShell.DeleteSandboxTemplate:output_type -> openshell.v1.DeleteSandboxTemplateResponse + 72, // 426: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 73, // 427: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 74, // 428: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 83, // 429: openshell.v1.OpenShell.GetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 86, // 430: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 70, // 431: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 70, // 432: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 88, // 433: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 96, // 434: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 96, // 435: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 92, // 436: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 94, // 437: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 98, // 438: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 103, // 439: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 105, // 440: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 103, // 441: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 121, // 442: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 121, // 443: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 117, // 444: openshell.v1.OpenShell.GetProviderCredentials:output_type -> openshell.v1.GetProviderCredentialsResponse + 122, // 445: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 147, // 446: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 146, // 447: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 149, // 448: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 151, // 449: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 153, // 450: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 121, // 451: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 137, // 452: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 139, // 453: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 141, // 454: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 143, // 455: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 154, // 456: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 156, // 457: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 297, // 458: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 298, // 459: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 172, // 460: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 174, // 461: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 176, // 462: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 178, // 463: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 252, // 464: openshell.v1.OpenShell.ReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 85, // 465: openshell.v1.OpenShell.ReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 181, // 466: openshell.v1.OpenShell.ReportSandboxConfiguration:output_type -> openshell.v1.ReportSandboxConfigurationResponse + 160, // 467: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 162, // 468: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 186, // 469: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 185, // 470: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 188, // 471: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 196, // 472: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 198, // 473: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 203, // 474: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 205, // 475: openshell.v1.OpenShell.PeerRelay:output_type -> openshell.v1.PeerRelayFrame + 85, // 476: openshell.v1.OpenShell.PeerReportProviderReadiness:output_type -> openshell.v1.ReportProviderReadinessResponse + 252, // 477: openshell.v1.OpenShell.PeerReportEndpointStatus:output_type -> openshell.v1.ReportEndpointStatusResponse + 83, // 478: openshell.v1.OpenShell.PeerGetSandboxProviderStatus:output_type -> openshell.v1.GetSandboxProviderStatusResponse + 110, // 479: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 215, // 480: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 217, // 481: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 219, // 482: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 221, // 483: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 224, // 484: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 226, // 485: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 228, // 486: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 230, // 487: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 233, // 488: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 21, // 489: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 23, // 490: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 235, // 491: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 237, // 492: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 239, // 493: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 241, // 494: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 244, // 495: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 246, // 496: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 248, // 497: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 415, // [415:498] is the sub-list for method output_type + 332, // [332:415] is the sub-list for method input_type + 332, // [332:332] is the sub-list for extension type_name + 332, // [332:332] is the sub-list for extension extendee + 0, // [0:332] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -20510,7 +20710,7 @@ func file_openshell_proto_init() { (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[141].OneofWrappers = []any{ + file_openshell_proto_msgTypes[144].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -20518,14 +20718,14 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[145].OneofWrappers = []any{} - file_openshell_proto_msgTypes[164].OneofWrappers = []any{ + file_openshell_proto_msgTypes[148].OneofWrappers = []any{} + file_openshell_proto_msgTypes[167].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[165].OneofWrappers = []any{ + file_openshell_proto_msgTypes[168].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), @@ -20533,15 +20733,15 @@ func file_openshell_proto_init() { (*GatewayMessage_RelayClose)(nil), (*GatewayMessage_SessionRedirect)(nil), } - file_openshell_proto_msgTypes[176].OneofWrappers = []any{ + file_openshell_proto_msgTypes[179].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[180].OneofWrappers = []any{ + file_openshell_proto_msgTypes[183].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[182].OneofWrappers = []any{ + file_openshell_proto_msgTypes[185].OneofWrappers = []any{ (*PeerRelayFrame_Init)(nil), (*PeerRelayFrame_Data)(nil), } @@ -20551,7 +20751,7 @@ func file_openshell_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 20, - NumMessages: 256, + NumMessages: 260, 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 d185e24283..5ca018daf6 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -52,6 +52,7 @@ const ( OpenShell_ExecSandboxInteractive_FullMethodName = "/openshell.v1.OpenShell/ExecSandboxInteractive" OpenShell_CreateProvider_FullMethodName = "/openshell.v1.OpenShell/CreateProvider" OpenShell_GetProvider_FullMethodName = "/openshell.v1.OpenShell/GetProvider" + OpenShell_GetProviderCredentials_FullMethodName = "/openshell.v1.OpenShell/GetProviderCredentials" OpenShell_ListProviders_FullMethodName = "/openshell.v1.OpenShell/ListProviders" OpenShell_ListProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ListProviderProfiles" OpenShell_GetProviderProfile_FullMethodName = "/openshell.v1.OpenShell/GetProviderProfile" @@ -187,6 +188,9 @@ type OpenShellClient interface { CreateProvider(ctx context.Context, in *CreateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) // Fetch a provider by name. GetProvider(ctx context.Context, in *GetProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) + // Export selected usable runtime credentials. Only directly authenticated + // mTLS operators may call this method; refresh material is never returned. + GetProviderCredentials(ctx context.Context, in *GetProviderCredentialsRequest, opts ...grpc.CallOption) (*GetProviderCredentialsResponse, error) // List providers. ListProviders(ctx context.Context, in *ListProvidersRequest, opts ...grpc.CallOption) (*ListProvidersResponse, error) // List available provider type profiles. @@ -650,6 +654,16 @@ func (c *openShellClient) GetProvider(ctx context.Context, in *GetProviderReques return out, nil } +func (c *openShellClient) GetProviderCredentials(ctx context.Context, in *GetProviderCredentialsRequest, opts ...grpc.CallOption) (*GetProviderCredentialsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProviderCredentialsResponse) + err := c.cc.Invoke(ctx, OpenShell_GetProviderCredentials_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) ListProviders(ctx context.Context, in *ListProvidersRequest, opts ...grpc.CallOption) (*ListProvidersResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListProvidersResponse) @@ -1281,6 +1295,9 @@ type OpenShellServer interface { CreateProvider(context.Context, *CreateProviderRequest) (*ProviderResponse, error) // Fetch a provider by name. GetProvider(context.Context, *GetProviderRequest) (*ProviderResponse, error) + // Export selected usable runtime credentials. Only directly authenticated + // mTLS operators may call this method; refresh material is never returned. + GetProviderCredentials(context.Context, *GetProviderCredentialsRequest) (*GetProviderCredentialsResponse, error) // List providers. ListProviders(context.Context, *ListProvidersRequest) (*ListProvidersResponse, error) // List available provider type profiles. @@ -1526,6 +1543,9 @@ func (UnimplementedOpenShellServer) CreateProvider(context.Context, *CreateProvi func (UnimplementedOpenShellServer) GetProvider(context.Context, *GetProviderRequest) (*ProviderResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetProvider not implemented") } +func (UnimplementedOpenShellServer) GetProviderCredentials(context.Context, *GetProviderCredentialsRequest) (*GetProviderCredentialsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProviderCredentials not implemented") +} func (UnimplementedOpenShellServer) ListProviders(context.Context, *ListProvidersRequest) (*ListProvidersResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListProviders not implemented") } @@ -2199,6 +2219,24 @@ func _OpenShell_GetProvider_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _OpenShell_GetProviderCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderCredentialsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetProviderCredentials(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetProviderCredentials_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetProviderCredentials(ctx, req.(*GetProviderCredentialsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_ListProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListProvidersRequest) if err := dec(in); err != nil { @@ -3213,6 +3251,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetProvider", Handler: _OpenShell_GetProvider_Handler, }, + { + MethodName: "GetProviderCredentials", + Handler: _OpenShell_GetProviderCredentials_Handler, + }, { MethodName: "ListProviders", Handler: _OpenShell_ListProviders_Handler, diff --git a/sdk/go/proto/optionsv1/options.pb.go b/sdk/go/proto/optionsv1/options.pb.go index 3219a94b1c..85b0903037 100644 --- a/sdk/go/proto/optionsv1/options.pb.go +++ b/sdk/go/proto/optionsv1/options.pb.go @@ -29,7 +29,8 @@ const ( // descriptor-pool-based auth table to enforce auth mode, role, and scope. type AuthorizationRule struct { state protoimpl.MessageState `protogen:"open.v1"` - // Authentication mode: "bearer", "sandbox", "dual", or "unauthenticated". + // Authentication mode: "bearer", "sandbox", "dual", "peer", "operator", + // or "unauthenticated". Operator requires a verified mTLS operator certificate. AuthMode string `protobuf:"bytes,1,opt,name=auth_mode,json=authMode,proto3" json:"auth_mode,omitempty"` // Minimum workspace-level role required (checked by handler via // authorize_workspace): "user" or "admin". Mutually exclusive with diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 99c4fac9d4..e780a362ba 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -115,6 +115,36 @@ const sandbox = await SandboxClient.connect({ gateway, oidcToken }) await sandbox.create({ image }) ``` +## Provider credential retrieval + +`client.providers.getCredentials(name, keys, options)` exports selected runtime +credentials using direct gateway mTLS with a trusted `OU=operator` certificate. +Enable operator authentication at the gateway and omit OIDC and edge tokens. +No sandbox or provider attachment is required. + +```ts +import { ProviderClient } from '@nvidia/openshell-sdk' + +const providers = await ProviderClient.connect({ gateway, caCert, clientCert, clientKey }) +const credentials = await providers.getCredentials( + 'my-graph', + ['MS_GRAPH_ACCESS_TOKEN'], + { workspace: 'production', minimumRemainingLifetimeSecs: 300 }, +) +const token = credentials.MS_GRAPH_ACCESS_TOKEN!.value +``` + +The workspace and a nonempty selection are required. The result maps keys to +`ProviderCredentialValue` objects with `value` and optional `expirationTime` +(`Date`, truncating sub-millisecond precision). Lifetime is in seconds, including +fractions; omission or zero uses the gateway's five-minute margin, up to a +24-hour requested maximum. `signal` and `timeoutMs` bound the RPC, but canceling +delivery does not roll back an already-started refresh. The helper does not +retry or cache exports. Do not log the result: the values can be used outside +sandbox policy enforcement. See the published +[SDK example](https://docs.nvidia.com/openshell/latest/sdk/typescript#retrieve-provider-credentials) +and [provider reference](https://docs.nvidia.com/openshell/latest/how-it-works/providers/overview#retrieve-runtime-credentials). + ## Streaming and interactive exec `execStream` yields stdout/stderr chunks as they arrive, so long or chatty commands surface output incrementally instead of buffering until exit. The stream ends with a terminal `{ type: 'exit', exitCode }` event, yielded in-band so a failing command cannot look successful under `for await`. Discriminate it with `'type' in event`. If the gateway closes the stream without an exit event, `execStream` throws. `exec` drains `execStream` internally, so its buffered `ExecResult` is unchanged. @@ -245,7 +275,7 @@ The SDK's goal is agent parity: anything the OpenShell gateway can do should be - `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. +- `client.providers` (`ProviderClient`) is available today for operator-only credential retrieval. Provider CRUD and profile helpers remain planned. `health()` lives at the root today and will move under `client.gateway` (with a root alias) when that lands. diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index f735ff9495..587d4fc5b1 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -32,6 +32,7 @@ import { } from './gen/openshell_pb.js'; import type { EffectiveSetting, GetSandboxConfigResponse, SandboxPolicy, SettingValue } from './gen/sandbox_pb.js'; import { PolicySource, type SandboxPolicySchema, SettingScope, type SettingValueSchema } from './gen/sandbox_pb.js'; +import { ProviderClient } from './providers.js'; import { validateSshResponse } from './ssh-validate.js'; import { buildTransport, type ConnectOptions } from './transport.js'; @@ -1809,6 +1810,8 @@ export class OpenShellClient { readonly sandbox: SandboxClient; /** Reusable sandbox workload template lifecycle. */ readonly sandboxTemplates: SandboxTemplateClient; + /** Privileged provider credential retrieval; requires direct operator mTLS. */ + readonly providers: ProviderClient; /** * Advanced escape hatch: a generated client for every gateway RPC, including @@ -1829,6 +1832,7 @@ export class OpenShellClient { this.raw = this.grpc; this.sandbox = new SandboxClient(transport, this.grpc); this.sandboxTemplates = new SandboxTemplateClient(transport, this.grpc); + this.providers = new ProviderClient(transport); } /** diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 9229f8bcbe..e4ac061518 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -65,3 +65,5 @@ export type { ErrorInfo, FieldViolation, SdkErrorCode } from './errors.js'; export { fromConnect, SdkError } from './errors.js'; export type { ClientCredentialsOptions, OidcTokenProvider } from './oidc.js'; export { clientCredentials } from './oidc.js'; +export type { ProviderCredentialsOptions, ProviderCredentialValue } from './providers.js'; +export { ProviderClient } from './providers.js'; diff --git a/sdk/typescript/src/providers.test.ts b/sdk/typescript/src/providers.test.ts new file mode 100644 index 0000000000..78e8212b42 --- /dev/null +++ b/sdk/typescript/src/providers.test.ts @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { Code, ConnectError, createRouterTransport, type ServiceImpl } from '@connectrpc/connect'; +import { describe, expect, it, vi } from 'vitest'; +import { + OpenShellClient, + ProviderClient, + type ProviderCredentialsOptions, + type ProviderCredentialValue, +} from './index.js'; +import { OpenShell } from './raw.js'; +import * as transportModule from './transport.js'; + +function providers(impl: Partial>, useBinaryFormat = true): ProviderClient { + return new ProviderClient( + createRouterTransport((router) => router.service(OpenShell, impl), { transport: { useBinaryFormat } }), + ); +} + +describe('provider credential retrieval', () => { + it.each([true, false])('curates selected secrets and optional expiration (binary: %s)', async (binary) => { + const calls: MessageInitShape[] = []; + const provider = providers( + { + getProviderCredentials: (request) => { + calls.push(request); + return { + credentials: { + ACCESS_TOKEN: { value: 'access-secret', expirationTime: { seconds: 1893456000n, nanos: 123456789 } }, + STATIC_KEY: { value: 'static-secret' }, + EPOCH_KEY: { value: 'epoch-secret', expirationTime: { seconds: 0n, nanos: 0 } }, + }, + }; + }, + }, + binary, + ); + const result: Record = await provider.getCredentials( + 'provider', + ['ACCESS_TOKEN', 'STATIC_KEY', 'EPOCH_KEY'], + { workspace: 'production', minimumRemainingLifetimeSecs: 300.123456789 }, + ); + expect(Object.keys(result)).toEqual(['ACCESS_TOKEN', 'STATIC_KEY', 'EPOCH_KEY']); + expect(result.ACCESS_TOKEN).toEqual({ + value: 'access-secret', + expirationTime: new Date('2030-01-01T00:00:00.123Z'), + }); + expect(result.STATIC_KEY).toEqual({ value: 'static-secret' }); + expect(result.EPOCH_KEY).toEqual({ value: 'epoch-secret', expirationTime: new Date(0) }); + expect(result.ACCESS_TOKEN).not.toHaveProperty('$typeName'); + expect(calls[0]).toMatchObject({ + name: 'provider', + workspaceScope: { selection: { case: 'workspace', value: 'production' } }, + credentialKeys: ['ACCESS_TOKEN', 'STATIC_KEY', 'EPOCH_KEY'], + minimumRemainingLifetime: { seconds: 300n, nanos: 123456789 }, + }); + }); + + it.each([undefined, 0, 86400])('preserves default/zero/max lifetime: %s', async (lifetime) => { + let observed: MessageInitShape | undefined; + const provider = providers({ + getProviderCredentials: (request) => { + observed = request; + return { credentials: { KEY: { value: 'secret' } } }; + }, + }); + await provider.getCredentials('provider', ['KEY'], { + workspace: 'default', + minimumRemainingLifetimeSecs: lifetime, + }); + expect(observed?.minimumRemainingLifetime).toEqual( + lifetime === undefined ? undefined : expect.objectContaining({ seconds: BigInt(lifetime), nanos: 0 }), + ); + }); + + it('rounds a positive sub-nanosecond margin up', async () => { + const provider = providers({ + getProviderCredentials: (request) => { + expect(request.minimumRemainingLifetime).toMatchObject({ seconds: 0n, nanos: 1 }); + return { credentials: { KEY: { value: 'secret' } } }; + }, + }); + const result = await provider.getCredentials('provider', ['KEY'], { + workspace: 'default', + minimumRemainingLifetimeSecs: 1e-12, + }); + expect(result).toEqual({ KEY: { value: 'secret' } }); + }); + + it.each([ + ['', ['KEY'], { workspace: 'default' }], + [' provider', ['KEY'], { workspace: 'default' }], + ['p'.repeat(254), ['KEY'], { workspace: 'default' }], + ['provider', [], { workspace: 'default' }], + ['provider', ['KEY', 'KEY'], { workspace: 'default' }], + ['provider', Array.from({ length: 33 }, (_, i) => `KEY_${i}`), { workspace: 'default' }], + ['provider', ['KEY=secret-input'], { workspace: 'default' }], + ['provider', ['1KEY'], { workspace: 'default' }], + ['provider', ['KEY\n'], { workspace: 'default' }], + ['provider', ['é'], { workspace: 'default' }], + ['provider', ['K'.repeat(257)], { workspace: 'default' }], + ['provider', ['KEY'], { workspace: '' }], + ['provider', ['KEY'], { workspace: ' default' }], + ['provider', ['KEY'], { workspace: 'default', minimumRemainingLifetimeSecs: -1 }], + ['provider', ['KEY'], { workspace: 'default', minimumRemainingLifetimeSecs: 86400.1 }], + ['provider', ['KEY'], { workspace: 'default', minimumRemainingLifetimeSecs: Number.NaN }], + ['provider', ['KEY'], { workspace: 'default', minimumRemainingLifetimeSecs: Infinity }], + ['provider', ['KEY'], { workspace: 'default', timeoutMs: 0 }], + ['provider', ['KEY'], { workspace: 'default', timeoutMs: Infinity }], + ] satisfies [string, string[], ProviderCredentialsOptions][])( + 'rejects invalid selectors or options before contacting the gateway: %s', + async (name, keys, options) => { + const retrieve = vi.fn(); + const provider = providers({ getProviderCredentials: retrieve }); + await expect(provider.getCredentials(name, keys, options)).rejects.toMatchObject({ code: 'invalid_config' }); + expect(retrieve).not.toHaveBeenCalled(); + try { + await provider.getCredentials(name, keys, options); + } catch (error) { + expect(String(error)).not.toContain('secret-input'); + } + }, + ); + + it.each([ + [Code.Unauthenticated, 'auth'], + [Code.PermissionDenied, 'auth'], + [Code.NotFound, 'not_found'], + [Code.FailedPrecondition, 'rpc'], + [Code.Unavailable, 'rpc'], + [Code.Aborted, 'aborted'], + [Code.DeadlineExceeded, 'canceled'], + ])('preserves status without retry: %s', async (status, code) => { + const retrieve = vi.fn(() => { + throw new ConnectError('retrieval failed', status as Code); + }); + const provider = providers({ getProviderCredentials: retrieve }); + await expect(provider.getCredentials('provider', ['KEY'], { workspace: 'other' })).rejects.toMatchObject({ + code, + connectCode: status, + }); + expect(retrieve).toHaveBeenCalledOnce(); + }); + + it.each([['ACCESS_TOKEN'], ['ACCESS_TOKEN', 'STATIC_KEY', 'PRIVATE_KEY']])( + 'rejects partial or extra responses without revealing values: %s', + async (...keys) => { + const provider = providers({ + getProviderCredentials: () => ({ + credentials: Object.fromEntries(keys.map((key) => [key, { value: 'must-not-deliver' }])), + }), + }); + const call = provider.getCredentials('provider', ['ACCESS_TOKEN', 'STATIC_KEY'], { workspace: 'other' }); + await expect(call).rejects.toMatchObject({ + code: 'rpc', + message: '[rpc] gateway returned an unexpected credential selection', + }); + }, + ); + + it('snapshots selection across an in-flight RPC', async () => { + let start!: () => void; + let release!: () => void; + const started = new Promise((resolve) => { + start = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + const provider = providers({ + getProviderCredentials: async (request) => { + start(); + await gate; + expect(request.credentialKeys).toEqual(['KEY']); + return { credentials: { KEY: { value: 'secret' } } }; + }, + }); + const keys = ['KEY']; + const response = provider.getCredentials('provider', keys, { workspace: 'default' }); + await started; + keys.push('PRIVATE_KEY'); + release(); + expect(Object.keys(await response)).toEqual(['KEY']); + }); + + it.each([false, true])('forwards cancellation/deadlines (deadline: %s)', async (deadline) => { + let start!: () => void; + const started = new Promise((resolve) => { + start = resolve; + }); + const provider = providers({ + getProviderCredentials: (_, context) => + new Promise((_, reject) => { + start(); + context.signal.addEventListener('abort', () => reject(new ConnectError('canceled', Code.Canceled)), { + once: true, + }); + }), + }); + const controller = new AbortController(); + const response = provider.getCredentials('provider', ['KEY'], { + workspace: 'default', + signal: controller.signal, + ...(deadline ? { timeoutMs: 20 } : {}), + }); + const assertion = expect(response).rejects.toMatchObject({ code: 'canceled' }); + await started; + if (!deadline) controller.abort(); + await assertion; + }); + + it('composes on the root transport and supports standalone connect', async () => { + const transport = createRouterTransport((router) => + router.service(OpenShell, { + getProviderCredentials: () => ({ credentials: { KEY: { value: 'secret' } } }), + }), + ); + const build = vi.spyOn(transportModule, 'buildTransport').mockReturnValue(transport); + const options = { + gateway: 'https://gateway.example.com', + clientCert: Buffer.from('operator-cert'), + clientKey: Buffer.from('operator-key'), + }; + try { + const root = await OpenShellClient.connect(options); + expect(root.providers.transport).toBe(root.transport); + expect(root.providers.transport).toBe(root.sandbox.transport); + const standalone = await ProviderClient.connect(options); + expect(await standalone.getCredentials('provider', ['KEY'], { workspace: 'other' })).toEqual({ + KEY: { value: 'secret' }, + }); + expect(build).toHaveBeenCalledWith(options); + } finally { + build.mockRestore(); + } + }); +}); diff --git a/sdk/typescript/src/providers.ts b/sdk/typescript/src/providers.ts new file mode 100644 index 0000000000..1c2f9efa52 --- /dev/null +++ b/sdk/typescript/src/providers.ts @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { type Client, createClient, type Transport } from '@connectrpc/connect'; +import { fromConnect, SdkError } from './errors.js'; +import { OpenShell } from './gen/openshell_pb.js'; +import { buildTransport, type ConnectOptions } from './transport.js'; + +/** An exported runtime secret. Never log or serialize it into shared output. */ +export interface ProviderCredentialValue { + value: string; + /** Absent for non-expiring values; sub-millisecond precision is truncated. */ + expirationTime?: Date; +} + +export interface ProviderCredentialsOptions { + /** Required explicit workspace. There is no implicit default or all-workspaces export. */ + workspace: string; + /** Omitted or zero uses the gateway's five-minute margin. Maximum: 24 hours. */ + minimumRemainingLifetimeSecs?: number; + /** Abort delivery. An already-started gateway refresh can still commit. */ + signal?: AbortSignal; + /** Deadline for this RPC in milliseconds. */ + timeoutMs?: number; +} + +/** Provider operations over a shared transport; no sandbox is required. */ +export class ProviderClient { + readonly raw: Client; + readonly transport: Transport; + + constructor(transport: Transport) { + this.transport = transport; + this.raw = createClient(OpenShell, transport); + } + + /** + * Use direct gateway HTTPS with a trusted OU=operator client certificate/key + * and no OIDC or edge token. Operator authentication must be enabled at the gateway. + */ + static async connect(options: ConnectOptions): Promise { + return new ProviderClient(buildTransport(options)); + } + + /** + * Export exactly the selected runtime keys, or reject without a result. + * Requires direct operator mTLS. The gateway refreshes eligible missing or + * short-lived outputs, never exports private refresh inputs, and may commit + * refresh effects even if delivery fails. This helper does not retry or cache. + */ + async getCredentials( + name: string, + credentialKeys: readonly string[], + options: ProviderCredentialsOptions, + ): Promise> { + if (!options?.workspace || options.workspace.trim() !== options.workspace) { + throw new SdkError('invalid_config', 'an explicit canonical workspace is required'); + } + if (!name || name.trim() !== name || new TextEncoder().encode(name).byteLength > 253) { + throw new SdkError('invalid_config', 'a canonical provider name is required'); + } + // Copy before awaiting, so caller mutation cannot affect response validation. + const keys = [...credentialKeys]; + if ( + !Array.isArray(credentialKeys) || + keys.length < 1 || + keys.length > 32 || + keys.some((key) => key.length > 256 || key.trim() !== key || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) || + new Set(keys).size !== keys.length + ) { + throw new SdkError('invalid_config', 'select between 1 and 32 unique runtime environment keys'); + } + const lifetime = options.minimumRemainingLifetimeSecs; + if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime < 0 || lifetime > 86400)) { + throw new SdkError('invalid_config', 'minimumRemainingLifetimeSecs must be between zero and 24 hours'); + } + if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) { + throw new SdkError('invalid_config', 'timeoutMs must be finite and positive'); + } + // Round up to nanoseconds rather than silently weakening a fractional margin. + const nanos = lifetime === undefined ? undefined : Math.ceil(lifetime * 1e9); + try { + const response = await this.raw.getProviderCredentials( + { + workspaceScope: { selection: { case: 'workspace', value: options.workspace } }, + name, + credentialKeys: keys, + minimumRemainingLifetime: + nanos === undefined ? undefined : { seconds: BigInt(Math.floor(nanos / 1e9)), nanos: nanos % 1e9 }, + }, + { signal: options.signal, timeoutMs: options.timeoutMs }, + ); + const returned = Object.entries(response.credentials); + if (returned.length !== keys.length || returned.some(([key]) => !keys.includes(key))) { + throw new SdkError('rpc', 'gateway returned an unexpected credential selection'); + } + return Object.fromEntries( + returned.map(([key, credential]) => { + const expiration = credential.expirationTime; + // Keep an explicit Unix epoch and truncate rather than round up expiry. + const expirationTime = expiration + ? new Date(Number(expiration.seconds) * 1000 + Math.floor(expiration.nanos / 1e6)) + : undefined; + return [key, { value: credential.value, ...(expirationTime ? { expirationTime } : {}) }]; + }), + ); + } catch (error) { + throw fromConnect(error); + } + } +} diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index ad04f2ae23..418b67ec51 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -72,8 +72,9 @@ Common findings: - `No active gateway`: register one with `openshell gateway add `. - Connection refused: gateway process is not running, service exposure is wrong, or a port-forward/proxy is not active. - TLS/certificate errors: the endpoint scheme or trust chain is wrong, a CLI mTLS bundle does not match the gateway CA, a supervisor is missing the gateway CA, or TLS termination does not match the gateway listener. Workloads and supervisors should not contain a user TLS client certificate or private key. -- A Snap refresh restarts the gateway with its migrated mTLS config. The secure Snap gateway uses `https://127.0.0.1:17670` and requires a client bundle in the user's Snap state. Refresh replaces insecure configs without keeping a copy; follow the published Snap installation steps to re-register an old HTTP client. +- Fresh Snap installations use `openshell.user-gateway`; upgraded installations retain their system-owned state through `openshell.system-gateway`. Existing HTTPS registrations continue to work after the service rename. Follow the published Snap installation steps to enroll a new user, replace an old HTTP registration, or change modes safely through the `disable` maintenance state. If `gateway-mode=disable`, no local Snap gateway is expected to run. - `Unauthenticated` from an edge or OIDC gateway: refresh stored credentials with `openshell gateway login [name]`, then retry. Use `gateway logout` only when intentionally clearing local credentials. +- Operator credential retrieval requires direct gateway mTLS, `[openshell.gateway.mtls_auth] operator_enabled = true`, and a verified certificate with exact `OU=operator`. OIDC admin roles and forwarded certificate headers do not grant access. Do not combine the operator certificate with an `Authorization` header. Enabling this role grants gateway-wide administration; never repair a denial by issuing operator certificates to ordinary users. See the published gateway authentication and provider references. - A direct development endpoint with a private or self-signed certificate can be isolated with `--gateway-endpoint --gateway-insecure`; do not persist or recommend insecure verification for shared gateways. ### Step 2: Identify the Compute Platform @@ -1045,7 +1046,8 @@ credential failures. | Custom compute driver is unavailable | Driver process/socket missing, inaccessible, or selected name does not match its endpoint/config key | Socket ownership/mode, driver service logs, gateway `GetCapabilities` logs | | Sandbox remains `Stopping` or `Starting` | Driver stop/start failed, retained resource is missing, or a fresh supervisor has not connected | Gateway and driver logs; `docker inspect`, `podman inspect`, Agent Sandbox status/PVC, or VM state marker and launcher process | | Image pull failure | Gateway or sandbox image cannot be pulled | Runtime events and image pull credentials | -| Gateway API resources fail with `the server could not find the requested resource` | Optional Gateway API resources were applied without Envoy Gateway CRDs | Install Envoy Gateway and enable `grpcRoute` before applying the optional ingress resources | +| Gateway API resources fail with `the server could not find the requested resource` | Optional Gateway API resources were applied without Gateway API CRDs | Install the standard Gateway API CRDs and selected controller, then enable `grpcRoute` before applying the optional ingress resources | +| A custom GatewayClass does not program the chart-created Gateway, or its proxy Service conflicts with the OpenShell Service | The selected controller is unavailable, or the default `grpcRoute.gateway.name` reuses the OpenShell Service name | Check `kubectl get gatewayclass,gateway,grpcroute -A`; set a distinct Gateway name such as `-ingress`. For an existing Gateway, set `gateway.create=false` plus `gateway.name`, `gateway.namespace`, and optional `gateway.sectionName` | | HTTPS ingress (`grpcRoute.gateway.listener.protocol=HTTPS`) connection resets or TLS handshake hangs | Envoy terminates TLS but the gateway pod still expects TLS, so the plaintext backend hop fails | Set `server.disableTls=true` so Envoy forwards plaintext to the pod; verify the listener `certificateRefs` Secret exists in the release namespace and `openshell status` over `https://` | | With `grpcRoute.replicaRouting.enabled=true`, sandbox SSH, forward, or exec still relay through a peer replica | A `-replica-` Service has no endpoint, so Envoy returns `Unavailable` and the CLI retries unrouted | `kubectl -n openshell get endpoints -replica-0 -replica-1`; confirm `workload.kind=statefulset` and the GRPCRoute status is `Accepted`/`ResolvedRefs` | | HTTPS ingress returns `Unauthenticated` after connecting | TLS terminates at Envoy, so the gateway never sees a client cert; no OIDC issuer is configured for identity | Configure `server.oidc.issuer` and register with `openshell gateway add https:// --oidc-issuer `, or set `server.auth.allowUnauthenticatedUsers=true` for a trusted-proxy/dev cluster | diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index d626aaf1b3..db349e0068 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -30,15 +30,18 @@ The user's input falls into one of three tiers. Work with whatever the user prov The user provides API endpoints and a broad intent. No API docs needed. Examples: + - "Allow curl to hit api.github.com, read-only" - "Give claude full access to api.anthropic.com" - "Let /usr/bin/myapp talk to internal-svc:8080 but only for reading" This is sufficient for: + - **L4-only** policies (host:port + binary checks, no method or path rules) - **Preset-based L7** policies (`read-only`, `read-write`, `full` on all paths) For this tier, default to: + - `access: read-only` when the user says "read", "browse", "view", "query", "fetch" - `access: read-write` when the user says "read-write", "create", "update" (but not "delete") - `access: full` when the user says "full access", "everything", "unrestricted" @@ -53,8 +56,9 @@ For this tier, default to: The user knows some API paths but doesn't have full docs. Examples: + - "Allow GET on /api/v1/models and POST on /api/v1/completions at integrate.api.nvidia.com" -- "Read-only on /repos/** at api.github.com, but also allow POST on /repos/*/issues" +- "Read-only on /repos/** at api.github.com, but also allow POST on /repos/*/*/issues" Generate explicit `rules` for the known paths. If the user also wants broader access beyond the specific paths, combine with a catch-all rule or suggest a preset instead. @@ -121,11 +125,13 @@ Ask these when the user's intent is broad and more specificity is possible: When the user mentions a recognizable API host but hasn't provided docs, and the current tier is **Minimal**, attempt to upgrade to **Full** by searching for the API documentation online. **When to trigger:** + - The host is a well-known public API (e.g., `api.github.com`, `api.anthropic.com`, `api.openai.com`, `integrate.api.nvidia.com`, `api.stripe.com`, `api.slack.com`, `api.gitlab.com`) - The user has NOT already provided API docs - The user has NOT explicitly asked for a broad preset ("just read-only, nothing fancy") **How to do it:** + 1. Tell the user: "I can look up the REST API docs for [service] to help generate a more precise policy. Want me to do that?" 2. If the user agrees (or hasn't declined), search for the docs: - Search the web with a query like `"[service name] REST API documentation endpoints"` or `"[service name] OpenAPI spec"` @@ -134,6 +140,7 @@ When the user mentions a recognizable API host but hasn't provided docs, and the 4. Use the discovered endpoints to offer tighter scoping: "I found [N] endpoints in the [service] API. Based on your intent, I can narrow the policy to just [subset]. Want me to do that, or keep the broader preset?" **When to skip:** + - The user explicitly asked for a broad preset or said "don't bother with docs" - The API is internal, private, or not publicly documented - The host is not recognizable as a well-known service @@ -148,6 +155,7 @@ If the user confirms the policy must stay broad (they don't know the paths, need ### Iteration You may need to go back and forth a few times. Keep the loop tight: + 1. Ask one batch of clarifying questions (group related questions together) 2. Update your understanding based on the answer 3. If the answer reveals further scoping opportunities, ask a follow-up @@ -160,6 +168,7 @@ You may need to go back and forth a few times. Keep the loop tight: Read the published [policy schema reference](https://docs.nvidia.com/openshell/latest/how-it-works/policies/schema) before generating or changing a policy. Published documentation is the authority for the current schema; do not infer fields from examples in this skill. Key sections to reference: + - **Policy Schema Reference** — top-level structure - **`network_policies`** — rule structure - **`NetworkEndpoint`** fields — host, port, protocol, tls, enforcement, access, rules, allowed_ips @@ -230,15 +239,21 @@ Only needed for the **Moderate** and **Full** tiers. Translate API path paramete | API path | Glob pattern | |----------|-------------| | `/repos/{owner}/{repo}` | `/repos/*/*` | -| `/repos/{owner}/{repo}/issues` | `/repos/*/issues` | -| `/repos/{owner}/{repo}/issues/{id}` | `/repos/*/issues/*` | +| `/repos/{owner}/{repo}/issues` | `/repos/*/*/issues` | +| `/repos/{owner}/{repo}/issues/{id}` | `/repos/*/*/issues/*` | | `/api/v1/models/{model_id}/versions/{version}` | `/api/v1/models/*/versions/*` | | All sub-paths under `/api/v1/` | `/api/v1/**` | -Path matching uses the runtime `glob` engine. Both `*` and `**` may cross `/` -boundaries; `?` matches one character, and bracket classes such as `[0-9]` and -`[!0]` are supported. Prefer segment-shaped patterns such as -`/repos/*/issues` for readability, but do not rely on `*` to stop at `/`. +In REST rule paths, `*` stops at `/`, while a whole-segment `**` crosses `/`. +For example, `/api/v1/**` covers descendants but not `/api/v1` itself; add an +explicit rule for that path when needed. `?` matches one character except `/`, +and bracket classes such as `[0-9]` are supported. + +The endpoint `path` selector has separate semantics: `/api/v1/**` includes +`/api/v1` itself, and `*` in other patterns can cross `/`. These semantics apply +both to selecting the endpoint's configuration and to deciding which endpoints' +allow and deny rules apply. See the published policy schema's +[matcher semantics](https://docs.nvidia.com/openshell/latest/how-it-works/policies/schema#matcher-semantics). ### Building the Explicit Rules List @@ -532,6 +547,7 @@ Show the generated policy YAML with: ## Step 8: Confirm and Refine After presenting or applying the policy, ask if the user wants to: + - Tighten or loosen any rules - Add more endpoints or binaries - Switch between enforce/audit mode diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index f99b6cb7ed..472d7fadbb 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -242,13 +242,23 @@ When OAuth refresh fails, inspect the `RECOVERY` and `FAILURE_CODE` columns from `LAST_ERROR`. `retry` means the worker will try again, `reauthorize` means the user must obtain a new OAuth grant and run `provider refresh configure`, `fix_configuration` means an operator must repair the OAuth client, scopes, or -administrator policy, and `investigate` means the issuer returned an -unrecognized response. The gateway parks `reauthorize` records until a manual -rotate or reconfiguration. It retries +administrator policy, and `investigate` can mean an unrecognized issuer +response or an interrupted mint with an uncertain outcome. The gateway parks +`reauthorize` records until a manual rotate or reconfiguration. It retries `fix_configuration` records hourly so externally repaired configuration can recover without rapid token-endpoint traffic. The existing access credential remains usable only until its recorded expiry. +`refresh_in_progress` and `refresh_committing` can indicate active work. Check +gateway health and refresh status again before treating them as failures; +concurrent refresh requests wait for the active mint. If a gateway interruption +leaves either marker behind and rotation reports an uncertain previous mint, +the gateway will not reuse the grant automatically. Investigate the issuer state, +then run `provider refresh configure` with a valid grant. Obtain a new OAuth +grant first if the previous refresh token may have been consumed. Do not clear +the marker directly or repeatedly rotate it; reconfiguration revokes old +workload handles, so restart processes holding them. + --- ## Workflow 3: Sandbox Lifecycle diff --git a/snap/hooks/configure b/snap/hooks/configure new file mode 100755 index 0000000000..6a3b00fa38 --- /dev/null +++ b/snap/hooks/configure @@ -0,0 +1,32 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Apply the configured gateway service model. The disabled mode provides a +# safe intermediate state while changing between user and system gateways. + +set -eu + +gateway_mode=$(snapctl get gateway-mode) +case "$gateway_mode" in + user) + snapctl stop --disable "${SNAP_INSTANCE_NAME}.system-gateway" + snapctl start --enable "${SNAP_INSTANCE_NAME}.user-gateway" + ;; + system) + snapctl stop --disable "${SNAP_INSTANCE_NAME}.user-gateway" + snapctl start --enable "${SNAP_INSTANCE_NAME}.system-gateway" + ;; + disable) + snapctl stop --disable "${SNAP_INSTANCE_NAME}.user-gateway" + snapctl stop --disable "${SNAP_INSTANCE_NAME}.system-gateway" + ;; + '') + echo "openshell: gateway-mode is not configured" >&2 + exit 1 + ;; + *) + echo "openshell: unsupported gateway-mode: ${gateway_mode}" >&2 + exit 1 + ;; +esac diff --git a/snap/hooks/install b/snap/hooks/install new file mode 100755 index 0000000000..22b40425f3 --- /dev/null +++ b/snap/hooks/install @@ -0,0 +1,10 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Fresh installations use the per-user gateway. The configure hook applies the +# service selection after this transaction commits. + +set -eu + +snapctl set gateway-mode=user diff --git a/snap/hooks/post-refresh b/snap/hooks/post-refresh index 14fbe5d482..869a80281c 100755 --- a/snap/hooks/post-refresh +++ b/snap/hooks/post-refresh @@ -2,29 +2,35 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Remove insecure gateway configs on refresh, then restart the gateway so the -# default config takes effect even when the previous revision used -# refresh-mode: endure and kept its plaintext gateway running. +# One-time migration for installations that predate per-user snap gateways. +# Existing installations retain the system gateway; fresh installations record +# gateway-mode=user in the install hook and skip this migration. The configure +# hook applies the selected service after this transaction commits. set -eu +gateway_mode=$(snapctl get gateway-mode) +case "$gateway_mode" in + user | system | disable) + exit 0 + ;; + '') + ;; + *) + echo "openshell: unsupported gateway-mode: ${gateway_mode}" >&2 + exit 1 + ;; +esac + config_file="${SNAP_COMMON}/gateway.toml" insecure='^[[:space:]]*(allow_unauthenticated_users|disable_tls)[[:space:]]*=[[:space:]]*true([[:space:]#]|$)' -# Keep secure operator configs, symlinks, and directories. Remove a config -# that explicitly allows plaintext or anonymous access, even if it has other -# edits. -if [ ! -e "$config_file" ]; then - exit 0 -elif [ -L "$config_file" ]; then - exit 0 -elif [ -f "$config_file" ]; then - grep -Eq "$insecure" "$config_file" || exit 0 +# Keep secure operator configs and directories. Remove a regular config, or +# only the symlink to one, when it explicitly allows plaintext or anonymous +# access. Never modify a symlink target. +if [ -f "$config_file" ] && grep -Eq "$insecure" "$config_file"; then echo "openshell: removing insecure gateway config to use the mTLS default" >&2 -elif [ -e "$config_file" ]; then - exit 0 + rm -f "$config_file" fi -rm -f "$config_file" - -snapctl restart "${SNAP_INSTANCE_NAME}.gateway" +snapctl set gateway-mode=system diff --git a/snapcraft.yaml b/snapcraft.yaml index 7a7340d857..8eab454c26 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -13,7 +13,7 @@ description: | The OpenShell snap ships a CLI (`openshell`), a terminal UI (`openshell.term`), a standalone policy prover (`openshell.prover`), and a - managed gateway daemon (`openshell.gateway`). + managed gateway daemon (`openshell.user-gateway`). **Setup instructions** @@ -23,26 +23,48 @@ description: | Snap Store installations automatically connect the required interfaces. - If the OpenShell snap was installed before Docker was running, it may - have hit the systemd start limit when trying to repeatedly start and - connect to docker. If this is the case, it can be resolved by doing: + If the OpenShell snap was installed before Docker was running, its user + service may have hit the systemd start limit. Resolve this from the + affected user's session after Docker is available: - sudo systemctl reset-failed snap.openshell.gateway.service - sudo snap restart openshell.gateway + systemctl --user reset-failed snap.openshell.user-gateway.service + snap start --user openshell.user-gateway - 2. Give your user the gateway client certificate and register the gateway. - The gateway requires mTLS; only users holding this certificate can use - it, so copy it only for trusted users: + 2. Register the per-user gateway after it provisions its mTLS credentials: - snap services openshell.gateway - d=~/snap/openshell/common/.local/state/openshell/tls - mkdir -p -m 700 "$d" "$d/client" - sudo install -o "$USER" -m 600 /var/snap/openshell/common/tls/ca.crt "$d/" - sudo install -o "$USER" -m 600 -t "$d/client" \ - /var/snap/openshell/common/tls/client/tls.crt /var/snap/openshell/common/tls/client/tls.key + snap services openshell.user-gateway openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status + **Legacy upgrades** + + Installations upgraded from the former `openshell.gateway` service retain + their state through `openshell.system-gateway`. Existing HTTPS registrations + continue to work because the endpoint and TLS identity do not change. + + If this user still has an old HTTP registration, or has not previously been + enrolled, wait for `openshell.system-gateway` to be active, copy its client + credentials, and register its HTTPS endpoint: + + snap services openshell.system-gateway + d=~/snap/openshell/common/.local/state/openshell/tls + mkdir -p -m 700 "$d" "$d/client" + sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/ca.crt "$d/" + sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.crt "$d/client/" + sudo install -o "$USER" -g "$(id -gn)" -m 600 \ + /var/snap/openshell/common/tls/client/tls.key "$d/client/" + openshell gateway list + old_registration="NAME_FROM_GATEWAY_LIST" + openshell gateway remove "$old_registration" + openshell gateway add https://127.0.0.1:17670 --local --name openshell + openshell status + + The user and system gateways have separate state. See the OpenShell + installation guide before changing `gateway-mode`; use `gateway-mode=disable` + to stop both services while preserving the current TLS directory. + base: core24 grade: stable confinement: strict @@ -92,23 +114,45 @@ apps: - openshell-prover plugs: - home - gateway: + system-gateway: command: bin/openshell-gateway-wrapper daemon: simple + daemon-scope: system + install-mode: disable # refresh-mode: endure prevents snapd from restarting the gateway daemon # during snap refreshes, which would kill active sandbox sessions. - # Operators must manually restart the service after a refresh if needed. + # The installer restarts the selected service after an explicit refresh. refresh-mode: endure - # Snapd runs this daemon as root. The wrapper serves TLS from the bundle + # This compatibility daemon runs as root. The wrapper serves TLS from the bundle # generated in $SNAP_COMMON/tls, and the default config requires client - # certificates; the installer copies the client bundle to the target - # user. The wrapper uses $SNAP_COMMON/gateway.db. Before startup it + # certificates; the legacy installer path copies the client bundle to the + # target user. The wrapper uses $SNAP_COMMON/gateway.db. Before startup it # bootstraps package-managed credentials and validates the selected # operator-provided config without creating or rewriting it. A nonempty # OPENSHELL_GATEWAY_CONFIG takes precedence over gateway.toml. environment: XDG_DATA_HOME: "$SNAP_COMMON" - XDG_RUNTIME_DIR: "$SNAP_COMMON" + OPENSHELL_SNAP_CONFIG_FILE: "$SNAP_COMMON/gateway.toml" + OPENSHELL_DB_URL: "sqlite:$SNAP_COMMON/gateway.db?mode=rwc" + OPENSHELL_LOCAL_TLS_DIR: "$SNAP_COMMON/tls" + plugs: + - docker + - log-observe + - network + - network-bind + - system-observe + user-gateway: + command: bin/openshell-gateway-wrapper + daemon: simple + daemon-scope: user + install-mode: disable + refresh-mode: endure + # Use persistent user-owned XDG roots shared with the Snap CLI and TUI. + # Snapd provides the ephemeral XDG_RUNTIME_DIR automatically. + environment: + XDG_CONFIG_HOME: "$SNAP_USER_COMMON/.config" + XDG_DATA_HOME: "$SNAP_USER_COMMON/.local/share" + XDG_STATE_HOME: "$SNAP_USER_COMMON/.local/state" plugs: - docker - log-observe diff --git a/tasks/scripts/snap-gateway-wrapper.sh b/tasks/scripts/snap-gateway-wrapper.sh index a047cf72e3..38ac1f6a9c 100755 --- a/tasks/scripts/snap-gateway-wrapper.sh +++ b/tasks/scripts/snap-gateway-wrapper.sh @@ -2,68 +2,28 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Snap wrapper for openshell-gateway. Sets snap-specific defaults: -# - OPENSHELL_DB_URL -> sqlite:$SNAP_COMMON/gateway.db (overridable) -# - OPENSHELL_LOCAL_TLS_DIR -> $SNAP_COMMON/tls (overridable) -# The gateway serves TLS from the generated bundle and requires client -# certificates. It bootstraps package-managed credentials and validates, but -# never creates or rewrites, an operator-provided config before starting. +# Snap wrapper for openshell-gateway. The system service supplies explicit +# compatibility paths; the user service uses OpenShell's XDG defaults. set -eu -CANONICAL_CONFIG_FILE="${SNAP_COMMON}/gateway.toml" -export OPENSHELL_DB_URL="${OPENSHELL_DB_URL:-sqlite:${SNAP_COMMON}/gateway.db?mode=rwc}" -export OPENSHELL_LOCAL_TLS_DIR="${OPENSHELL_LOCAL_TLS_DIR:-${SNAP_COMMON}/tls}" +if [ -z "${OPENSHELL_GATEWAY_CONFIG:-}" ] \ + && [ -n "${OPENSHELL_SNAP_CONFIG_FILE:-}" ] \ + && { [ -e "$OPENSHELL_SNAP_CONFIG_FILE" ] || [ -L "$OPENSHELL_SNAP_CONFIG_FILE" ]; } +then + export OPENSHELL_GATEWAY_CONFIG="$OPENSHELL_SNAP_CONFIG_FILE" +fi -# Mirror clap's CLI-over-environment precedence so preflight always inspects -# the same file the daemon will load. Reject ambiguous duplicate selectors -# before either command runs. -cli_config="" -config_seen=false -expect_config_path=false -options_done=false -for argument in "$@"; do - if [ "$options_done" = true ]; then - continue - fi - if [ "$expect_config_path" = true ]; then - case "$argument" in - -*) - echo "openshell-gateway: --config requires a nonempty path" >&2 - exit 2 - ;; - esac - if [ "$config_seen" = true ]; then - echo "openshell-gateway: duplicate --config option" >&2 - exit 2 - fi - cli_config=$argument - config_seen=true - expect_config_path=false - continue +if [ -z "${OPENSHELL_LOCAL_TLS_DIR:-}" ]; then + if [ -z "${XDG_STATE_HOME:-}" ]; then + echo "openshell-gateway: OPENSHELL_LOCAL_TLS_DIR or XDG_STATE_HOME is required" >&2 + exit 1 fi - case "$argument" in - --) - options_done=true - ;; - --config) - expect_config_path=true - ;; - --config=*) - if [ "$config_seen" = true ]; then - echo "openshell-gateway: duplicate --config option" >&2 - exit 2 - fi - cli_config=${argument#--config=} - config_seen=true - ;; - esac -done -if [ "$expect_config_path" = true ] || { [ "$config_seen" = true ] && [ -z "$cli_config" ]; }; then - echo "openshell-gateway: --config requires a nonempty path" >&2 - exit 2 + export OPENSHELL_LOCAL_TLS_DIR="${XDG_STATE_HOME}/openshell/tls" fi +"${SNAP}/bin/openshell-gateway" config preflight -- "$@" + # Generate the local TLS bundle and the JWT bundle used for launch-scoped # supervisor credentials; generate-certs is idempotent and preserves an # existing bundle. @@ -71,16 +31,4 @@ fi --output-dir "$OPENSHELL_LOCAL_TLS_DIR" \ --server-san host.openshell.internal -if [ "$config_seen" = true ]; then - "${SNAP}/bin/openshell-gateway" config preflight -- "$@" - exec "${SNAP}/bin/openshell-gateway" "$@" -elif [ -n "${OPENSHELL_GATEWAY_CONFIG:-}" ]; then - "${SNAP}/bin/openshell-gateway" config preflight -- "$@" - exec "${SNAP}/bin/openshell-gateway" "$@" -elif [ -e "$CANONICAL_CONFIG_FILE" ] || [ -L "$CANONICAL_CONFIG_FILE" ]; then - "${SNAP}/bin/openshell-gateway" config preflight -- --config "$CANONICAL_CONFIG_FILE" "$@" - exec "${SNAP}/bin/openshell-gateway" --config "$CANONICAL_CONFIG_FILE" "$@" -else - "${SNAP}/bin/openshell-gateway" config preflight -- "$@" - exec "${SNAP}/bin/openshell-gateway" "$@" -fi +exec "${SNAP}/bin/openshell-gateway" "$@" diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index c0a42b6700..34f84ddd72 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -283,14 +283,33 @@ assert_snap_install_flow() { case "${1:-}:${2:-}" in list:docker) return 1 ;; list:openshell) [ "$openshell_present" = "1" ] ;; + get:openshell) printf '%s\n' system ;; *) command snap "$@" ;; esac } - as_root() { printf 'root:%s\n' "$*"; } + as_root() { + if [ "${1:-}:${2:-}" = snap:get ]; then + shift + snap "$@" + else + printf 'root:%s\n' "$*" + fi + } + as_target_user() { printf 'target:%s\n' "$*"; } set_linux_target_runtime_dir() { :; } wait_for_docker_daemon() { printf '%s\n' "wait:docker"; } - register_snap_gateway() { printf '%s\n' "register:gateway"; } - wait_for_snap_gateway_listener() { printf '%s\n' "wait:gateway-listener"; } + wait_for_user_docker_daemon() { printf '%s\n' "wait:user-docker"; } + snap_gateway_mode() { + if [ "$openshell_present" = 1 ]; then + printf '%s\n' system + else + printf '%s\n' user + fi + } + register_system_snap_gateway() { printf '%s\n' "register:system-gateway"; } + register_user_snap_gateway() { printf '%s\n' "register:user-gateway"; } + wait_for_system_snap_gateway_listener() { printf '%s\n' "wait:system-gateway-listener"; } + wait_for_user_snap_gateway_listener() { printf '%s\n' "wait:user-gateway-listener"; } wait_for_local_gateway_status() { printf '%s\n' "wait:gateway-status"; } info() { :; } export TARGET_USER=test-user @@ -308,26 +327,118 @@ assert_snap_install_flow() { fi } +assert_snap_user_refresh_flow() { + local calls + ( + has_cmd() { + case "$1" in + snap | docker) return 0 ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac + } + snap() { + case "${1:-}:${2:-}" in + list:docker) return 1 ;; + list:openshell) return 0 ;; + get:openshell) printf '%s\n' user ;; + *) command snap "$@" ;; + esac + } + as_root() { + if [ "${1:-}:${2:-}" = snap:get ]; then + shift + snap "$@" + else + printf 'root:%s\n' "$*" + fi + } + as_target_user() { printf 'target:%s\n' "$*"; } + set_linux_target_runtime_dir() { :; } + wait_for_user_docker_daemon() { printf '%s\n' "wait:user-docker"; } + snap_gateway_mode() { printf '%s\n' user; } + wait_for_user_snap_gateway_listener() { printf '%s\n' "wait:user-gateway-listener"; } + register_user_snap_gateway() { printf '%s\n' "register:user-gateway"; } + wait_for_local_gateway_status() { printf '%s\n' "wait:gateway-status"; } + info() { :; } + export TARGET_USER=test-user + install_linux_snap + ) >"$out" + calls="$(cat "$out")" + expected="wait:user-docker +root:snap refresh openshell --channel=latest/stable +root:snap restart openshell.user-gateway +wait:user-gateway-listener +register:user-gateway +wait:gateway-status" + if [ "$calls" != "$expected" ]; then + echo "FAIL: existing user-mode Snap refresh used the wrong service" >&2 + printf 'Expected:\n%s\nActual:\n%s\n' "$expected" "$calls" >&2 + exit 1 + fi +} + assert_snap_install_flow \ "existing Docker is reused" \ 1 0 "" \ - "wait:docker + "wait:user-docker root:snap install openshell --channel=latest/stable -root:snap restart openshell.gateway -wait:gateway-listener -register:gateway +wait:user-gateway-listener +register:user-gateway wait:gateway-status" +assert_snap_user_refresh_flow + assert_snap_install_flow \ "existing OpenShell snap is refreshed" \ 1 1 "" \ "wait:docker root:snap refresh openshell --channel=latest/stable -root:snap restart openshell.gateway -wait:gateway-listener -register:gateway +root:snap restart openshell.system-gateway +wait:system-gateway-listener +register:system-gateway wait:gateway-status" +if ( + has_cmd() { + case "$1" in + snap) return 0 ;; + docker) return 1 ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac + } + snap() { + case "${1:-}:${2:-}" in + list:docker) return 1 ;; + list:openshell) return 0 ;; + get:openshell) printf '%s\n' disable ;; + *) echo "FAIL: disabled mode reached snap command: $*" >&2; return 99 ;; + esac + } + as_root() { + if [ "${1:-}:${2:-}" = snap:get ]; then + shift + snap "$@" + else + echo "FAIL: disabled mode reached root command: $*" >&2 + return 99 + fi + } + set_linux_target_runtime_dir() { :; } + install_linux_snap +) >"$out" 2>"$err"; then + echo "FAIL: disabled Snap gateway mode should block installation" >&2 + exit 1 +fi +if ! grep -Fq "OpenShell snap gateway mode is disabled" "$err"; then + echo "FAIL: disabled Snap gateway mode missing recovery guidance" >&2 + cat "$err" >&2 || true + exit 1 +fi +if grep -Fq "disabled mode reached" "$err"; then + cat "$err" >&2 || true + exit 1 +fi + assert_snap_install_rejected() { local name=$1 local docker_present=$2 @@ -386,7 +497,11 @@ root_probes_file="${tmpdir}/docker-root-probes" printf '0\n' >"$attempts_file" printf '0\n' >"$root_probes_file" if ! ( - docker() { + env() { + if [ "$*" != "-u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info" ]; then + echo "unexpected Docker readiness probe: $*" >&2 + return 1 + fi attempts="$(cat "$attempts_file")" attempts=$((attempts + 1)) printf '%s\n' "$attempts" >"$attempts_file" @@ -415,7 +530,7 @@ if [ "$(cat "$root_probes_file")" != "3" ]; then fi if ( - docker() { return 1; } + env() { return 1; } as_root() { "$@"; } snap() { return 1; } sleep() { :; } @@ -425,12 +540,69 @@ if ( echo "FAIL: Docker readiness timeout should fail" >&2 exit 1 fi -if ! grep -Fq "Docker daemon did not become reachable within 2s" "$err"; then +if ! grep -Fq "Docker daemon at /var/run/docker.sock did not become reachable within 2s" "$err"; then echo "FAIL: missing Docker readiness timeout message" >&2 cat "$err" >&2 || true exit 1 fi +user_attempts_file="${tmpdir}/docker-user-attempts" +user_probes_file="${tmpdir}/docker-user-probes" +printf '0\n' >"$user_attempts_file" +printf '0\n' >"$user_probes_file" +if ! ( + env() { + if [ "$*" != "-u DOCKER_HOST -u DOCKER_CONTEXT docker --host unix:///var/run/docker.sock info" ]; then + echo "unexpected user Docker readiness probe: $*" >&2 + return 1 + fi + attempts="$(cat "$user_attempts_file")" + attempts=$((attempts + 1)) + printf '%s\n' "$attempts" >"$user_attempts_file" + [ "$attempts" -ge 3 ] + } + as_target_user() { + user_probes="$(cat "$user_probes_file")" + printf '%s\n' "$((user_probes + 1))" >"$user_probes_file" + "$@" + } + sleep() { :; } + info() { :; } + TARGET_USER=test-user + export DOCKER_HOST=tcp://remote.example:2376 + export DOCKER_CONTEXT=remote + OPENSHELL_INSTALL_DOCKER_TIMEOUT=3 wait_for_user_docker_daemon +) >"$out" 2>"$err"; then + echo "FAIL: user Docker readiness should succeed after retries" >&2 + cat "$err" >&2 || true + exit 1 +fi +if [ "$(cat "$user_attempts_file")" != "3" ]; then + echo "FAIL: user Docker readiness did not retry three times" >&2 + exit 1 +fi +if [ "$(cat "$user_probes_file")" != "3" ]; then + echo "FAIL: user Docker readiness probes did not run through as_target_user" >&2 + exit 1 +fi + +if ( + env() { return 1; } + as_target_user() { "$@"; } + sleep() { :; } + info() { :; } + TARGET_USER=test-user + OPENSHELL_INSTALL_DOCKER_TIMEOUT=2 wait_for_user_docker_daemon +) >"$out" 2>"$err"; then + echo "FAIL: user Docker readiness timeout should fail" >&2 + exit 1 +fi +if ! grep -Fq "Docker daemon at /var/run/docker.sock did not become reachable as test-user within 2s" "$err"; then + echo "FAIL: missing user Docker readiness timeout message" >&2 + cat "$err" >&2 || true + exit 1 +fi + if ! grep -Fq '/snap/bin/openshell' "${ROOT}/install.sh"; then echo "FAIL: Snap installs must use the Snap CLI explicitly" >&2 exit 1 @@ -445,7 +617,7 @@ if ! ( info() { :; } TARGET_USER=test-user snap_gateway_uses_mtls() { return 0; } - register_snap_gateway + register_system_snap_gateway ) >"$out" 2>"$err"; then echo "FAIL: Snap gateway registration should succeed" >&2 cat "$err" >&2 || true @@ -465,7 +637,7 @@ if ! ( copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; } print_gateway_add_output() { :; } snap_gateway_uses_mtls() { return 1; } - register_snap_gateway + register_system_snap_gateway ) >"$out" 2>"$err"; then echo "FAIL: legacy plaintext Snap gateway registration should succeed" >&2 cat "$err" >&2 || true @@ -494,7 +666,7 @@ assert_snap_listener_probe() { snap_gateway_uses_mtls() { [ "$uses_mtls" = "1" ]; } info() { :; } OPENSHELL_SNAP_TLS_DIR=/tls - wait_for_snap_gateway_listener >/dev/null + wait_for_system_snap_gateway_listener >/dev/null printf '%s\n' "$_last_output" )" if [ "$actual" != "$expected" ]; then @@ -630,6 +802,22 @@ if [ "$(run_listener_wait "$restarting_unit")" != "5" ]; then exit 1 fi +: >"$registration_calls_file" +if ! ( + as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; } + print_gateway_add_output() { :; } + register_user_snap_gateway +) >"$out" 2>"$err"; then + echo "FAIL: user Snap gateway registration should succeed" >&2 + cat "$err" >&2 || true + exit 1 +fi +if [[ $(cat "$registration_calls_file") != "target:/snap/bin/openshell gateway add https://127.0.0.1:17670 --local --name openshell" ]]; then + echo "FAIL: user Snap gateway registration must use its own local TLS bundle" >&2 + cat "$registration_calls_file" >&2 + exit 1 +fi + if [ "$(PLATFORM=darwin local_gateway_endpoint)" != "https://localhost:17670" ]; then echo "FAIL: macOS local gateway endpoint must use a TLS-compatible loopback hostname" >&2 exit 1 diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 330fb565ab..cf221fbee3 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -119,7 +119,8 @@ snapcraft="${ROOT}/snapcraft.yaml" snap_workflow="${ROOT}/.github/workflows/snap-package.yml" snap_install_docs="${ROOT}/docs/about/installation.mdx" snap_canary="${ROOT}/.github/workflows/release-canary.yml" -snap_repro="${ROOT}/nix/test-guest/scripts/snap-gateway-repro.sh" +snap_configure_hook="${ROOT}/snap/hooks/configure" +snap_install_hook="${ROOT}/snap/hooks/install" snap_post_refresh_hook="${ROOT}/snap/hooks/post-refresh" package_deb="${ROOT}/tasks/scripts/package-deb.sh" assert_file_exists "$snap_wrapper" @@ -127,18 +128,18 @@ assert_file_exists "$snapcraft" assert_file_exists "$snap_workflow" assert_file_exists "$snap_install_docs" assert_file_exists "$snap_canary" -assert_file_exists "$snap_repro" +assert_file_exists "$snap_configure_hook" +assert_file_exists "$snap_install_hook" assert_file_exists "$snap_post_refresh_hook" assert_file_exists "$package_deb" assert_contains "$service" "ExecStartPre=/usr/bin/openshell-gateway config preflight" assert_contains "$package_deb" "\$src_dir/openshell-gateway.service" assert_contains "$package_deb" "\$pkgroot/usr/lib/systemd/user/openshell-gateway.service" -assert_contains "$snap_wrapper" "if [ -n \"\${OPENSHELL_GATEWAY_CONFIG:-}\" ]; then" -assert_contains \ - "$snap_wrapper" \ - "elif [ -e \"\$CANONICAL_CONFIG_FILE\" ] || [ -L \"\$CANONICAL_CONFIG_FILE\" ]; then" -assert_contains "$snap_wrapper" "config preflight -- --config \"\$CANONICAL_CONFIG_FILE\" \"\$@\"" -assert_not_contains "$snap_wrapper" "[ -f \"\$CANONICAL_CONFIG_FILE\" ]" +assert_contains "$snap_wrapper" '[ -z "${OPENSHELL_GATEWAY_CONFIG:-}" ]' +assert_contains "$snap_wrapper" '[ -e "$OPENSHELL_SNAP_CONFIG_FILE" ] || [ -L "$OPENSHELL_SNAP_CONFIG_FILE" ]' +assert_contains "$snap_wrapper" 'export OPENSHELL_GATEWAY_CONFIG="$OPENSHELL_SNAP_CONFIG_FILE"' +assert_contains "$snap_wrapper" 'config preflight -- "$@"' +assert_not_contains "$snap_wrapper" "CANONICAL_CONFIG_FILE" bash "$ROOT/tasks/scripts/test-snap-gateway-wrapper.sh" "$snap_wrapper" # Store installs autoconnect all required interfaces and require snapd 2.76 for @@ -149,7 +150,8 @@ for snap_file in \ "$snapcraft" \ "$snap_install_docs" \ "$snap_canary" \ - "$snap_repro" \ + "$snap_configure_hook" \ + "$snap_install_hook" \ "$snap_post_refresh_hook"; do assert_not_contains "$snap_file" "docker:docker-daemon" assert_not_contains "$snap_file" "default-provider: docker" @@ -158,16 +160,39 @@ if [[ -e "${ROOT}/snap/hooks/connect-plug-docker" ]]; then echo "FAIL: obsolete Snap Docker connection hook must not exist" >&2 exit 1 fi -if [[ -e "${ROOT}/snap/hooks/install" ]]; then - echo "FAIL: obsolete Snap install hook must not exist" >&2 +if [[ ! -x "$snap_install_hook" ]]; then + echo "FAIL: Snap install hook must be executable" >&2 + exit 1 +fi +if [[ ! -x "$snap_configure_hook" ]]; then + echo "FAIL: Snap configure hook must be executable" >&2 + exit 1 +fi +assert_contains "$snapcraft" 'daemon-scope: system' +assert_contains "$snapcraft" 'daemon-scope: user' +assert_contains "$snapcraft" 'install-mode: disable' +assert_not_contains "$snapcraft" 'install-mode: enable' +if grep -Eq '^ gateway:$' "$snapcraft"; then + echo "FAIL: removed Snap gateway app must not remain declared" >&2 exit 1 fi +assert_contains "$snapcraft" ' system-gateway:' +assert_contains "$snapcraft" ' user-gateway:' +assert_contains "$snapcraft" 'OPENSHELL_SNAP_CONFIG_FILE: "$SNAP_COMMON/gateway.toml"' +assert_contains "$snapcraft" 'OPENSHELL_DB_URL: "sqlite:$SNAP_COMMON/gateway.db?mode=rwc"' +assert_contains "$snapcraft" 'OPENSHELL_LOCAL_TLS_DIR: "$SNAP_COMMON/tls"' +assert_not_contains "$snapcraft" 'XDG_RUNTIME_DIR:' +assert_not_contains "$snapcraft" 'OPENSHELL_SNAP_CONFIG_FILE: "$SNAP_USER_COMMON' +assert_not_contains "$snapcraft" 'OPENSHELL_DB_URL: "sqlite:$SNAP_USER_COMMON' +assert_not_contains "$snapcraft" 'OPENSHELL_LOCAL_TLS_DIR: "$SNAP_USER_COMMON' assert_contains "$snapcraft" 'refresh-mode: endure' if [[ ! -x "$snap_post_refresh_hook" ]]; then echo "FAIL: Snap post-refresh hook must be executable" >&2 exit 1 fi assert_not_contains "$ROOT/tasks/scripts/snap-gateway-wrapper.sh" 'OPENSHELL_DISABLE_TLS' +bash "$ROOT/tasks/scripts/test-snap-configure-hook.sh" "$snap_configure_hook" +bash "$ROOT/tasks/scripts/test-snap-install-hook.sh" "$snap_install_hook" bash "$ROOT/tasks/scripts/test-snap-post-refresh-hook.sh" "$snap_post_refresh_hook" assert_contains "$snap_workflow" 'name: openshell-prover-${{ matrix.rust_arch }}-unknown-linux-musl' assert_contains "$snap_workflow" 'chmod +x prebuilt/prover/openshell-prover' @@ -193,21 +218,29 @@ assert_not_contains "$snap_install_docs" "snap connect openshell:home" assert_not_contains "$snap_install_docs" "snap connect openshell:network" assert_not_contains "$snap_install_docs" "snap connect openshell:network-bind" assert_contains "$snap_install_docs" "snap connect openshell:docker :docker" -assert_contains "$snap_install_docs" "systemctl reset-failed snap.openshell.gateway.service" -assert_contains "$snap_install_docs" "snap restart openshell.gateway" +assert_contains "$snap_install_docs" "systemctl --user reset-failed snap.openshell.user-gateway.service" +assert_contains "$snap_install_docs" "snap start --user openshell.user-gateway" +assert_contains "$snapcraft" "snap start --user openshell.user-gateway" +assert_contains "$snap_install_docs" "openshell.user-gateway" +assert_contains "$snap_install_docs" "openshell.system-gateway" +assert_contains "$snap_install_docs" "Existing HTTPS registrations continue to work" +assert_contains "$snapcraft" "Existing HTTPS registrations" +assert_contains "$snap_install_docs" "/var/snap/openshell/common/tls" +assert_contains "$snap_install_docs" 'old_registration="NAME_FROM_GATEWAY_LIST"' +assert_contains "$snap_install_docs" 'openshell gateway remove "$old_registration"' +assert_contains "$snap_install_docs" "openshell gateway add https://127.0.0.1:17670 --local --name openshell" +assert_contains "$snap_install_docs" "sudo snap set openshell gateway-mode=disable" +assert_contains "$snap_install_docs" 'tls-backup-$(date +%s%N)' +assert_contains "$snap_install_docs" "sudo snap set openshell gateway-mode=user" +assert_contains "$snap_install_docs" "sudo snap set openshell gateway-mode=system" +assert_contains "$snapcraft" "gateway-mode=disable" +assert_not_contains "$snap_install_docs" "snap services --user" assert_contains "$snap_canary" "install.sh | sh" assert_contains "$snap_canary" "ubuntu-snap-system-docker:" assert_contains "$snap_canary" "ubuntu-snap-docker-preflight:" assert_contains "$snap_canary" "openshell.prover check" -assert_contains "$snap_repro" 'OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}"' -assert_contains "$snap_repro" "/snap/bin/openshell.prover check" -assert_contains "$snap_repro" "system-docker" -assert_contains "$snap_repro" "missing-docker" -assert_contains "$snap_repro" "docker-snap" assert_not_contains "$snap_canary" "--dangerous" -assert_not_contains "$snap_repro" "--dangerous" assert_not_contains "$snap_canary" "snap connect openshell:docker" -assert_not_contains "$snap_repro" "snap connect openshell:docker" if ! awk '/config preflight/ { seen = 1 } /generate-certs/ { exit !seen }' "$service"; then echo "FAIL: Debian preflight must precede certificate generation" >&2 exit 1 diff --git a/tasks/scripts/test-snap-configure-hook.sh b/tasks/scripts/test-snap-configure-hook.sh new file mode 100755 index 0000000000..50c8f98763 --- /dev/null +++ b/tasks/scripts/test-snap-configure-hook.sh @@ -0,0 +1,76 @@ +#!/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 + +hook=${1:?Usage: test-snap-configure-hook.sh } +work=$(mktemp -d "${TMPDIR:-/tmp}/openshell snap configure hook.XXXXXX") +trap 'rm -rf "$work"' EXIT + +run_hook() { + local mode=$1 + rm -f "$work/snapctl.log" + mkdir -p "$work/bin" + cat >"$work/bin/snapctl" <>'$work/snapctl.log' +EOF + chmod 755 "$work/bin/snapctl" + PATH="$work/bin:$PATH" SNAP_INSTANCE_NAME=openshell "$hook" +} + +run_hook user +expected=$'stop --disable openshell.system-gateway\nstart --enable openshell.user-gateway' +[[ $(cat "$work/snapctl.log") == "$expected" ]] + +run_hook system +expected=$'stop --disable openshell.user-gateway\nstart --enable openshell.system-gateway' +[[ $(cat "$work/snapctl.log") == "$expected" ]] + +run_hook disable +expected=$'stop --disable openshell.user-gateway\nstop --disable openshell.system-gateway' +[[ $(cat "$work/snapctl.log") == "$expected" ]] + +for mode in '' invalid; do + if run_hook "$mode" >"$work/out" 2>"$work/err"; then + echo "FAIL: configure hook accepted gateway mode '${mode}'" >&2 + exit 1 + fi + [[ ! -e "$work/snapctl.log" ]] +done +grep -Fq 'unsupported gateway-mode: invalid' "$work/err" + +mkdir -p "$work/failure-bin" +cat >"$work/failure-bin/snapctl" <>'$work/failure.log' +[ "\${1:-}" != stop ] +EOF +chmod 755 "$work/failure-bin/snapctl" +if PATH="$work/failure-bin:$PATH" SNAP_INSTANCE_NAME=openshell "$hook"; then + echo "FAIL: configure hook ignored inactive-service stop failure" >&2 + exit 1 +fi +[[ $(cat "$work/failure.log") == 'stop --disable openshell.system-gateway' ]] + +cat >"$work/failure-bin/snapctl" <>'$work/start-failure.log' +[ "\${1:-}" != start ] +EOF +chmod 755 "$work/failure-bin/snapctl" +if PATH="$work/failure-bin:$PATH" SNAP_INSTANCE_NAME=openshell "$hook"; then + echo "FAIL: configure hook ignored selected-service start failure" >&2 + exit 1 +fi +expected=$'stop --disable openshell.system-gateway\nstart --enable openshell.user-gateway' +[[ $(cat "$work/start-failure.log") == "$expected" ]] + +echo "Snap configure hook tests passed" diff --git a/tasks/scripts/test-snap-gateway-wrapper.sh b/tasks/scripts/test-snap-gateway-wrapper.sh index 691a1e7628..c39c6e9bc8 100755 --- a/tasks/scripts/test-snap-gateway-wrapper.sh +++ b/tasks/scripts/test-snap-gateway-wrapper.sh @@ -4,9 +4,7 @@ set -euo pipefail -wrapper_input=${1:?Usage: test-snap-gateway-wrapper.sh } -wrapper_dir=$(cd "$(dirname "$wrapper_input")" && pwd) -wrapper="${wrapper_dir}/$(basename "$wrapper_input")" +wrapper=${1:?Usage: test-snap-gateway-wrapper.sh } work=$(mktemp -d "${TMPDIR:-/tmp}/openshell snap wrapper.XXXXXX") trap 'rm -rf "$work"' EXIT @@ -18,57 +16,30 @@ mkdir -p "$snap/bin" "$common" cat >"$snap/bin/openshell-gateway" <<'EOF' #!/bin/sh -printf '%s\n' "$*" >>"$FAKE_GATEWAY_LOG" -if [ "${1:-}" = generate-certs ]; then - exit 0 -fi -printf 'env:%s|%s|%s\n' \ +printf '%s|config=%s|db=%s|tls=%s\n' \ + "$*" \ "${OPENSHELL_GATEWAY_CONFIG:-}" \ "${OPENSHELL_DB_URL:-}" \ - "${OPENSHELL_DISABLE_TLS:-}" >>"$FAKE_GATEWAY_LOG" -if [ "${1:-}" = config ] && [ "${2:-}" = preflight ]; then - if [ "${FAKE_PREFLIGHT_FAIL:-}" = 1 ]; then - exit 42 - fi - if [ "${FAKE_REJECT_UNPAIRED_RATE:-}" = 1 ]; then - case " $* " in - *" --grpc-rate-limit-requests "*) - case " $* " in - *" --grpc-rate-limit-window-seconds "*) ;; - *) exit 43 ;; - esac - ;; - esac - fi + "${OPENSHELL_LOCAL_TLS_DIR:-}" >>"$FAKE_GATEWAY_LOG" +if [ "${1:-}:${2:-}" = config:preflight ] && [ "${FAKE_PREFLIGHT_FAIL:-}" = 1 ]; then + exit 42 fi EOF chmod +x "$snap/bin/openshell-gateway" -run_wrapper() { - local config=$1 - local fail=${2:-} - if [ "$config" = unset ]; then - env -u OPENSHELL_GATEWAY_CONFIG \ - SNAP="$snap" \ - SNAP_COMMON="$common" \ - FAKE_GATEWAY_LOG="$log" \ - FAKE_PREFLIGHT_FAIL="$fail" \ - "$wrapper" --trace - else - env \ - SNAP="$snap" \ - SNAP_COMMON="$common" \ - OPENSHELL_GATEWAY_CONFIG="$config" \ - FAKE_GATEWAY_LOG="$log" \ - FAKE_PREFLIGHT_FAIL="$fail" \ - "$wrapper" --trace - fi +run_system_wrapper() { + env -u OPENSHELL_GATEWAY_CONFIG \ + SNAP="$snap" \ + SNAP_COMMON="$common" \ + OPENSHELL_SNAP_CONFIG_FILE="$common/gateway.toml" \ + OPENSHELL_DB_URL="sqlite:$common/gateway.db?mode=rwc" \ + OPENSHELL_LOCAL_TLS_DIR="$common/tls" \ + FAKE_GATEWAY_LOG="$log" \ + "$wrapper" "$@" } assert_log() { - printf '%s\n' \ - "generate-certs --output-dir $common/tls --server-san host.openshell.internal" \ - "$1" >"$expected" + printf '%s\n' "$1" >"$expected" if ! cmp -s "$expected" "$log"; then echo "FAIL: unexpected call sequence" >&2 diff -u "$expected" "$log" >&2 @@ -76,148 +47,86 @@ assert_log() { fi } -override="$work/override.toml" -printf 'operator override\n' >"$override" -cp "$override" "$work/override-before" +# A missing compatibility config remains optional and OpenShell performs its +# normal config discovery. : >"$log" -run_wrapper "$override" -assert_log "config preflight -- --trace -env:$override|sqlite:$common/gateway.db?mode=rwc| ---trace -env:$override|sqlite:$common/gateway.db?mode=rwc|" -cmp -s "$work/override-before" "$override" +run_system_wrapper --trace +assert_log "config preflight -- --trace|config=|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +generate-certs --output-dir $common/tls --server-san host.openshell.internal|config=|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +--trace|config=|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls" + +# An existing compatibility config is exposed through the standard gateway +# config environment variable for both preflight and startup. +printf 'valid schema-v2\n' >"$common/gateway.toml" +: >"$log" +run_system_wrapper --trace +assert_log "config preflight -- --trace|config=$common/gateway.toml|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +generate-certs --output-dir $common/tls --server-san host.openshell.internal|config=$common/gateway.toml|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +--trace|config=$common/gateway.toml|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls" -cli_config="$work/cli.toml" -printf 'CLI override\n' >"$cli_config" -cp "$cli_config" "$work/cli-before" +# An operator-provided environment path takes precedence over the Snap +# compatibility path, while CLI arguments are replayed unchanged. +override="$work/override.toml" +printf 'operator override\n' >"$override" : >"$log" env \ SNAP="$snap" \ SNAP_COMMON="$common" \ + OPENSHELL_SNAP_CONFIG_FILE="$common/gateway.toml" \ OPENSHELL_GATEWAY_CONFIG="$override" \ + OPENSHELL_DB_URL="sqlite:$common/gateway.db?mode=rwc" \ + OPENSHELL_LOCAL_TLS_DIR="$common/tls" \ FAKE_GATEWAY_LOG="$log" \ - "$wrapper" --trace --config "$cli_config" -assert_log "config preflight -- --trace --config $cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc| ---trace --config $cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc|" -cmp -s "$work/cli-before" "$cli_config" - + "$wrapper" --config "$work/cli.toml" --trace +assert_log "config preflight -- --config $work/cli.toml --trace|config=$override|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +generate-certs --output-dir $common/tls --server-san host.openshell.internal|config=$override|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls +--config $work/cli.toml --trace|config=$override|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls" + +# Broken compatibility symlinks are selected so preflight fails closed rather +# than silently falling back to defaults. +rm "$common/gateway.toml" +ln -s "$work/missing.toml" "$common/gateway.toml" : >"$log" -if env \ - SNAP="$snap" \ - SNAP_COMMON="$common" \ - OPENSHELL_GATEWAY_CONFIG="$override" \ - FAKE_GATEWAY_LOG="$log" \ - FAKE_PREFLIGHT_FAIL=1 \ - "$wrapper" --config="$cli_config"; then - echo "FAIL: CLI-selected config preflight failure reached gateway start" >&2 +if FAKE_PREFLIGHT_FAIL=1 run_system_wrapper --trace; then + echo "FAIL: broken compatibility symlink reached certificate generation" >&2 exit 1 fi -assert_log "config preflight -- --config=$cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc|" -cmp -s "$work/cli-before" "$cli_config" +assert_log "config preflight -- --trace|config=$common/gateway.toml|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls" +# Preflight failure prevents certificate generation and gateway startup. : >"$log" -if env \ - SNAP="$snap" \ - SNAP_COMMON="$common" \ - OPENSHELL_GATEWAY_CONFIG="$override" \ - FAKE_GATEWAY_LOG="$log" \ - FAKE_REJECT_UNPAIRED_RATE=1 \ - "$wrapper" --grpc-rate-limit-requests 10; then - echo "FAIL: invalid daemon overrides reached gateway start" >&2 +if FAKE_PREFLIGHT_FAIL=1 run_system_wrapper --config --; then + echo "FAIL: preflight failure reached certificate generation" >&2 exit 1 fi -assert_log "config preflight -- --grpc-rate-limit-requests 10 -env:$override|sqlite:$common/gateway.db?mode=rwc|" - -for invalid_selector in terminator nested-config; do - : >"$log" - if [ "$invalid_selector" = terminator ]; then - invalid_args=(--config --) - else - invalid_args=(--config "--config=$cli_config") - fi - if env \ - SNAP="$snap" \ - SNAP_COMMON="$common" \ - OPENSHELL_GATEWAY_CONFIG="$override" \ - FAKE_GATEWAY_LOG="$log" \ - "$wrapper" "${invalid_args[@]}"; then - echo "FAIL: invalid $invalid_selector selector reached gateway execution" >&2 - exit 1 - fi - if [ -s "$log" ]; then - echo "FAIL: invalid $invalid_selector selector reached preflight" >&2 - exit 1 - fi -done +assert_log "config preflight -- --config --|config=$common/gateway.toml|db=sqlite:$common/gateway.db?mode=rwc|tls=$common/tls" +# User mode supplies only XDG roots. The wrapper derives TLS state, leaves the +# database unset for OpenShell's native default, and lets OpenShell discover +# the XDG config itself. +user_common="$work/user-common" +user_tls="$user_common/.local/state/openshell/tls" : >"$log" -env \ +env -u OPENSHELL_GATEWAY_CONFIG \ + -u OPENSHELL_SNAP_CONFIG_FILE \ + -u OPENSHELL_DB_URL \ + -u OPENSHELL_LOCAL_TLS_DIR \ SNAP="$snap" \ SNAP_COMMON="$common" \ - OPENSHELL_GATEWAY_CONFIG="$override" \ + XDG_CONFIG_HOME="$user_common/.config" \ + XDG_STATE_HOME="$user_common/.local/state" \ FAKE_GATEWAY_LOG="$log" \ - "$wrapper" --config=--dash-leading -assert_log "config preflight -- --config=--dash-leading -env:$override|sqlite:$common/gateway.db?mode=rwc| ---config=--dash-leading -env:$override|sqlite:$common/gateway.db?mode=rwc|" - -canonical="$common/gateway.toml" -printf 'valid schema-v2\n' >"$canonical" -cp "$canonical" "$work/canonical-before" -: >"$log" -run_wrapper unset -assert_log "config preflight -- --config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc| ---config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc|" -cmp -s "$work/canonical-before" "$canonical" - -rm "$canonical" -: >"$log" -run_wrapper unset -assert_log "config preflight -- --trace -env:|sqlite:$common/gateway.db?mode=rwc| ---trace -env:|sqlite:$common/gateway.db?mode=rwc|" - -assert_preflight_failure() { - local name=$1 - : >"$log" - if run_wrapper unset 1; then - echo "FAIL: $name reached gateway start" >&2 - exit 1 - fi - assert_log "config preflight -- --config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc|" -} - -printf 'legacy version = 1\n' >"$canonical" -cp "$canonical" "$work/legacy-before" -assert_preflight_failure legacy -cmp -s "$work/legacy-before" "$canonical" - -printf 'not valid TOML = [\n' >"$canonical" -cp "$canonical" "$work/malformed-before" -assert_preflight_failure malformed -cmp -s "$work/malformed-before" "$canonical" - -rm "$canonical" -ln -s "$work/missing-target" "$canonical" -readlink "$canonical" >"$work/link-before" -assert_preflight_failure broken-symlink -readlink "$canonical" >"$work/link-after" -cmp -s "$work/link-before" "$work/link-after" - -rm "$canonical" -mkdir "$canonical" -printf 'nonregular marker\n' >"$canonical/marker" -cp "$canonical/marker" "$work/marker-before" -assert_preflight_failure nonregular -cmp -s "$work/marker-before" "$canonical/marker" + "$wrapper" --trace +assert_log "config preflight -- --trace|config=|db=|tls=$user_tls +generate-certs --output-dir $user_tls --server-san host.openshell.internal|config=|db=|tls=$user_tls +--trace|config=|db=|tls=$user_tls" + +if env -u OPENSHELL_LOCAL_TLS_DIR -u XDG_STATE_HOME \ + SNAP="$snap" FAKE_GATEWAY_LOG="$log" "$wrapper" --trace \ + >"$work/out" 2>"$work/err"; then + echo "FAIL: wrapper accepted missing TLS roots" >&2 + exit 1 +fi +grep -Fq 'OPENSHELL_LOCAL_TLS_DIR or XDG_STATE_HOME is required' "$work/err" echo "Snap gateway wrapper tests passed" diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh new file mode 100755 index 0000000000..2815f95e6c --- /dev/null +++ b/tasks/scripts/test-snap-install-hook.sh @@ -0,0 +1,25 @@ +#!/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 + +hook=${1:?Usage: test-snap-install-hook.sh } +work=$(mktemp -d "${TMPDIR:-/tmp}/openshell snap install hook.XXXXXX") +trap 'rm -rf "$work"' EXIT + +mkdir -p "$work/bin" +cat >"$work/bin/snapctl" <>"$work/snapctl.log" +EOF +chmod 755 "$work/bin/snapctl" + +PATH="$work/bin:$PATH" SNAP_INSTANCE_NAME=openshell "$hook" +if [[ $(cat "$work/snapctl.log") != 'set gateway-mode=user' ]]; then + echo "FAIL: install hook did not initialize user gateway mode" >&2 + cat "$work/snapctl.log" >&2 + exit 1 +fi + +echo "Snap install hook tests passed" diff --git a/tasks/scripts/test-snap-post-refresh-hook.sh b/tasks/scripts/test-snap-post-refresh-hook.sh index 1f97ac2598..778583c35a 100755 --- a/tasks/scripts/test-snap-post-refresh-hook.sh +++ b/tasks/scripts/test-snap-post-refresh-hook.sh @@ -4,144 +4,125 @@ set -euo pipefail -hook_input=${1:?Usage: test-snap-post-refresh-hook.sh } -hook_dir=$(cd "$(dirname "$hook_input")" && pwd) -hook="${hook_dir}/$(basename "$hook_input")" +hook=${1:?Usage: test-snap-post-refresh-hook.sh } work=$(mktemp -d "${TMPDIR:-/tmp}/openshell snap post-refresh hook.XXXXXX") trap 'rm -rf "$work"' EXIT -mkdir -p "${work}/bin" -cat >"${work}/bin/snapctl" <>"${work}/snapctl.log" -EOF -chmod 755 "${work}/bin/snapctl" - run_hook() { local common=$1 - - PATH="${work}/bin:$PATH" SNAP_COMMON="$common" SNAP_INSTANCE_NAME=openshell "$hook" -} - -assert_no_restart() { - local name=$1 - local common=$2 - - rm -f "${work}/snapctl.log" - run_hook "$common" - if [[ -e "${work}/snapctl.log" ]]; then - echo "FAIL: post-refresh hook restarted the gateway for ${name}" >&2 - cat "${work}/snapctl.log" >&2 - exit 1 - fi -} - -assert_removed_and_restarted() { - local name=$1 - local common=$2 - - rm -f "${work}/snapctl.log" - run_hook "$common" - if [[ -e "$common/gateway.toml" ]] || [[ -L "$common/gateway.toml" ]]; then - echo "FAIL: post-refresh hook did not remove ${name}" >&2 - exit 1 - fi - if [[ $(cat "${work}/snapctl.log") != "restart openshell.gateway" ]]; then - echo "FAIL: post-refresh hook did not restart the gateway once for ${name}" >&2 - cat "${work}/snapctl.log" >&2 - exit 1 - fi + local mode=$2 + rm -f "$work/snapctl.log" + mkdir -p "$work/bin" + cat >"$work/bin/snapctl" <>'$work/snapctl.log' +EOF + chmod 755 "$work/bin/snapctl" + PATH="$work/bin:$PATH" SNAP_COMMON="$common" SNAP_INSTANCE_NAME=openshell "$hook" } -common="${work}/missing" +for mode in user system disable; do + common="$work/$mode" + mkdir -p "$common" + printf '%s\n' 'disable_tls = true' >"$common/gateway.toml" + cp "$common/gateway.toml" "$work/$mode-before" + run_hook "$common" "$mode" + cmp -s "$work/$mode-before" "$common/gateway.toml" + [[ ! -e "$work/snapctl.log" ]] +done + +common="$work/missing" mkdir -p "$common" -assert_no_restart "a missing config" "$common" -if [[ -e "$common/gateway.toml" ]] || [[ -L "$common/gateway.toml" ]]; then - echo "FAIL: post-refresh hook created a missing config" >&2 - exit 1 -fi +run_hook "$common" "" +expected='set gateway-mode=system' +[[ $(cat "$work/snapctl.log") == "$expected" ]] -common="${work}/secure" +common="$work/secure" mkdir -p "$common" cat >"$common/gateway.toml" <<'EOF' [openshell] version = 2 - [openshell.gateway] -compute_driver = "docker" disable_tls = false - [openshell.gateway.auth] allow_unauthenticated_users = false -# allow_unauthenticated_users = true -EOF -cp "$common/gateway.toml" "${work}/secure-before" -assert_no_restart "a secure config" "$common" -cmp -s "${work}/secure-before" "$common/gateway.toml" - -common="${work}/unauthenticated" -mkdir -p "$common" -cat >"$common/gateway.toml" <<'EOF' -[openshell.gateway.auth] -allow_unauthenticated_users = true EOF -assert_removed_and_restarted "an unauthenticated config" "$common" - -common="${work}/tls-disabled" +cp "$common/gateway.toml" "$work/secure-before" +run_hook "$common" "" +cmp -s "$work/secure-before" "$common/gateway.toml" +[[ $(cat "$work/snapctl.log") == "$expected" ]] + +for setting in 'allow_unauthenticated_users = true' 'disable_tls = true # legacy'; do + common="$work/unsafe-${setting%% *}" + mkdir -p "$common" + printf '%s\n' "$setting" >"$common/gateway.toml" + run_hook "$common" "" + [[ ! -e "$common/gateway.toml" ]] + expected='set gateway-mode=system' + [[ $(cat "$work/snapctl.log") == "$expected" ]] +done + +common="$work/symlink" mkdir -p "$common" -cat >"$common/gateway.toml" <<'EOF' -[openshell.gateway] -disable_tls = true # old local override - -# operator note -EOF -assert_removed_and_restarted "an edited TLS-disabled config" "$common" - -common="${work}/broken-link" +printf '%s\n' 'disable_tls = true' >"$work/linked-config.toml" +ln -s "$work/linked-config.toml" "$common/gateway.toml" +run_hook "$common" "" +[[ ! -e "$common/gateway.toml" && ! -L "$common/gateway.toml" ]] +[[ -f "$work/linked-config.toml" ]] +expected='set gateway-mode=system' +[[ $(cat "$work/snapctl.log") == "$expected" ]] + +common="$work/broken-symlink" mkdir -p "$common" -ln -s "${work}/missing-target" "$common/gateway.toml" -assert_no_restart "a broken operator symlink" "$common" -if [[ $(readlink "$common/gateway.toml") != "${work}/missing-target" ]]; then - echo "FAIL: post-refresh hook replaced a broken operator symlink" >&2 - exit 1 -fi +ln -s "$work/missing-target" "$common/gateway.toml" +run_hook "$common" "" +[[ -L "$common/gateway.toml" ]] +[[ $(cat "$work/snapctl.log") == "$expected" ]] -common="${work}/existing-link" +common="$work/unknown" mkdir -p "$common" -printf '%s\n' 'disable_tls = true' >"${work}/linked-config.toml" -ln -s "${work}/linked-config.toml" "$common/gateway.toml" -assert_no_restart "an existing operator symlink" "$common" -if [[ $(readlink "$common/gateway.toml") != "${work}/linked-config.toml" ]]; then - echo "FAIL: post-refresh hook replaced an existing operator symlink" >&2 +if run_hook "$common" invalid >"$work/out" 2>"$work/err"; then + echo "FAIL: post-refresh hook accepted unknown service mode" >&2 exit 1 fi +grep -Fq 'unsupported gateway-mode: invalid' "$work/err" +[[ ! -e "$work/snapctl.log" ]] -common="${work}/directory" -mkdir -p "$common/gateway.toml" -assert_no_restart "an operator-owned directory" "$common" -if [[ ! -d "$common/gateway.toml" ]]; then - echo "FAIL: post-refresh hook replaced an operator-owned directory" >&2 +common="$work/get-failure" +mkdir -p "$common" "$work/get-failure-bin" +cat >"$work/get-failure-bin/snapctl" <<'EOF' +#!/bin/sh +exit 1 +EOF +chmod 755 "$work/get-failure-bin/snapctl" +if PATH="$work/get-failure-bin:$PATH" SNAP_COMMON="$common" \ + SNAP_INSTANCE_NAME=openshell "$hook"; then + echo "FAIL: post-refresh hook treated snapctl get failure as a missing mode" >&2 exit 1 fi -common="${work}/remove-failure" -mkdir -p "$common" "${work}/failing-bin" +common="$work/remove-failure" +mkdir -p "$common" "$work/remove-failure-bin" printf '%s\n' 'disable_tls = true' >"$common/gateway.toml" -cat >"${work}/failing-bin/rm" <<'EOF' +cat >"$work/remove-failure-bin/snapctl" <>'$work/remove-failure.log' +EOF +cat >"$work/remove-failure-bin/rm" <<'EOF' #!/bin/sh exit 1 EOF -chmod 755 "${work}/failing-bin/rm" -rm -f "${work}/snapctl.log" -if PATH="${work}/failing-bin:${work}/bin:$PATH" SNAP_COMMON="$common" \ +chmod 755 "$work/remove-failure-bin/snapctl" "$work/remove-failure-bin/rm" +if PATH="$work/remove-failure-bin:$PATH" SNAP_COMMON="$common" \ SNAP_INSTANCE_NAME=openshell "$hook"; then - echo "FAIL: post-refresh hook succeeded when config removal failed" >&2 - exit 1 -fi -if [[ -e "${work}/snapctl.log" ]]; then - echo "FAIL: post-refresh hook restarted after config removal failed" >&2 - cat "${work}/snapctl.log" >&2 + echo "FAIL: post-refresh hook ignored config removal failure" >&2 exit 1 fi +[[ ! -e "$work/remove-failure.log" ]] echo "Snap post-refresh hook tests passed" diff --git a/tests/ansible/playbooks/conformance/cli.yaml b/tests/ansible/playbooks/conformance/cli.yaml index dd8ce2b592..11eb07e36d 100644 --- a/tests/ansible/playbooks/conformance/cli.yaml +++ b/tests/ansible/playbooks/conformance/cli.yaml @@ -5,6 +5,9 @@ - name: Run OpenShell conformance tests hosts: all gather_facts: false + vars: + # Conformance is skipped when the gateway never connects. + conformance_failed: "{{ gateway_status is failed or conformance_result.rc != 0 }}" tasks: - name: Wait for SSH ansible.builtin.wait_for_connection: @@ -55,6 +58,20 @@ register: openshell_cli changed_when: false + # Every test boot restarts the gateway, and some installers take minutes to + # recover. Wait once here; the runner's per-scenario preflight stays short. + - name: Wait for the registered gateway + ansible.builtin.command: + argv: [timeout, 10s, "{{ openshell_cli.stdout }}", status, --output, json] + register: gateway_status + changed_when: false + until: >- + gateway_status.rc == 0 and + (gateway_status.stdout | from_json).status == 'connected' + retries: 60 + delay: 5 + ignore_errors: true + - name: Run OpenShell conformance archive ansible.builtin.command: argv: @@ -77,34 +94,25 @@ register: conformance_result changed_when: false failed_when: false + when: gateway_status is succeeded # Preserve both streams for failures without adding passing-test output to # every tmachine run. - name: Show OpenShell conformance diagnostics ansible.builtin.debug: var: conformance_result - when: conformance_result.rc != 0 + when: conformance_failed - - name: Read OpenShell gateway logs - become: true - ansible.builtin.command: - argv: - - journalctl - - --no-pager - - --lines - - "500" - - _SYSTEMD_UNIT=openshell-gateway.service - - "+" - - _SYSTEMD_USER_UNIT=openshell-gateway.service - register: openshell_gateway_logs - changed_when: false - failed_when: false - when: conformance_result.rc != 0 + - name: Collect OpenShell gateway diagnostics + ansible.builtin.include_role: + name: openshell_diagnostics + when: conformance_failed - - name: Show OpenShell gateway logs - ansible.builtin.debug: - var: openshell_gateway_logs.stdout_lines - when: conformance_result.rc != 0 + - name: Require a connected gateway + ansible.builtin.assert: + that: + - gateway_status is succeeded + fail_msg: OpenShell gateway did not connect before conformance; see gateway diagnostics - name: Require OpenShell conformance success ansible.builtin.assert: diff --git a/tests/ansible/playbooks/openshell-k3s.yaml b/tests/ansible/playbooks/openshell-k3s.yaml index dd00aa4bda..591262a933 100644 --- a/tests/ansible/playbooks/openshell-k3s.yaml +++ b/tests/ansible/playbooks/openshell-k3s.yaml @@ -137,39 +137,32 @@ environment: KUBECONFIG: /etc/rancher/k3s/k3s.yaml - - name: Install gateway port-forward service - ansible.builtin.copy: - dest: /etc/systemd/system/openshell-k3s-port-forward.service - mode: "0644" - content: | - [Unit] - Description=OpenShell K3s gateway port forward - After=k3s.service - Requires=k3s.service - - [Service] - ExecStart=/usr/local/bin/k3s kubectl --namespace openshell port-forward --address 127.0.0.1 service/openshell 17670:8080 - Restart=always - RestartSec=1 - - [Install] - WantedBy=multi-user.target - - - name: Start gateway port-forward service - ansible.builtin.systemd_service: - name: openshell-k3s-port-forward.service - daemon_reload: true - enabled: true - state: started + - name: Discover the gateway ClusterIP and gRPC port + ansible.builtin.command: + argv: + - /usr/local/bin/k3s + - kubectl + - --request-timeout=10s + - --namespace + - openshell + - get + - service + - openshell + - --output=jsonpath={.spec.clusterIP}:{.spec.ports[?(@.name=="grpc")].port} + register: k3s_gateway_address + changed_when: false - - name: Wait for OpenShell gateway - ansible.builtin.wait_for: - host: 127.0.0.1 - port: 17670 - timeout: 60 + # Rejects headless Services (ClusterIP "None") and a missing grpc port. + - name: Require a gateway Service address + ansible.builtin.assert: + that: + - k3s_gateway_address.stdout is match('^[0-9.]+:[0-9]+$') + fail_msg: "Unexpected gateway Service address: {{ k3s_gateway_address.stdout }}" - name: Register OpenShell gateway for test client hosts: all gather_facts: false + vars: + openshell_client_gateway_endpoint: "http://{{ k3s_gateway_address.stdout }}" roles: - openshell_client diff --git a/tests/ansible/roles/openshell_diagnostics/files/collect-k3s.sh b/tests/ansible/roles/openshell_diagnostics/files/collect-k3s.sh new file mode 100644 index 0000000000..c20334bb13 --- /dev/null +++ b/tests/ansible/roles/openshell_diagnostics/files/collect-k3s.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# No kubeconfig, Secrets, environment dumps, or complete Pod specifications. +set -u +umask 077 +mkdir -p /var/lib/openshell/diagnostics +output=/var/lib/openshell/diagnostics/k3s.txt + +collect() { + printf '\n### %s\n' "$1" + shift + # Bound both stalled commands and unusually large log streams. + timeout 10s "$@" 2>&1 | tail -c 262144 + printf '\ncommand status: %s\n' "${PIPESTATUS[0]}" +} + +{ + date --utc --iso-8601=seconds + collect 'K3s service state' systemctl show k3s.service \ + -p ActiveState -p SubState -p Result -p NRestarts -p ExecMainStatus \ + -p ExecMainPID -p ActiveEnterTimestamp -p ExecMainStartTimestamp \ + -p StartLimitIntervalUSec -p StartLimitBurst -p RestartUSec + collect 'K3s boot journal' journalctl -b --no-pager --lines=500 -u k3s.service + # kube-proxy may program either iptables backend; dump whichever exist. + # shellcheck disable=SC2016 # Expanded by the inner shell. + collect 'Gateway Service routing rules' bash -c ' + for save in iptables-nft-save iptables-legacy-save; do + command -v "$save" >/dev/null || continue + echo "# $save" + "$save" 2>/dev/null | awk "/openshell\/openshell:grpc/ { print }" + done' + collect 'K3s version' /usr/local/bin/k3s --version + collect 'API readiness' /usr/local/bin/k3s kubectl --request-timeout=5s get --raw=/readyz + collect 'Nodes' /usr/local/bin/k3s kubectl --request-timeout=5s get nodes -o wide + collect 'Gateway Pod identity and state' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell get pods \ + -o 'custom-columns=NAME:.metadata.name,UID:.metadata.uid,PHASE:.status.phase,CONDITIONS:.status.conditions,CONTAINERS:.status.containerStatuses' + collect 'Gateway Services' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell get services \ + -o 'custom-columns=NAME:.metadata.name,TYPE:.spec.type,CLUSTERIP:.spec.clusterIP,SELECTOR:.spec.selector,PORTS:.spec.ports' + collect 'Test client gateway registrations' runuser -u tmachine -- openshell gateway list --output json + collect 'Test client gateway status' runuser -u tmachine -- openshell status --output json + collect 'Endpoint readiness' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell get endpointslices \ + -o 'custom-columns=NAME:.metadata.name,PORTS:.ports,ADDRESSES:.endpoints[*].addresses,CONDITIONS:.endpoints[*].conditions' + collect 'Gateway events' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell get events --sort-by=.lastTimestamp + collect 'Gateway current logs' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell logs openshell-0 -c openshell-gateway --tail=300 --timestamps + collect 'Gateway previous logs' /usr/local/bin/k3s kubectl --request-timeout=5s -n openshell logs openshell-0 -c openshell-gateway --previous --tail=300 --timestamps + collect 'Memory' free -m + collect 'Disk' df -h / /var/lib/rancher/k3s +} | sed -E \ + -e 's/(Bearer )[A-Za-z0-9._~+\/-]+/\1[REDACTED]/gI' \ + -e 's/((token|password|secret|authorization)[" ]*[=:][" ]*)[^ ,"]+/\1[REDACTED]/gI' \ + > "$output" +cat "$output" diff --git a/tests/ansible/roles/openshell_diagnostics/tasks/main.yaml b/tests/ansible/roles/openshell_diagnostics/tasks/main.yaml new file mode 100644 index 0000000000..74a204ab96 --- /dev/null +++ b/tests/ansible/roles/openshell_diagnostics/tasks/main.yaml @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +# Collect whatever applies to the installed gateway before the VM exits. +- name: Read OpenShell gateway logs + become: true + ansible.builtin.command: + argv: + - journalctl + - --no-pager + - --lines + - "500" + - _SYSTEMD_UNIT=openshell-gateway.service + - "+" + - _SYSTEMD_USER_UNIT=openshell-gateway.service + register: openshell_gateway_logs + changed_when: false + failed_when: false + +- name: Show OpenShell gateway logs + ansible.builtin.debug: + var: openshell_gateway_logs.stdout_lines + +- name: Check for K3s + become: true + ansible.builtin.stat: + path: /usr/local/bin/k3s + register: k3s_diagnostics_binary + failed_when: false + +- name: Preserve K3s diagnostics + when: k3s_diagnostics_binary.stat.exists | default(false) + become: true + block: + - name: Collect bounded K3s diagnostics + ansible.builtin.script: collect-k3s.sh + register: k3s_diagnostics_collection + changed_when: false + failed_when: false + + - name: Show K3s diagnostics + ansible.builtin.debug: + var: k3s_diagnostics_collection.stdout_lines + + - name: Fetch K3s diagnostics + ansible.builtin.fetch: + src: /var/lib/openshell/diagnostics/k3s.txt + dest: "{{ role_path }}/../../../../artifacts/tmachine-diagnostics/{{ inventory_hostname }}/k3s.txt" + flat: true + failed_when: false diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index 8561dc97df..23f2ab0c61 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -821,16 +821,15 @@ dependencies = [ [[package]] name = "noyalib" -version = "0.0.28" +version = "0.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" dependencies = [ "hashbrown", "indexmap", "libm", "memchr", "rustc-hash", - "serde", "serde_core", "smallvec", ]