diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c797b3f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +bin/ +coverage*.out +deploy/helm/ +docs/ +examples/ +.github/ +*.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..baab2fe --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + groups: + go-minor-patch: + update-types: [minor, patch] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7c989e5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +# Build, test with a coverage gate, lint, and check that generated bindings match the vendored protos. +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e #v7.0.0 + with: + go-version: "1.26.9" + - uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 #v4.3.0 + - name: Build + run: go build ./... + - name: Test + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... + - name: Coverage gate + run: | + # Generated bindings, thin main packages and the demo are exercised end to end, + # not by unit tests; the gate covers the library packages. + grep -v -e '/gen/' -e '/cmd/' -e '/examples/' coverage.out > coverage.lib.out + COVERAGE=$(go tool cover -func=coverage.lib.out | grep '^total:' | awk '{print $NF}' | tr -d '%') + echo "library coverage: ${COVERAGE}%" + THRESHOLD=95 + if [ "$(echo "${COVERAGE} < ${THRESHOLD}" | bc -l)" -eq 1 ]; then + echo "::error::Coverage ${COVERAGE}% is below the required ${THRESHOLD}% threshold" + exit 1 + fi + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 + with: + name: coverage-report + path: coverage.out + retention-days: 30 + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e #v7.0.0 + with: + go-version: "1.26.9" + - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #v9.3.0 + with: + version: v2.10.1 + args: --verbose --concurrency=4 --allow-parallel-runners + + deploy-artifacts: + name: Container images & Helm chart + runs-on: ubuntu-latest + strategy: + matrix: + cmd: [ans-openshell-middleware, ans-openshell-interceptor, ans-ocsf-hydrator, ans-reference-callee] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - name: Build image + run: docker build -f deploy/Dockerfile --build-arg CMD=${{ matrix.cmd }} -t ${{ matrix.cmd }}:ci . + - uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 #v4.3.0 + - name: Lint and render chart + run: | + helm lint deploy/helm/ans-openshell + helm template ci deploy/helm/ans-openshell --set hydrator.enabled=true > /dev/null + + proto-drift: + name: Generated bindings match protos + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e #v7.0.0 + with: + go-version: "1.26.9" + - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 #v1 + with: + setup_only: true + # Pinned so the drift check is reproducible; make proto documents the plugin pins. + version: 1.73.0 + - name: Install protoc plugins + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.12 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2 + - name: Regenerate and diff + run: | + buf generate + git diff --exit-code -- gen/ diff --git a/.github/workflows/dco-merge-group.yml b/.github/workflows/dco-merge-group.yml new file mode 100644 index 0000000..f8f4302 --- /dev/null +++ b/.github/workflows/dco-merge-group.yml @@ -0,0 +1,19 @@ +name: DCO merge-group shim + +# The org-wide DCO app (https://github.com/apps/dco) validates sign-offs on +# pull requests but does not report on merge-queue (merge_group) commits, so +# a required "DCO" check would stall the queue. This shim reports a passing +# "DCO" check for merge groups only — the real validation already happened +# on the pull request. Same pattern as onnx/onnx#5399. + +on: + merge_group: + +permissions: {} + +jobs: + dco: + name: DCO + runs-on: ubuntu-latest + steps: + - run: echo "DCO was validated on the pull request by the DCO app." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..68ba3d8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,109 @@ +# Automatically creates release PRs and, when a release-please release lands, +# builds and uploads the four ans-openshell binaries to that release and +# publishes the chart's container images to ghcr.io in the same workflow run. +# +# Why these are combined: events triggered by GITHUB_TOKEN (such as +# release-please publishing a release) do NOT trigger downstream workflows. +# Splitting goreleaser into its own `release: published` workflow would silently +# never fire. Running goreleaser as a needs:-gated job in the same workflow +# avoids this entirely without requiring a PAT. +# Docs: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +name: Release + +on: + push: + branches: + - main + # Manual trigger for recovery scenarios (e.g., re-running after a failed release). + workflow_dispatch: + +# contents:write is required for creating git tags, GitHub Releases, and uploading release assets. +# pull-requests:write is required for creating and updating release PRs. +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - name: Run Release Please + id: release + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + goreleaser: + name: Build & upload binaries + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history + tags so `git describe` resolves the version. + fetch-depth: 0 + ref: ${{ needs.release-please.outputs.tag_name }} + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: "1.26.9" + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-images: + name: Publish container images + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + cmd: [ans-openshell-middleware, ans-openshell-interceptor, ans-ocsf-hydrator] + env: + TAG: ${{ needs.release-please.outputs.tag_name }} + CMD: ${{ matrix.cmd }} + OWNER: ${{ github.repository_owner }} + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Log in to ghcr.io + run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # release-please tags vX.Y.Z and bumps Chart.yaml appVersion to X.Y.Z, the chart's + # default image tag, so the pushed tag is the version without its v prefix. The + # Dockerfile cross-compiles, so both platforms build natively. The first push + # creates each package private; make it public once in the organisation's package + # settings or the chart defaults cannot pull it. + - name: Build and push + run: | + VERSION="${TAG#v}" + docker buildx build --platform linux/amd64,linux/arm64 -f deploy/Dockerfile \ + --build-arg CMD="${CMD}" --build-arg VERSION="${VERSION}" \ + -t "ghcr.io/${OWNER}/${CMD}:${VERSION}" --push . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0aeb50e --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Binaries +/bin/ +/dist/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +/ans-openshell-middleware +/ans-openshell-interceptor +/ans-ocsf-hydrator +/ans-reference-callee +/demo + +# Test binaries, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out +coverage.html + +# Dependency directories and workspace files +vendor/ +go.work +go.work.sum + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Secrets and certificates +.env +.env.* +*.pem +*.key +*.csr diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7e0572d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,452 @@ +# This file is used by golangci-lint to configure the linter. + + +version: "2" + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 50 + +formatters: + enable: + - goimports # checks if the code and import statements are formatted according to the 'goimports' command + #- golines # checks if code is formatted, and fixes long lines + + ## you may want to enable + #- gci # checks if code and import statements are formatted, with additional rules + #- gofmt # checks if the code is formatted according to 'gofmt' command + + ## disabled + #- gofumpt # [replaced by goimports, gofumports is not available yet] checks if code and import statements are formatted, with additional rules + + # All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml + settings: + goimports: + # A list of prefixes, which, if set, checks import paths + # with the given prefixes are grouped after 3rd-party packages. + # Default: [] + local-prefixes: + - github.com/agentnameservice/ans-openshell + + golines: + # Target maximum line length. + # Default: 100 + max-len: 120 + +linters: + enable: + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - canonicalheader # checks whether net/http.Header uses canonical header + - copyloopvar # detects places where loop variables are copied (Go 1.22+) + - cyclop # checks function and package cyclomatic complexity + - depguard # checks if package imports are in a list of acceptable packages + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # checks exhaustiveness of enum switch statements + - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions + - fatcontext # detects nested contexts in loops + - forbidigo # forbids identifiers + - funcorder # checks the order of functions, methods, and constructors + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - gochecknoglobals # checks that no global variables exist + - gochecknoinits # checks that no init functions are present in Go code + - gochecksumtype # checks exhaustiveness on Go "sum types" + - gocognit # computes and checks the cognitive complexity of functions + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocyclo # computes and checks the cyclomatic complexity of functions + - godox # detects usage of FIXME, TODO and other keywords inside comments + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution + - ineffassign # detects when assignments to existing variables are not used + - interfacebloat # checks the number of methods inside an interface + - intrange # finds places where for loops could make use of an integer range + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - mnd # detects magic numbers + - musttag # enforces field tags in (un)marshaled structs + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr) + - nilnil # checks that there is no simultaneous return of nil error and an invalid value + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - nonamedreturns # reports all named returns + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - protogetter # reports direct reads from proto message fields when getters should be used + - reassign # checks that package variables are not reassigned + - recvcheck # checks for receiver type consistency + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sloglint # ensure consistent code style when using log/slog + - spancheck # checks for mistakes with OpenTelemetry/Census spans + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - testableexamples # checks if examples are testable (have an expected output) + - testifylint # checks usage of github.com/stretchr/testify + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks for unused constants, variables, functions and types + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - usetesting # reports uses of functions with replacement inside the testing package + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace + + ## you may want to enable + #- decorder # checks declaration order and count of types, constants, variables and functions + #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega + #- godot # checks if comments end in a period + #- goheader # checks is file header matches to pattern + #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters + #- ireturn # accept interfaces, return concrete types + # + #- tagalign # checks that struct tags are well aligned + #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope + #- wrapcheck # checks that errors returned from external packages are wrapped + #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + + ## disabled + #- containedctx # detects struct contained context.Context field + #- contextcheck # [too many false positives] checks the function whether use a non-inherited context + #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + #- dupword # [useless without config] checks for duplicate words in the source code + #- err113 # [too strict] checks the errors handling expressions + #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted + #- forcetypeassert # [replaced by errcheck] finds forced type assertions + #- gomodguard # [use more powerful depguard] allow and block lists linter for direct Go module dependencies + #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase + #- grouper # analyzes expression groups + #- importas # enforces consistent import aliases + #- lll # [replaced by golines] reports long lines + #- maintidx # measures the maintainability index of each function + #- misspell # [useless] finds commonly misspelled English words in comments + #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity + #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test + #- tagliatelle # checks the struct tags + #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines + + # All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml + settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled. + # Default: 0.0 + package-average: 10.0 + + depguard: + # Rules to apply. + # + # Variables: + # - File Variables + # Use an exclamation mark `!` to negate a variable. + # Example: `!$test` matches any file that is not a go test file. + # + # `$all` - matches all go files + # `$test` - matches all go test files + # + # - Package Variables + # + # `$gostd` - matches all of go's standard library (Pulled from `GOROOT`) + # + # Default (applies if no custom rules are defined): Only allow $gostd in all files. + rules: + "deprecated": + # List of file globs that will match this list of settings to compare against. + # By default, if a path is relative, it is relative to the directory where the golangci-lint command is executed. + # The placeholder '${base-path}' is substituted with a path relative to the mode defined with `run.relative-path-mode`. + # The placeholder '${config-path}' is substituted with a path relative to the configuration file. + # Default: $all + files: + - "$all" + # List of packages that are not allowed. + # Entries can be a variable (starting with $), a string prefix, or an exact match (if ending with $). + # Default: [] + deny: + - pkg: github.com/golang/protobuf + desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules + - pkg: github.com/satori/go.uuid + desc: Use github.com/google/uuid instead, satori's package is not maintained + - pkg: github.com/gofrs/uuid$ + desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5 + "non-test files": + files: + - "!$test" + deny: + - pkg: math/rand$ + desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2 + "non-main files": + files: + - "!**/main.go" + deny: + - pkg: log$ + desc: Use log/slog instead, see https://go.dev/blog/slog + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and their names from checks. + # Regular expressions must match complete canonical struct package/name/structname. + # Default: [] + exclude: + # std libs + - ^net/http.Client$ + - ^net/http.Cookie$ + - ^net/http.Request$ + - ^net/http.Response$ + - ^net/http.Server$ + - ^net/http.Transport$ + - ^net/url.URL$ + - ^os/exec.Cmd$ + - ^reflect.StructField$ + # public libs + - ^github.com/Shopify/sarama.Config$ + - ^github.com/Shopify/sarama.ProducerMessage$ + - ^github.com/mitchellh/mapstructure.DecoderConfig$ + - ^github.com/prometheus/client_golang/.+Opts$ + - ^github.com/spf13/cobra.Command$ + - ^github.com/spf13/cobra.CompletionOptions$ + - ^github.com/stretchr/testify/mock.Mock$ + - ^github.com/testcontainers/testcontainers-go.+Request$ + - ^github.com/testcontainers/testcontainers-go.FromDockerfile$ + - ^golang.org/x/tools/go/analysis.Analyzer$ + - ^google.golang.org/protobuf/.+Options$ + - ^gopkg.in/yaml.v3.Node$ + + funcorder: + # Checks if the exported methods of a structure are placed before the non-exported ones. + # Default: true + struct-method: false + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + + gochecksumtype: + # Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed. + # Default: true + default-signifies-exhaustive: false + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be found at https://go-critic.com/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: false + + inamedparam: + # Skips check for interface methods with only a single parameter. + # Default: false + skip-single-param: true + + mnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - args.Error + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, golines ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + perfsprint: + # Optimizes into strings concatenation. + # Default: true + strconcat: false + + reassign: + # Patterns for global variable names that are checked for reassignment. + # See https://github.com/curioswitch/go-reassign#usage + # Default: ["EOF", "Err.*"] + patterns: + - ".*" + + rowserrcheck: + # database/sql is always checked. + # Default: [] + packages: + - github.com/jmoiron/sqlx + + sloglint: + # Enforce not using global loggers. + # Values: + # - "": disabled + # - "all": report all global loggers + # - "default": report only the default slog logger + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global + # Default: "" + no-global: all + # Enforce using methods that accept a context. + # Values: + # - "": disabled + # - "all": report all contextless calls + # - "scope": report only if a context exists in the scope of the outermost function + # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only + # Default: "" + context: scope + + staticcheck: + # SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks + # Example (to disable some checks): [ "all", "-SA1000", "-SA1001"] + # Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] + checks: + - all + # Incorrect or missing package comment. + # https://staticcheck.dev/docs/checks/#ST1000 + - -ST1000 + # Use consistent method receiver names. + # https://staticcheck.dev/docs/checks/#ST1016 + - -ST1016 + # Omit embedded fields from selector expression. + # https://staticcheck.dev/docs/checks/#QF1008 + - -QF1008 + + usetesting: + # Enable/disable `os.TempDir()` detections. + # Default: false + os-temp-dir: true + + exclusions: + paths: + - gen/ + # Mode of the generated files analysis. + # + # - `strict`: sources are excluded by strictly following the Go generated file convention. + # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` + # This line must appear before the first non-comment, non-blank text in the file. + # https://go.dev/s/generatedcode + # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. + # - `disable`: disable the generated files exclusion. + # + # Default: strict + generated: lax + # Log a warning if an exclusion rule is unused. + # Default: false + warn-unused: true + # Predefined exclusion rules. + # Default: [] + presets: + - std-error-handling + - common-false-positives + # Excluding configuration per-path, per-linter, per-text and per-source. + rules: + - source: 'TODO' + linters: [ godot ] + - text: 'should have a package comment' + linters: [ revive ] + - text: 'exported \S+ \S+ should have comment( \(or a comment on this block\))? or be unexported' + linters: [ revive ] + - text: 'package comment should be of the form ".+"' + source: '// ?(nolint|TODO)' + linters: [ revive ] + - text: 'comment on exported \S+ \S+ should be of the form ".+"' + source: '// ?(nolint|TODO)' + linters: [ revive, staticcheck ] + - path: '_test\.go' + linters: + - bodyclose + - dupl + - errcheck + - funlen + - gochecknoglobals + - gocognit + - goconst + - gosec + - noctx + - wrapcheck + # Shadowing an error with this pattern is a standard pattern in the Go. + - source: 'if err := .*; err != nil' + linters: [ govet ] diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..e899fe1 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,102 @@ +# GoReleaser config for the four ans-openshell binaries: +# ans-openshell-middleware — OpenShell supervisor middleware (ANS-6 signing) +# ans-openshell-interceptor — OpenShell gateway interceptor (destination vetting) +# ans-ocsf-hydrator — OCSF audit export to Trust Index observations +# ans-reference-callee — demo callee verifying ANS-6 Method B callers +# +# All four are pure Go; the release matrix is linux + darwin × amd64 + arm64. +# Container images are built from deploy/Dockerfile and are not part of the +# GoReleaser release. +# Docs: https://goreleaser.com/customization/ +version: 2 + +project_name: ans-openshell + +before: + hooks: + - go mod tidy + +# Each main declares `var version`; the Makefile and deploy/Dockerfile stamp +# the same -X main.version target. +builds: + - id: ans-openshell-middleware + main: ./cmd/ans-openshell-middleware + binary: ans-openshell-middleware + env: [CGO_ENABLED=0] + goos: [linux, darwin] + goarch: [amd64, arm64] + mod_timestamp: "{{ .CommitTimestamp }}" + flags: [-trimpath] + ldflags: + - -s -w + - -X main.version={{.Version}} + + - id: ans-openshell-interceptor + main: ./cmd/ans-openshell-interceptor + binary: ans-openshell-interceptor + env: [CGO_ENABLED=0] + goos: [linux, darwin] + goarch: [amd64, arm64] + mod_timestamp: "{{ .CommitTimestamp }}" + flags: [-trimpath] + ldflags: + - -s -w + - -X main.version={{.Version}} + + - id: ans-ocsf-hydrator + main: ./cmd/ans-ocsf-hydrator + binary: ans-ocsf-hydrator + env: [CGO_ENABLED=0] + goos: [linux, darwin] + goarch: [amd64, arm64] + mod_timestamp: "{{ .CommitTimestamp }}" + flags: [-trimpath] + ldflags: + - -s -w + - -X main.version={{.Version}} + + - id: ans-reference-callee + main: ./cmd/ans-reference-callee + binary: ans-reference-callee + env: [CGO_ENABLED=0] + goos: [linux, darwin] + goarch: [amd64, arm64] + mod_timestamp: "{{ .CommitTimestamp }}" + flags: [-trimpath] + ldflags: + - -s -w + - -X main.version={{.Version}} + +# One archive per (goos, goarch), each containing all four binaries. +archives: + - id: ans-openshell + ids: + - ans-openshell-middleware + - ans-openshell-interceptor + - ans-ocsf-hydrator + - ans-reference-callee + name_template: "ans-openshell_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + formats: [tar.gz] + files: + - LICENSE + - README.md + - third_party/openshell/LICENSE + - third_party/openshell/README.md + +checksum: + name_template: checksums.txt + algorithm: sha256 + +snapshot: + version_template: "{{ incpatch .Version }}-snapshot-{{ .ShortCommit }}" + +# release-please owns the changelog and the GitHub Release body. GoReleaser +# only attaches binaries + checksums to the existing release. +changelog: + disable: true + +release: + mode: append + # owner/name are intentionally omitted: GoReleaser infers them from the CI + # environment / git remote, so releases keep working across org renames and + # transfers. diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a1d923 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +oss@godaddy.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e34815 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,181 @@ +# Contributing + +Everyone is welcome to contribute to GoDaddy's Open Source Software. +Contributing doesn’t just mean submitting pull requests. To get involved, +you can report or triage bugs, and participate in discussions on the +evolution of each project. + +No matter how you want to get involved, we ask that you first learn what’s +expected of anyone who participates in the project by reading the Contribution +Guidelines and our [Code of Conduct][coc]. + +## Answering Questions + +One of the most important and immediate ways you can support this project is +to answer questions on [Github][issues]. Whether you’re +helping a newcomer understand a feature or troubleshooting an edge case with a +seasoned developer, your knowledge and experience with a programming language +can go a long way to help others. + +## Reporting Bugs + +**Do not report potential security vulnerabilities here. Refer to +[SECURITY.md](./SECURITY.md) for more details about the process of reporting +security vulnerabilities.** + +Before submitting a ticket, please search our [Issue Tracker][issues] to make +sure it does not already exist and have a simple replication of the behavior. If +the issue is isolated to one of the dependencies of this project, please create +a Github issue in that project. All dependencies should be open source software +and can be found on Github. + +Submit a ticket for your issue, assuming one does not already exist: + +- Create it on the project's [issue Tracker][issues]. +- Clearly describe the issue by following the template layout + - Make sure to include steps to reproduce the bug. + - A reproducible (unit) test could be helpful in solving the bug. + - Describe the environment that (re)produced the problem. + +## Triaging bugs or contributing code + +If you're triaging a bug, first make sure that you can reproduce it. Once a bug +can be reproduced, reduce it to the smallest amount of code possible. Reasoning +about a sample or unit test that reproduces a bug in just a few lines of code +is easier than reasoning about a longer sample. + +From a practical perspective, contributions are as simple as: + +1. Fork and clone the repo, [see Github's instructions if you need help.][fork] +1. Create a branch for your PR with `git checkout -b pr/your-branch-name` +1. Make changes on the branch of your forked repository. +1. When committing, reference your issue (if present) and include a note about + the fix. +1. Please also add/update unit tests for your changes. +1. Push the changes to your fork and submit a pull request to the 'main + development branch' branch of the projects' repository. + +If you are interested in making a large change and feel unsure about its overall +effect, start with opening an Issue in the project's [Issue Tracker][issues] +with a high-level proposal and discuss it with the core contributors through +Github comments. After reaching a consensus with core +contributors about the change, discuss the best way to go about implementing it. + +> Tip: Keep your main branch pointing at the original repository and make +> pull requests from branches on your fork. To do this, run: +> +> ``` +> git remote add upstream https://github.com/agentnameservice/ans-openshell.git +> git fetch upstream +> git branch --set-upstream-to=upstream/main main +> ``` +> +> This will add the original repository as a "remote" called "upstream," Then +> fetch the git information from that remote, then set your local main +> branch to use the upstream main branch whenever you run git pull. Then you +> can make all of your pull request branches based on this main branch. +> Whenever you want to update your version of main, do a regular git pull. + +## Commit Messages + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) to +automate versioning and changelog generation. Commit messages must follow this format: + +``` +[optional scope]: +``` + +Common types: +- `feat:` — A new feature (minor version bump) +- `fix:` — A bug fix (patch version bump) +- `docs:` — Documentation changes +- `chore:` — Maintenance tasks +- `refactor:` — Code refactoring +- `test:` — Test additions or changes +- `ci:` — CI/CD changes +- `perf:` — Performance improvements + +Breaking changes: Append `!` after the type (e.g., `feat!: redesign API`) to signal +a breaking change. For a Go module, a major version bump (e.g., v1 to v2) requires +updating the module path to include `/v2`. Discuss with maintainers before introducing +breaking changes. + +Commits that do not follow this format are ignored by the release automation and will +not appear in the changelog or trigger a version bump. + +## Code Review + +Any open source project relies heavily on code review to improve software +quality. All significant changes, by all developers, must be reviewed before +they are committed to the repository. Code reviews are conducted on GitHub +through comments on pull requests or commits. The developer responsible for a +code change is also responsible for making all necessary review-related changes. + +Sometimes code reviews will take longer than you would hope for, especially for +larger features. Here are some accepted ways to speed up review times for your +patches: + +- Review other people’s changes. If you help out, others will more likely be + willing to do the same for you. +- Split your change into multiple smaller changes. The smaller your change, + the higher the probability that somebody will take a quick look at it. + +**Note that anyone is welcome to review and give feedback on a change, but only +people with commit access to the repository can approve it.** + +## Attribution of Changes + +When contributors submit a change to this project, after that change is +approved, other developers with commit access may commit it for the author. When +doing so, it is important to retain correct attribution of the contribution. +Generally speaking, Git handles attribution automatically. + +## Code Style and Documentation + +Ensure that your contribution follows the standards set by the project's style +guide with respect to patterns, naming, documentation and testing. + +## Developer Certificate of Origin + +This project follows the Linux Foundation contribution model. Every commit +must carry a `Signed-off-by:` trailer certifying the +[Developer Certificate of Origin](https://developercertificate.org) — your +assertion that you have the right to submit the change under the project's +license. Sign off each commit with: + +``` +git commit -s +``` + +Missing sign-offs on an existing branch can be fixed with +`git rebase --signoff origin/main` followed by a force-push. The `DCO` +status check — the org-wide [DCO app](https://github.com/apps/dco), the +same enforcement used across Linux Foundation projects — validates this +on every pull request; bot-authored commits (Dependabot, release-please) +and merge commits are exempt. + +## AI-Assisted Contributions + +AI-assisted contributions are welcome **with disclosure**, following the +[Linux kernel convention](https://docs.kernel.org/process/coding-assistants.html): + +- Disclose the tooling in the pull request's **AI assistance** section, + naming the tool and model, e.g. `Assisted-by: Claude Code (claude-fable-5)`. +- AI tools must never add `Signed-off-by:` lines. DCO certification belongs + to the human submitter, who remains fully responsible for the correctness + and licensing of the contribution. + +## Linking Issues + +Every pull request must reference an issue with a closing keyword (e.g. +`Fixes #123`). Open the issue first — issues are where triage and +prioritization happen. + +# Additional Resources + +- [General GitHub Documentation](https://help.github.com/) +- [GitHub Pull Request documentation](https://help.github.com/send-pull-requests/) + +[issues]: https://github.com/agentnameservice/ans-openshell/issues +[coc]: ./CODE_OF_CONDUCT.md +[fork]: https://help.github.com/en/articles/fork-a-repo \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f8386d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 GoDaddy Operating Company, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b04fb6b --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +GO ?= go +# golangci-lint must type-check with the Go version it was built against; override when the +# local toolchain is newer, e.g. LINT_GOTOOLCHAIN=go1.26.1. +LINT_GOTOOLCHAIN ?= local +BIN := bin +CMDS := ans-openshell-middleware ans-openshell-interceptor ans-ocsf-hydrator ans-reference-callee + +.PHONY: all build proto test cover lint vet demo clean + +all: build + +build: + @mkdir -p $(BIN) + @for c in $(CMDS); do echo "building $$c"; $(GO) build -o $(BIN)/$$c ./cmd/$$c; done + +# Generated code must match CI byte for byte. protoc-gen-go formats its output with the +# go/format of the toolchain that built it, so the plugins are built with the CI toolchain. +PROTO_GOTOOLCHAIN ?= go1.26.9 +PROTOC_GEN_GO_VERSION := v1.36.12 +PROTOC_GEN_GO_GRPC_VERSION := v1.6.2 + +proto: + @mkdir -p $(BIN) + GOBIN=$(abspath $(BIN)) GOTOOLCHAIN=$(PROTO_GOTOOLCHAIN) $(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO_VERSION) + GOBIN=$(abspath $(BIN)) GOTOOLCHAIN=$(PROTO_GOTOOLCHAIN) $(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC_VERSION) + PATH="$(abspath $(BIN)):$$PATH" buf generate + +test: + $(GO) test -race ./... + +cover: + $(GO) test -race -coverprofile=coverage.out -covermode=atomic ./... + @grep -v -e '/gen/' -e '/cmd/' -e '/examples/' coverage.out > coverage.lib.out + @$(GO) tool cover -func=coverage.lib.out | tail -1 + +lint: + GOTOOLCHAIN=$(LINT_GOTOOLCHAIN) golangci-lint run ./... + +vet: + $(GO) vet ./... + +demo: + $(GO) run ./examples/demo + +clean: + rm -rf $(BIN) coverage*.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..f322241 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# ans-openshell + +ANS identity for agents running in [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell). + +An agent inside an OpenShell sandbox never sees its own credentials: the supervisor outside the +sandbox injects them. This repository extends that model to agent identity. A supervisor +middleware signs every allowed outbound request as the sandbox's registered +[Agent Name Service](https://github.com/agentnameservice/ans-registry) (ANS) agent using +ANS-6 Method B (a DPoP proof plus the agent's transparency-log receipt and status token), so any +callee that implements ANS-6 can verify who is calling without the sandbox ever holding the +identity key. A gateway interceptor vets destination hosts against ANS registration before a +policy is committed, and a hydrator turns OpenShell's OCSF audit export into Trust Index +observations. + +| Binary | Purpose | +| --- | --- | +| `ans-openshell-middleware` | OpenShell supervisor middleware (`PRE_CREDENTIALS`): mints the ANS-6 proof, staples the receipt and status token, optionally vets the destination | +| `ans-openshell-interceptor` | OpenShell gateway interceptor (`validate`): denies or annotates proposed network policies whose hosts are not registered ANS agents | +| `ans-ocsf-hydrator` | Aggregates OCSF JSONL into signed Trust Index observations | +| `ans-reference-callee` | Demo callee that verifies ANS-6 Method B callers with the SDK's `pop.Middleware` | + +Example configurations for every service are under [deploy/examples](deploy/examples). The reason +codes the middleware and the interceptor return to the gateway are defined in +`internal/middleware/reasons.go` and `internal/vetting/verdict.go`. + +## Status + +v0.1, research preview. OpenShell's supervisor-middleware and gateway-interceptor contracts are +themselves marked as research previews upstream and may change. The extension is built against +the OpenShell protos recorded in `third_party/openshell/VERSION` and ANS SDK v0.1.19. + +## Quick start + +Build everything: + +```bash +make build +``` + +Run the in-process demo, which starts a fake transparency log, the middleware service, a +reference callee, and plays the supervisor: it sends an `EvaluateHttpRequest`, applies the +returned header mutations to a real HTTP request and shows the callee accepting the signed call +and rejecting a replay, a tampered body and an unsigned request: + +```bash +make demo +``` + +Run the tests: + +```bash +make cover +``` + +## Running the services + +Every service takes one YAML file. Flags: `--config` (required), `--listen` to override the +configured address, `--log-level` (`debug`, `info`, `warn`, `error`), `--log-format` (`json`, +`text`) and `--version`. Both services expose plaintext `GET /healthz` and `GET /readyz` on +`health_listen` (default `:8081`) for probes. + +```bash +./bin/ans-openshell-middleware --config deploy/examples/middleware.yaml +./bin/ans-openshell-interceptor --config deploy/examples/interceptor.yaml +``` + +The hydrator is a batch job over the sandbox supervisors' OCSF records. Enable OpenShell's JSON +export with `openshell settings set --global --key ocsf_json_enabled --value true`; each +supervisor then writes one `OCSF-JSON` line per event to its stderr and, where its `/var/log` is +writable, to `/var/log/openshell-ocsf..jsonl` inside the supervisor container. Collect +either with your log pipeline into a file the hydrator can read, one bare line per record: it +accepts raw supervisor stderr lines and plain JSONL, aggregates per agent and posts one batch. +`--window-last 15m` restricts a scheduled run to the trailing window so it does not re-post the +whole file. The gateway's own `ocsf_log` JSONL holds gateway-origin events only, not sandbox +traffic, so it is not the input. + +```bash +./bin/ans-ocsf-hydrator --config deploy/examples/hydrator.yaml --input supervisor-ocsf.jsonl --window-last 15m +``` + +`--dry-run` prints the import request instead of posting it; `--window-start` and +`--window-end` (RFC 3339) pin the observation window. The job exits non-zero when events were +parsed but none mapped to a configured agent, which usually means the sandbox mapping drifted; +`--allow-unmapped` accepts that. The Trust Index admin key comes from +`trust_index.admin_key_file` or the `ANS_TRUST_INDEX_ADMIN_KEY` environment variable. + +The reference callee takes flags only: + +```bash +./bin/ans-reference-callee --listen :8443 --root-keys-file tl-root-keys.txt --trusted-host callee.example:8443 +``` + +## Deploying with OpenShell + +1. Register the services in the gateway configuration: see + [deploy/examples/gateway.toml](deploy/examples/gateway.toml). The middleware needs + `max_payload_bytes` large enough for the request bodies your agents send, because the body is + hashed into the proof. +2. Configure the middleware with the identities sandboxes may sign as: + [deploy/examples/middleware.yaml](deploy/examples/middleware.yaml). Each identity is an ANS + registration (ANS name, agent id) plus its Identity Certificate and P-256 key, read from files + or mounted secrets. The transparency log base URL and its pinned root keys let the service + fetch and refresh the receipt and status token in the background. +3. Provision the gateway's JWKS (its `/.well-known/jwks.json`) as `auth.jwks_file` so the service + can authenticate the gateway and the sandbox supervisors on their first call. +4. Bind the middleware in sandbox policies: [deploy/examples/policy.yaml](deploy/examples/policy.yaml). + `config.identity` picks the identity; `config.destination_policy` chooses `off`, `observe` or + `require_registered`. After creating the sandbox, approve its gateway-issued ID in the + middleware service configuration: `sandbox_identities: {: payments-agent}`. + Restart the service (or upgrade its Helm values) to apply the approval. Until then, signing + is denied. Sandbox names and workspace labels cannot grant identity access. +5. Optionally register the interceptor ([deploy/examples/interceptor.yaml](deploy/examples/interceptor.yaml)) + to vet hosts when sandboxes are created, policies are updated, custom provider profiles are + imported or updated, and Advisor proposals are submitted, edited or approved. An operation + naming more than 64 destinations is denied under `require_registered`, so import large profile + sets in batches. Approving a stored draft installs a rule the request does not carry, so the + interceptor resolves it from the gateway state attached to the evaluation (`current_state`, + which must carry the draft policy's `chunks`); the pinned OpenShell release does not populate + that state, so under `require_registered` draft approvals are refused until it does, while + `observe` allows and annotates them. Proposals are vetted when submitted or edited, so before + moving a gateway from `observe` to `require_registered` clear its pending drafts. Proposals the + gateway auto-approves (`proposal_approval_mode` auto) never pass through the approve RPCs and + are vetted at submission only. Built-in provider profiles never pass through an interceptable + RPC, so the interceptor cannot vet their endpoints; the middleware still vets the destination + of every signed request, so under its `require_registered` list the built-in endpoints your + sandboxes use in the middleware's `vetting.exempt_hosts`, as exact hosts where possible: a + `*.suffix` pattern exempts every host under it. +6. Ship the collected supervisor OCSF feed to the hydrator ([deploy/examples/hydrator.yaml](deploy/examples/hydrator.yaml)) + on a schedule to feed the Trust Index; the Helm chart's CronJob reads `hydrator.input` from a + volume your log collector writes. The signal ids it emits are configurable; the Trust + Index must register them before it scores them. + +A Helm chart for both gRPC services is in [deploy/helm/ans-openshell](deploy/helm/ans-openshell) +and a multi-stage Dockerfile in [deploy/Dockerfile](deploy/Dockerfile). + +## How a signed request looks to the callee + +```http +POST /v1/tasks HTTP/1.1 +Host: tools.example.com +DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr... +X-SCITT-Receipt: 0oRDoQEmoQRQ... +X-ANS-Status-Token: 0oRDoQEmoQRQ... +Authorization: Bearer +``` + +The callee verifies the proof's signature under the certificate in `x5c`, checks the certificate +fingerprint against the status token's valid identity certificates, checks the certificate's +`ans://` name against the status token and the receipt, confirms `htm`, `htu` and +`ans_content_digest` match the request it received, and rejects a reused `jti`. The SDK's +`pop.Middleware` does all of this; `cmd/ans-reference-callee` shows the wiring. + +## Security notes + +- The middleware is a signing oracle for the identities it holds. It authenticates every caller + with the OpenShell extension JWT and only mints proofs for a supervisor's own sandbox. Do not + run it with `allow_insecure_no_auth` outside local development. +- Policy authors can request an identity, but the operator-owned `sandbox_identities` map must + authorize that exact sandbox ID and identity before any proof is issued. Missing bindings + deny signing, including in local unauthenticated mode. +- Stale or missing artifacts, signer errors and unverifiable destinations under + `require_registered` all deny. Bind the middleware with `on_error: fail_closed` and `order` + last so no later middleware rewrites a signed request. Signing rejects raw or percent-encoded + OpenShell credential placeholders in the body or URL path: OpenShell substitutes them after + `PRE_CREDENTIALS`, which would invalidate the proof. Credential headers and query-only + substitution remain supported. +- `require_registered` screens destination registration; the current OpenShell API does not + provide the actual TLS peer certificate, so ANS-6 server-certificate binding is not implemented. + Normal TLS CA/hostname verification still applies. +- OAuth `ath` binding is not available at `PRE_CREDENTIALS` because the injected bearer token is + not visible to middleware; proofs carry no `ath`, which RFC 9449 permits when no DPoP-bound + token is presented. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md): Developer Certificate of Origin sign-off on every commit, +Conventional Commits, and disclosure of AI assistance. Security issues go to the address in +[SECURITY.md](SECURITY.md). + +## License + +MIT (see [LICENSE](LICENSE)). The OpenShell protocol definitions under `third_party/openshell` +are Apache-2.0, copyright NVIDIA Corporation & Affiliates. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..fa83efc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Reporting Security Issues + +We take security very seriously at GoDaddy. We appreciate your efforts to +responsibly disclose your findings, and will make every effort to acknowledge +your contributions. + +## Where should I report security issues? + +In order to give the community time to respond and upgrade, we strongly urge you +report all security issues privately. + +To report a security issue in one of our Open Source projects email us directly +at **oss@godaddy.com** and include the word "SECURITY" in the subject line. + +This mail is delivered to our Open Source Security team. + +After the initial reply to your report, the team will keep you informed of the +progress being made towards a fix and announcement, and may ask for additional +information or guidance. diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..51ddc69 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,32 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package + path: extension.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1;extensionv1 + - file_option: go_package + path: supervisor_middleware.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1;middlewarev1 + - file_option: go_package + path: gateway_interceptor.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1;interceptorv1 + - file_option: go_package + path: openshell.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/openshellv1;openshellv1 + - file_option: go_package + path: sandbox.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1;sandboxv1 + - file_option: go_package + path: datamodel.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/datamodelv1;datamodelv1 + - file_option: go_package + path: options.proto + value: github.com/agentnameservice/ans-openshell/gen/openshell/optionsv1;optionsv1 +plugins: + - local: protoc-gen-go + out: . + opt: module=github.com/agentnameservice/ans-openshell + - local: protoc-gen-go-grpc + out: . + opt: module=github.com/agentnameservice/ans-openshell diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..bcfe669 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: third_party/openshell/proto +lint: + ignore: + - third_party/openshell/proto +breaking: + use: + - FILE diff --git a/cmd/ans-ocsf-hydrator/main.go b/cmd/ans-ocsf-hydrator/main.go new file mode 100644 index 0000000..3f5a422 --- /dev/null +++ b/cmd/ans-ocsf-hydrator/main.go @@ -0,0 +1,308 @@ +// Command ans-ocsf-hydrator turns OpenShell's OCSF audit export into signed +// behavioral observations for the ANS Trust Index. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "log/slog" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/hydrator" + "github.com/agentnameservice/ans-openshell/internal/logging" + "github.com/agentnameservice/ans-openshell/pkg/provenance" +) + +// version is set at build time with -ldflags "-X main.version=...". +var version = "dev" + +// adminKeyEnv names the environment variable that supplies the Trust Index +// admin key when trust_index.admin_key_file is not set. +const adminKeyEnv = "ANS_TRUST_INDEX_ADMIN_KEY" + +type fileConfig struct { + TrustIndex struct { + BaseURL string `yaml:"base_url"` + AdminKeyFile string `yaml:"admin_key_file"` + } `yaml:"trust_index"` + Provenance struct { + AIMID string `yaml:"aim_id"` + EvidenceURL string `yaml:"evidence_url"` + } `yaml:"provenance"` + Signals struct { + PolicyViolationRate *string `yaml:"policy_violation_rate"` + ProtocolAdherence *string `yaml:"protocol_adherence"` + } `yaml:"signals"` + Signing struct { + KeyFile string `yaml:"key_file"` + CertFile string `yaml:"cert_file"` + } `yaml:"signing"` + Agents []hydrator.AgentMapping `yaml:"agents"` +} + +type flags struct { + configPath, input, windowStart, windowEnd string + windowLast time.Duration + dryRun, allowUnmapped bool +} + +// A run that yields nothing is usually misconfiguration rather than a quiet +// period, so it fails instead of quietly starving the Trust Index: no events in +// a non-empty input is the shape of a feed with ocsf_json_enabled off or a +// collector wrapping the lines, and no attributed event at all is mapping drift. +var ( + errNoEvents = errors.New("input had lines but no OCSF events; check that ocsf_json_enabled is set and that the collector writes bare lines (set --allow-unmapped to accept this)") + errNothingAttributed = errors.New("events were parsed but none of them mapped to a configured agent (set --allow-unmapped to accept this)") +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ans-ocsf-hydrator:", err) + os.Exit(1) + } +} + +func run() error { + var f flags + flag.StringVar(&f.configPath, "config", "", "path to the YAML configuration (required)") + flag.StringVar(&f.input, "input", "-", "OCSF JSONL file, or - for stdin") + flag.BoolVar(&f.dryRun, "dry-run", false, "print the import request instead of posting it") + flag.BoolVar(&f.allowUnmapped, "allow-unmapped", false, "exit 0 when the input yields no OCSF events or none of them map to an agent") + flag.StringVar(&f.windowStart, "window-start", "", "RFC 3339 start of the observation window (default: first event)") + flag.StringVar(&f.windowEnd, "window-end", "", "RFC 3339 end of the observation window (default: last event)") + flag.DurationVar(&f.windowLast, "window-last", 0, "aggregate only the trailing duration before now, e.g. 15m; an alternative to --window-start/--window-end") + logLevel := flag.String("log-level", "info", "debug, info, warn or error") + logFormat := flag.String("log-format", "json", "json or text") + showVer := flag.Bool("version", false, "print the version and exit") + flag.Parse() + if *showVer { + fmt.Fprintln(os.Stdout, version) + return nil + } + if f.configPath == "" { + return errors.New("--config is required") + } + logger, err := logging.New(*logLevel, *logFormat, os.Stderr) + if err != nil { + return err + } + var cfg fileConfig + if err := config.Load(f.configPath, &cfg); err != nil { + return err + } + opts, err := buildOptions(cfg, f) + if err != nil { + return err + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + sink, err := buildSink(ctx, cfg, f.dryRun, logger) + if err != nil { + return err + } + in, closeInput, err := openInput(f.input) + if err != nil { + return err + } + defer closeInput() + + report, err := hydrator.Run(ctx, in, sink, opts, logger) + logReport(ctx, logger, cfg.Agents, report, err) + if err != nil { + return err + } + if err := emptyRunError(report, f.allowUnmapped); err != nil { + logger.ErrorContext(ctx, "hydration rejected", "err", err) + return err + } + return nil +} + +// emptyRunError decides whether a run that posted nothing is a failure. A +// batch that did map but carried no network or HTTP activity is a quiet period +// and succeeds without posting; so does one whose events all fell outside an +// explicit window. +func emptyRunError(report hydrator.Report, allowEmpty bool) error { + switch { + case allowEmpty: + return nil + case report.Parse.Lines > 0 && report.Parse.Events == 0: + return errNoEvents + case report.Unmapped > 0 && report.Attributed == 0: + return errNothingAttributed + } + return nil +} + +func buildOptions(cfg fileConfig, f flags) (hydrator.Options, error) { + if len(cfg.Agents) == 0 { + return hydrator.Options{}, errors.New("agents: at least one agent mapping is required") + } + agents, err := hydrator.NewAgentMap(cfg.Agents) + if err != nil { + return hydrator.Options{}, err + } + if cfg.Provenance.AIMID == "" { + return hydrator.Options{}, errors.New("provenance.aim_id is required") + } + signals, err := signalConfig(cfg) + if err != nil { + return hydrator.Options{}, err + } + opts := hydrator.Options{Agents: agents, Signals: signals, AIMID: cfg.Provenance.AIMID, EvidenceURL: cfg.Provenance.EvidenceURL} + if opts.Signer, err = batchSigner(cfg); err != nil { + return hydrator.Options{}, err + } + if opts.Window, err = windowFlags(f, time.Now()); err != nil { + return hydrator.Options{}, err + } + return opts, nil +} + +// windowFlags resolves the observation window: --window-last gives a trailing +// window ending now, the shape a scheduled job wants, and the explicit pair +// gives a fixed one. +func windowFlags(f flags, now time.Time) (*hydrator.Window, error) { + explicit := f.windowStart != "" || f.windowEnd != "" + switch { + case f.windowLast < 0: + return nil, errors.New("--window-last must be positive") + case f.windowLast > 0 && explicit: + return nil, errors.New("--window-last cannot be combined with --window-start or --window-end") + case f.windowLast > 0: + now = now.UTC() + return &hydrator.Window{Start: now.Add(-f.windowLast), End: now}, nil + case explicit: + w, err := parseWindow(f.windowStart, f.windowEnd) + if err != nil { + return nil, err + } + return &w, nil + } + return nil, nil //nolint:nilnil // no window selects the span of the input +} + +// signalConfig applies the optional signal id overrides; an empty id disables +// that signal. +func signalConfig(cfg fileConfig) (hydrator.SignalConfig, error) { + signals := hydrator.DefaultSignalConfig() + if cfg.Signals.PolicyViolationRate != nil { + signals.PolicyViolationRateID = *cfg.Signals.PolicyViolationRate + signals.EmitPolicyViolationRate = *cfg.Signals.PolicyViolationRate != "" + } + if cfg.Signals.ProtocolAdherence != nil { + signals.ProtocolAdherenceID = *cfg.Signals.ProtocolAdherence + signals.EmitProtocolAdherence = *cfg.Signals.ProtocolAdherence != "" + } + if !signals.EmitPolicyViolationRate && !signals.EmitProtocolAdherence { + return hydrator.SignalConfig{}, errors.New("signals: every signal is disabled; nothing would be posted") + } + return signals, nil +} + +// batchSigner loads the optional signing key pair; nil means unsigned batches. +func batchSigner(cfg fileConfig) (provenance.BatchSigner, error) { + if cfg.Signing.KeyFile == "" && cfg.Signing.CertFile == "" { + return nil, nil //nolint:nilnil // no signer configured is a valid, documented state + } + if cfg.Signing.KeyFile == "" || cfg.Signing.CertFile == "" { + return nil, errors.New("signing.key_file and signing.cert_file must both be set") + } + keyPEM, err := os.ReadFile(cfg.Signing.KeyFile) + if err != nil { + return nil, fmt.Errorf("signing.key_file: %w", err) + } + certPEM, err := os.ReadFile(cfg.Signing.CertFile) + if err != nil { + return nil, fmt.Errorf("signing.cert_file: %w", err) + } + return provenance.NewPEMSigner(keyPEM, certPEM) +} + +func parseWindow(start, end string) (hydrator.Window, error) { + if start == "" || end == "" { + return hydrator.Window{}, errors.New("--window-start and --window-end must be given together") + } + s, err := time.Parse(time.RFC3339, start) + if err != nil { + return hydrator.Window{}, fmt.Errorf("--window-start: %w", err) + } + e, err := time.Parse(time.RFC3339, end) + if err != nil { + return hydrator.Window{}, fmt.Errorf("--window-end: %w", err) + } + if !e.After(s) { + return hydrator.Window{}, errors.New("--window-end must be after --window-start") + } + return hydrator.Window{Start: s, End: e}, nil +} + +func buildSink(ctx context.Context, cfg fileConfig, dryRun bool, logger *slog.Logger) (hydrator.Sink, error) { + if dryRun { + return hydrator.NewWriterSink(os.Stdout), nil + } + if cfg.TrustIndex.BaseURL == "" { + return nil, errors.New("trust_index.base_url is required unless --dry-run is set") + } + key := os.Getenv(adminKeyEnv) + if cfg.TrustIndex.AdminKeyFile != "" { + b, err := os.ReadFile(cfg.TrustIndex.AdminKeyFile) + if err != nil { + return nil, fmt.Errorf("trust_index.admin_key_file: %w", err) + } + key = strings.TrimSpace(string(b)) + } + if key == "" { + logger.WarnContext(ctx, "no trust index admin key configured; posting without Authorization") + } + if strings.HasPrefix(strings.ToLower(cfg.TrustIndex.BaseURL), "http://") { + logger.WarnContext(ctx, "trust index base url is plaintext http; the admin key and observations travel in the clear") + } + sink, err := hydrator.NewHTTPSink(cfg.TrustIndex.BaseURL, key, nil) + if err != nil { + return nil, err + } + sink.UserAgent = "ans-ocsf-hydrator/" + version + return sink, nil +} + +func openInput(path string) (io.Reader, func(), error) { + if path == "-" { + return os.Stdin, func() {}, nil + } + f, err := os.Open(path) + if err != nil { + return nil, nil, fmt.Errorf("--input: %w", err) + } + return f, func() { _ = f.Close() }, nil +} + +func logReport(ctx context.Context, logger *slog.Logger, configured []hydrator.AgentMapping, r hydrator.Report, runErr error) { + if runErr != nil { + logger.ErrorContext(ctx, "hydration failed", "err", runErr) + } + logger.InfoContext(ctx, "hydration summary", + "lines", r.Parse.Lines, "events", r.Parse.Events, "skipped", r.Parse.Skipped, "attributed", r.Attributed, + "unmapped", r.Unmapped, "no_container", r.NoContainer, "out_of_window", r.OutOfWindow, + "agents", len(r.Agents), "observations", r.Observations, "posted", r.Posted, "signed", r.Signed, + "window_start", r.Window.Start.Format(time.RFC3339), "window_end", r.Window.End.Format(time.RFC3339)) + seen := make(map[string]bool, len(r.Agents)) + for _, a := range r.Agents { + seen[a.AgentID] = true + logger.InfoContext(ctx, "agent summary", "agent_id", a.AgentID, "ans_name", a.AnsName, + "events", a.Events, "denied", a.Denied, "observations", a.Observations) + } + for _, m := range configured { + if !seen[m.AgentID] { + logger.WarnContext(ctx, "configured agent received no events", "agent_id", m.AgentID, "ans_name", m.AnsName) + } + } +} diff --git a/cmd/ans-ocsf-hydrator/main_test.go b/cmd/ans-ocsf-hydrator/main_test.go new file mode 100644 index 0000000..610bbea --- /dev/null +++ b/cmd/ans-ocsf-hydrator/main_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "errors" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/hydrator" +) + +func TestEmptyRunError(t *testing.T) { + t.Parallel() + tests := []struct { + name string + report hydrator.Report + allowEmpty bool + wantErr error + }{ + {name: "empty input", report: hydrator.Report{}}, + {name: "lines without events", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 3, Skipped: 3}}, wantErr: errNoEvents}, + {name: "lines without events but allowed", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 3, Skipped: 3}}, allowEmpty: true}, + {name: "every event unmapped", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 2, Events: 2}, Unmapped: 2}, wantErr: errNothingAttributed}, + {name: "every event unmapped but allowed", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 2, Events: 2}, Unmapped: 2}, allowEmpty: true}, + {name: "attributed events without network activity", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 1, Events: 1}, Attributed: 1}}, + {name: "mixed attribution", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 3, Events: 3}, Attributed: 1, Unmapped: 2, Observations: 2, Posted: true}}, + {name: "every event out of the window", report: hydrator.Report{Parse: hydrator.ParseStats{Lines: 2, Events: 2}, OutOfWindow: 2}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := emptyRunError(tt.report, tt.allowEmpty); !errors.Is(err, tt.wantErr) { + t.Fatalf("emptyRunError() = %v, want %v", err, tt.wantErr) + } + }) + } +} + +func TestWindowFlags(t *testing.T) { + t.Parallel() + now := time.Date(2026, 10, 19, 16, 0, 0, 0, time.FixedZone("x", -5*3600)) + tests := []struct { + name string + flags flags + want *hydrator.Window + wantErr string + }{ + {name: "no window", flags: flags{}}, + {name: "trailing window ends now in UTC", flags: flags{windowLast: 15 * time.Minute}, + want: &hydrator.Window{Start: now.UTC().Add(-15 * time.Minute), End: now.UTC()}}, + {name: "explicit window", flags: flags{windowStart: "2026-10-19T10:00:00Z", windowEnd: "2026-10-19T11:00:00Z"}, + want: &hydrator.Window{Start: time.Date(2026, 10, 19, 10, 0, 0, 0, time.UTC), End: time.Date(2026, 10, 19, 11, 0, 0, 0, time.UTC)}}, + {name: "explicit window needs both ends", flags: flags{windowStart: "2026-10-19T10:00:00Z"}, wantErr: "together"}, + {name: "trailing and explicit are exclusive", flags: flags{windowLast: time.Minute, windowEnd: "2026-10-19T11:00:00Z"}, wantErr: "cannot be combined"}, + {name: "negative trailing window", flags: flags{windowLast: -time.Minute}, wantErr: "positive"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := windowFlags(tt.flags, now) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("windowFlags() error = %v, want %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("windowFlags() error = %v", err) + } + if (got == nil) != (tt.want == nil) || (got != nil && (!got.Start.Equal(tt.want.Start) || !got.End.Equal(tt.want.End))) { + t.Fatalf("windowFlags() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/cmd/ans-openshell-interceptor/main.go b/cmd/ans-openshell-interceptor/main.go new file mode 100644 index 0000000..8d6bb71 --- /dev/null +++ b/cmd/ans-openshell-interceptor/main.go @@ -0,0 +1,186 @@ +// Command ans-openshell-interceptor serves the OpenShell gateway interceptor +// that vets destination hosts in proposed sandbox policies against ANS. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "google.golang.org/grpc" + + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/grpcserver" + "github.com/agentnameservice/ans-openshell/internal/interceptor" + "github.com/agentnameservice/ans-openshell/internal/logging" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// version is set at build time with -ldflags "-X main.version=...". +var version = "dev" + +const ( + defaultListen = ":50052" + defaultHealthListen = ":8081" + defaultPolicy = "observe" + drainDelay = 3 * time.Second + healthCheckInterval = 5 * time.Second +) + +type fileConfig struct { + Listen string `yaml:"listen"` + // HealthListen serves plaintext HTTP /healthz and /readyz for probes. + // Empty disables it; the default is :8081. + HealthListen string `yaml:"health_listen"` + TLS config.TLS `yaml:"tls"` + Auth config.Auth `yaml:"auth"` + // DestinationPolicy is observe (default) or require_registered; off + // disables vetting. + DestinationPolicy string `yaml:"destination_policy"` + // LookupWait bounds how long one require_registered evaluation waits for + // cold lookups. Keep it below the registration's timeout in the gateway + // configuration: the gateway does not propagate its deadline. + LookupWait time.Duration `yaml:"lookup_wait"` + Vetting config.Vetting `yaml:"vetting"` +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ans-openshell-interceptor:", err) + os.Exit(1) + } +} + +func run() error { + var ( + configPath = flag.String("config", "", "path to the YAML configuration (required)") + listen = flag.String("listen", "", "listen address; overrides the configuration file") + logLevel = flag.String("log-level", "info", "debug, info, warn or error") + logFormat = flag.String("log-format", "json", "json or text") + showVer = flag.Bool("version", false, "print the version and exit") + ) + flag.Parse() + if *showVer { + fmt.Fprintln(os.Stdout, version) + return nil + } + if *configPath == "" { + return errors.New("--config is required") + } + logger, err := logging.New(*logLevel, *logFormat, os.Stderr) + if err != nil { + return err + } + var cfg fileConfig + if err := config.Load(*configPath, &cfg); err != nil { + return err + } + if *listen != "" { + cfg.Listen = *listen + } + if cfg.Listen == "" { + cfg.Listen = defaultListen + } + if cfg.HealthListen == "" { + cfg.HealthListen = defaultHealthListen + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + return serve(ctx, cfg, logger) +} + +func serve(ctx context.Context, cfg fileConfig, logger *slog.Logger) error { + svcCfg, err := serviceConfig(cfg) + if err != nil { + return err + } + auth, err := extauth.NewVerifier(cfg.Auth, logger) + if err != nil { + return err + } + go auth.Run(ctx) + if !cfg.TLS.Enabled() && !cfg.Auth.AllowInsecureNoAuth { + logger.WarnContext(ctx, "listening without TLS while extension authentication is on; gateway bearer tokens travel in the clear") + } + + vet, err := vetting.New(cfg.Vetting, vetting.Deps{}, logger) + if err != nil { + return err + } + defer vet.Close() + + srv, err := grpcserver.New(ctx, grpcserver.Options{ + Listen: cfg.Listen, + HealthListen: cfg.HealthListen, + TLS: cfg.TLS, + Unary: []grpc.UnaryServerInterceptor{auth.UnaryInterceptor()}, + Stream: []grpc.StreamServerInterceptor{auth.StreamInterceptor()}, + DrainDelay: drainDelay, + }, logger) + if err != nil { + return err + } + svc := interceptor.NewService(vet, svcCfg, logger) + interceptorv1.RegisterGatewayInterceptorServer(srv.Registrar(), svc) + go srv.WatchReadiness(ctx, healthCheckInterval, readinessProbe(ctx, auth, logger)) + logger.InfoContext(ctx, "ans-openshell-interceptor starting", "version", version, "mode", svcCfg.Mode.String()) + return srv.Serve(ctx) +} + +// serviceConfig maps the file configuration onto the interceptor service. The +// destination policy defaults to observe. vetting.request_budget is a +// middleware setting and is refused rather than silently ignored: the +// interceptor bounds lookups with lookup_wait. +func serviceConfig(cfg fileConfig) (interceptor.Config, error) { + if cfg.Vetting.RequestBudget != 0 { + return interceptor.Config{}, errors.New("vetting.request_budget applies to the middleware; the interceptor bounds lookups with lookup_wait") + } + if cfg.LookupWait < 0 { + return interceptor.Config{}, errors.New("lookup_wait must not be negative") + } + policy := cfg.DestinationPolicy + if policy == "" { + policy = defaultPolicy + } + mode, err := vetting.ParseMode(policy) + if err != nil { + return interceptor.Config{}, err + } + return interceptor.Config{ + Audience: cfg.Auth.Audience, + Mode: mode, + ImplementationVersion: version, + LookupWait: cfg.LookupWait, + }, nil +} + +// keyHolder reports whether a gateway verification key is held; +// extauth.Verifier satisfies it. +type keyHolder interface{ Ready() bool } + +// readinessProbe ties readiness to the verifier holding a gateway key and logs +// every change of state, the first evaluation included. +func readinessProbe(ctx context.Context, auth keyHolder, logger *slog.Logger) func() bool { + var last *bool + return func() bool { + ready := auth.Ready() + if last == nil || *last != ready { + level := slog.LevelWarn + if ready { + level = slog.LevelInfo + } + logger.LogAttrs(ctx, level, "readiness evaluated", + slog.Bool("ready", ready), slog.String("reason", "gateway verification keys")) + } + last = &ready + return ready + } +} diff --git a/cmd/ans-openshell-interceptor/main_test.go b/cmd/ans-openshell-interceptor/main_test.go new file mode 100644 index 0000000..fdb9554 --- /dev/null +++ b/cmd/ans-openshell-interceptor/main_test.go @@ -0,0 +1,179 @@ +package main + +import ( + "bytes" + "context" + "log/slog" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/interceptor" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +func TestServiceConfig(t *testing.T) { + t.Parallel() + tests := []struct { + name string + cfg fileConfig + want interceptor.Config + wantErr string + }{ + { + name: "defaults to observe", + cfg: fileConfig{Auth: config.Auth{Audience: "aud"}}, + want: interceptor.Config{Audience: "aud", Mode: vetting.ModeObserve, ImplementationVersion: version}, + }, + { + name: "require_registered with lookup wait", + cfg: fileConfig{DestinationPolicy: "require_registered", LookupWait: 250 * time.Millisecond}, + want: interceptor.Config{Mode: vetting.ModeRequire, LookupWait: 250 * time.Millisecond, ImplementationVersion: version}, + }, + {name: "off", cfg: fileConfig{DestinationPolicy: "off"}, want: interceptor.Config{Mode: vetting.ModeOff, ImplementationVersion: version}}, + {name: "unknown policy", cfg: fileConfig{DestinationPolicy: "maybe"}, wantErr: "destination_policy"}, + {name: "request_budget belongs to the middleware", cfg: fileConfig{Vetting: config.Vetting{RequestBudget: time.Second}}, wantErr: "request_budget"}, + {name: "negative lookup wait", cfg: fileConfig{LookupWait: -time.Second}, wantErr: "lookup_wait"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := serviceConfig(tt.cfg) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("serviceConfig() error = %v, want %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("serviceConfig() error = %v", err) + } + if got != tt.want { + t.Fatalf("serviceConfig() = %+v, want %+v", got, tt.want) + } + }) + } +} + +type fakeKeys struct{ ready bool } + +func (f *fakeKeys) Ready() bool { return f.ready } + +func TestReadinessProbe(t *testing.T) { + t.Parallel() + tests := []struct { + name string + states []bool + wantLogs int + wantLevel string + }{ + {name: "no gateway key", states: []bool{false}, wantLogs: 1, wantLevel: "WARN"}, + {name: "key held", states: []bool{true}, wantLogs: 1, wantLevel: "INFO"}, + {name: "unchanged state is logged once", states: []bool{true, true}, wantLogs: 1, wantLevel: "INFO"}, + {name: "loss then recovery", states: []bool{true, false, true}, wantLogs: 3, wantLevel: "INFO"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var logs bytes.Buffer + keys := &fakeKeys{} + probe := readinessProbe(t.Context(), keys, slog.New(slog.NewTextHandler(&logs, nil))) + for i, st := range tt.states { + keys.ready = st + if got := probe(); got != st { + t.Fatalf("probe() #%d = %v, want %v", i+1, got, st) + } + } + if n := strings.Count(logs.String(), "readiness evaluated"); n != tt.wantLogs { + t.Fatalf("logged %d readiness evaluations, want %d:\n%s", n, tt.wantLogs, logs.String()) + } + lines := strings.Split(strings.TrimSpace(logs.String()), "\n") + if last := lines[len(lines)-1]; !strings.Contains(last, "level="+tt.wantLevel) { + t.Fatalf("last readiness log %q, want level %s", last, tt.wantLevel) + } + }) + } +} + +func TestServeNotReadyWithoutGatewayKeys(t *testing.T) { + t.Parallel() + cfg := fileConfig{ + Listen: "127.0.0.1:0", + HealthListen: "127.0.0.1:0", + Auth: config.Auth{GatewayIssuer: "openshell-gateway:test", Audience: "urn:openshell:extension:interceptor:ans", + JWKSURL: "https://127.0.0.1:1/.well-known/jwks.json"}, + Vetting: config.Vetting{TrustedTLDomains: []string{"tl.example.com"}}, + } + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + logs := newHealthLogWatcher() + done := make(chan error, 1) + go func() { done <- serve(ctx, cfg, slog.New(slog.NewTextHandler(logs, nil))) }() + if code := readyzStatus(ctx, t, logs); code != http.StatusServiceUnavailable { + t.Fatalf("/readyz without gateway keys = %d, want 503", code) + } + cancel() + if err := <-done; err != nil { + t.Fatalf("serve() error = %v", err) + } + if out := logs.String(); !strings.Contains(out, "readiness evaluated") || !strings.Contains(out, "ready=false") { + t.Fatalf("readiness was not evaluated as not ready:\n%s", out) + } +} + +// healthLogWatcher captures the service log and reports the health listener's +// bound address once grpcserver logs it. +type healthLogWatcher struct { + mu sync.Mutex + buf bytes.Buffer + addr chan string + once sync.Once +} + +func newHealthLogWatcher() *healthLogWatcher { return &healthLogWatcher{addr: make(chan string, 1)} } + +func (w *healthLogWatcher) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + w.buf.Write(p) + const marker = `msg="health endpoints listening" addr=` + if i := bytes.Index(p, []byte(marker)); i >= 0 { + rest := p[i+len(marker):] + if end := bytes.IndexAny(rest, " \n"); end >= 0 { + rest = rest[:end] + } + w.once.Do(func() { w.addr <- string(rest) }) + } + return len(p), nil +} + +func (w *healthLogWatcher) String() string { + w.mu.Lock() + defer w.mu.Unlock() + return w.buf.String() +} + +// readyzStatus waits for the health listener to be announced and returns the +// first /readyz status it answers with. +func readyzStatus(ctx context.Context, t *testing.T, logs *healthLogWatcher) int { + t.Helper() + var addr string + select { + case addr = <-logs.addr: + case <-ctx.Done(): + t.Fatalf("health listener was never announced:\n%s", logs.String()) + } + for range 50 { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+addr+"/readyz", nil) + if resp, err := http.DefaultClient.Do(req); err == nil { + _ = resp.Body.Close() + return resp.StatusCode + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("GET /readyz on %s never answered", addr) + return 0 +} diff --git a/cmd/ans-openshell-middleware/main.go b/cmd/ans-openshell-middleware/main.go new file mode 100644 index 0000000..dc6b945 --- /dev/null +++ b/cmd/ans-openshell-middleware/main.go @@ -0,0 +1,212 @@ +// Command ans-openshell-middleware serves the OpenShell supervisor middleware +// that signs a sandbox's outbound requests as its registered ANS agent. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "slices" + "syscall" + "time" + + "google.golang.org/grpc" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/grpcserver" + "github.com/agentnameservice/ans-openshell/internal/identity" + "github.com/agentnameservice/ans-openshell/internal/logging" + "github.com/agentnameservice/ans-openshell/internal/middleware" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// version is set at build time with -ldflags "-X main.version=...". +var version = "dev" + +const ( + defaultListen = ":50051" + defaultHealthListen = ":8081" + healthCheckInterval = 5 * time.Second + drainDelay = 3 * time.Second +) + +type fileConfig struct { + Listen string `yaml:"listen"` + // HealthListen serves plaintext HTTP /healthz and /readyz for probes. + // Empty disables it; the default is :8081. + HealthListen string `yaml:"health_listen"` + TLS config.TLS `yaml:"tls"` + Auth config.Auth `yaml:"auth"` + TransparencyLog config.TransparencyLog `yaml:"transparency_log"` + Identities map[string]identity.Config `yaml:"identities"` + SandboxIdentities map[string]string `yaml:"sandbox_identities"` + // Vetting enables destination vetting when present; policies may only set + // destination_policy if it is. + Vetting *config.Vetting `yaml:"vetting"` + // MaxPayloadBytes and RequestTimeout are advertised in the manifest binding. + MaxPayloadBytes uint64 `yaml:"max_payload_bytes"` + RequestTimeout time.Duration `yaml:"request_timeout"` +} + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ans-openshell-middleware:", err) + os.Exit(1) + } +} + +func run() error { + var ( + configPath = flag.String("config", "", "path to the YAML configuration (required)") + listen = flag.String("listen", "", "listen address; overrides the configuration file") + logLevel = flag.String("log-level", "info", "debug, info, warn or error") + logFormat = flag.String("log-format", "json", "json or text") + showVer = flag.Bool("version", false, "print the version and exit") + ) + flag.Parse() + if *showVer { + fmt.Fprintln(os.Stdout, version) + return nil + } + if *configPath == "" { + return errors.New("--config is required") + } + logger, err := logging.New(*logLevel, *logFormat, os.Stderr) + if err != nil { + return err + } + var cfg fileConfig + if err := config.Load(*configPath, &cfg); err != nil { + return err + } + if *listen != "" { + cfg.Listen = *listen + } + if cfg.Listen == "" { + cfg.Listen = defaultListen + } + if cfg.HealthListen == "" { + cfg.HealthListen = defaultHealthListen + } + if len(cfg.Identities) == 0 { + return errors.New("identities: at least one identity is required") + } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + return serve(ctx, cfg, logger) +} + +func serve(ctx context.Context, cfg fileConfig, logger *slog.Logger) error { + auth, err := extauth.NewVerifier(cfg.Auth, logger) + if err != nil { + return err + } + go auth.Run(ctx) + if !cfg.TLS.Enabled() && !cfg.Auth.AllowInsecureNoAuth { + logger.WarnContext(ctx, "listening without TLS while extension authentication is on; gateway bearer tokens travel in the clear") + } + + registry, err := identity.NewRegistry(ctx, cfg.Identities, cfg.TransparencyLog, logger) + if err != nil { + return err + } + defer registry.Close() + authz, err := middleware.NewSandboxIdentities(cfg.SandboxIdentities, registry) + if err != nil { + return err + } + + var vet vetting.Verifier + if cfg.Vetting != nil { + vetSvc, vetErr := vetting.New(*cfg.Vetting, vetting.Deps{}, logger) + if vetErr != nil { + return vetErr + } + defer vetSvc.Close() + vet = vetSvc + } + + srv, err := grpcserver.New(ctx, grpcserver.Options{ + Listen: cfg.Listen, + HealthListen: cfg.HealthListen, + TLS: cfg.TLS, + Unary: []grpc.UnaryServerInterceptor{auth.UnaryInterceptor()}, + Stream: []grpc.StreamServerInterceptor{auth.StreamInterceptor()}, + DrainDelay: drainDelay, + }, logger) + if err != nil { + return err + } + svc := middleware.NewService(registry, authz, vet, serviceConfig(cfg), logger) + middlewarev1.RegisterSupervisorMiddlewareServer(srv.Registrar(), svc) + go srv.WatchReadiness(ctx, healthCheckInterval, readinessProbe(ctx, registry, auth, logger)) + logger.InfoContext(ctx, "ans-openshell-middleware starting", + "version", version, "identities", registry.Names(), "vetting", vet != nil) + return srv.Serve(ctx) +} + +// serviceConfig maps the file configuration onto the middleware service. +func serviceConfig(cfg fileConfig) middleware.Config { + c := middleware.Config{ + Audience: cfg.Auth.Audience, + MaxPayloadBytes: cfg.MaxPayloadBytes, + RequestTimeout: cfg.RequestTimeout, + ImplementationVersion: version, + RequireAuthenticatedCaller: !cfg.Auth.AllowInsecureNoAuth, + } + if cfg.Vetting != nil { + c.VettingBudget = cfg.Vetting.RequestBudget + } + return c +} + +// keyHolder reports whether a gateway verification key is held; +// extauth.Verifier satisfies it. +type keyHolder interface{ Ready() bool } + +// identityHealth reports the configured identities and which of them lack +// usable artifacts; identity.Registry satisfies it. +type identityHealth interface { + Names() []string + Unhealthy() []string +} + +// readinessProbe ties readiness to what the replica can actually do: it must +// hold a gateway verification key and at least one identity with usable +// artifacts. A single identity whose artifacts lapse is denied per request and +// logged here, but does not take the other identities out of service. Every +// change of state is logged, the first evaluation included. +func readinessProbe(ctx context.Context, registry identityHealth, auth keyHolder, logger *slog.Logger) func() bool { + var last *healthState + return func() bool { + next := healthState{authReady: auth.Ready(), unhealthy: registry.Unhealthy(), total: len(registry.Names())} + if last == nil || next.ready() != last.ready() || !slices.Equal(next.unhealthy, last.unhealthy) { + logger.LogAttrs(ctx, next.level(), "readiness evaluated", + slog.Bool("ready", next.ready()), slog.Bool("gateway_keys", next.authReady), + slog.Any("unhealthy_identities", next.unhealthy)) + } + last = &next + return next.ready() + } +} + +type healthState struct { + authReady bool + unhealthy []string + total int +} + +func (h healthState) ready() bool { return h.authReady && len(h.unhealthy) < h.total } + +func (h healthState) level() slog.Level { + if h.ready() && len(h.unhealthy) == 0 { + return slog.LevelInfo + } + return slog.LevelWarn +} diff --git a/cmd/ans-openshell-middleware/main_test.go b/cmd/ans-openshell-middleware/main_test.go new file mode 100644 index 0000000..fd20ec9 --- /dev/null +++ b/cmd/ans-openshell-middleware/main_test.go @@ -0,0 +1,283 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "maps" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/middleware" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func TestServeSandboxApprovals(t *testing.T) { + t.Parallel() + const startupTimeout = 10 * time.Second + for _, tt := range []struct { + name string + approval string + want map[string]string + wantErr string + }{ + {name: "approved sandbox", approval: "{sb-1: test-agent}", want: map[string]string{"sb-1": "test-agent"}}, + {name: "empty approvals", approval: "{}", want: map[string]string{}}, + {name: "unknown identity", approval: "{sb-1: unknown}", want: map[string]string{"sb-1": "unknown"}, wantErr: "sandbox_identities: sandbox \"sb-1\" names unknown identity"}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfg := loadStartupConfig(t, tt.approval, " allow_insecure_no_auth: true", "") + if !maps.Equal(cfg.SandboxIdentities, tt.want) { + t.Fatalf("loaded sandbox approvals = %v, want %v", cfg.SandboxIdentities, tt.want) + } + if tt.wantErr != "" { + cfg.Listen = "not-an-address" + } + ctx, cancel := context.WithTimeout(t.Context(), startupTimeout) + defer cancel() + logs := &startupLogWriter{cancel: cancel} + err := serve(ctx, cfg, slog.New(slog.NewTextHandler(logs, nil))) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("serve() error = %v, want %q before listener validation", err, tt.wantErr) + } + if logs.started.Load() { + t.Fatal("invalid approval reached server startup") + } + return + } + if err != nil || !logs.started.Load() { + t.Fatalf("serve() = %v, started = %v", err, logs.started.Load()) + } + }) + } +} + +// loadStartupConfig writes a complete middleware configuration with one static +// identity; auth is the indented body of the auth block. +func loadStartupConfig(t *testing.T, approval, auth, healthListen string) fileConfig { + t.Helper() + dir := t.TempDir() + id := testfixtures.MustNewIdentity("test-agent-id", "ans://v1.0.0.test.example") + tl := testfixtures.MustNewTL() + for name, content := range map[string][]byte{ + "key.pem": id.KeyPEM, + "cert.pem": id.CertPEM, + "receipt.cose": tl.Receipt(id.AgentID, id.AnsName), + "status.cose": tl.FreshStatusToken(id), + "roots.txt": []byte(tl.RootKeyC2SP()), + } { + if err := os.WriteFile(filepath.Join(dir, name), content, 0o600); err != nil { + t.Fatal(err) + } + } + yaml := fmt.Sprintf(`listen: "127.0.0.1:0" +health_listen: %q +auth: +%s +transparency_log: + root_keys_file: %q +identities: + test-agent: + ans_name: %q + agent_id: %q + key_file: %q + cert_file: %q + receipt_file: %q + status_token_file: %q +sandbox_identities: %s +`, healthListen, auth, filepath.Join(dir, "roots.txt"), id.AnsName, id.AgentID, + filepath.Join(dir, "key.pem"), filepath.Join(dir, "cert.pem"), + filepath.Join(dir, "receipt.cose"), filepath.Join(dir, "status.cose"), approval) + path := filepath.Join(dir, "middleware.yaml") + if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil { + t.Fatal(err) + } + var cfg fileConfig + if err := config.Load(path, &cfg); err != nil { + t.Fatal(err) + } + return cfg +} + +type startupLogWriter struct { + cancel context.CancelFunc + started atomic.Bool +} + +func (w *startupLogWriter) Write(p []byte) (int, error) { + if bytes.Contains(p, []byte("ans-openshell-middleware starting")) { + w.started.Store(true) + w.cancel() + } + return len(p), nil +} + +func TestServiceConfig(t *testing.T) { + t.Parallel() + tests := []struct { + name string + cfg fileConfig + want middleware.Config + }{ + { + name: "vetting budget propagates", + cfg: fileConfig{Auth: config.Auth{Audience: "aud"}, MaxPayloadBytes: 1024, RequestTimeout: time.Second, + Vetting: &config.Vetting{RequestBudget: 150 * time.Millisecond}}, + want: middleware.Config{Audience: "aud", MaxPayloadBytes: 1024, RequestTimeout: time.Second, + ImplementationVersion: version, VettingBudget: 150 * time.Millisecond, RequireAuthenticatedCaller: true}, + }, + { + name: "no vetting block", + cfg: fileConfig{Auth: config.Auth{Audience: "aud"}}, + want: middleware.Config{Audience: "aud", ImplementationVersion: version, RequireAuthenticatedCaller: true}, + }, + { + name: "insecure local mode does not require an authenticated caller", + cfg: fileConfig{Auth: config.Auth{AllowInsecureNoAuth: true}}, + want: middleware.Config{ImplementationVersion: version}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := serviceConfig(tt.cfg); got != tt.want { + t.Fatalf("serviceConfig() = %+v, want %+v", got, tt.want) + } + }) + } +} + +type fakeHealth struct { + ready bool + names []string + unhealthy []string +} + +func (f *fakeHealth) Ready() bool { return f.ready } +func (f *fakeHealth) Names() []string { return f.names } +func (f *fakeHealth) Unhealthy() []string { return f.unhealthy } + +func TestReadinessProbe(t *testing.T) { + t.Parallel() + tests := []struct { + name string + states []fakeHealth + want []bool + wantLogs int + wantLevel string + }{ + {name: "no gateway key with healthy identities", states: []fakeHealth{{names: []string{"a"}}}, want: []bool{false}, wantLogs: 1, wantLevel: "WARN"}, + {name: "key held and identities healthy", states: []fakeHealth{{ready: true, names: []string{"a"}}}, want: []bool{true}, wantLogs: 1, wantLevel: "INFO"}, + {name: "one unhealthy identity of two stays ready", states: []fakeHealth{{ready: true, names: []string{"a", "b"}, unhealthy: []string{"b"}}}, want: []bool{true}, wantLogs: 1, wantLevel: "WARN"}, + {name: "every identity unhealthy", states: []fakeHealth{{ready: true, names: []string{"a"}, unhealthy: []string{"a"}}}, want: []bool{false}, wantLogs: 1, wantLevel: "WARN"}, + {name: "unchanged state is logged once", states: []fakeHealth{{names: []string{"a"}}, {names: []string{"a"}}}, want: []bool{false, false}, wantLogs: 1, wantLevel: "WARN"}, + {name: "recovery is applied and logged", states: []fakeHealth{{names: []string{"a"}}, {ready: true, names: []string{"a"}}}, want: []bool{false, true}, wantLogs: 2, wantLevel: "INFO"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var logs bytes.Buffer + src := &fakeHealth{} + probe := readinessProbe(t.Context(), src, src, slog.New(slog.NewTextHandler(&logs, nil))) + for i, st := range tt.states { + *src = st + if got := probe(); got != tt.want[i] { + t.Fatalf("probe() #%d = %v, want %v", i+1, got, tt.want[i]) + } + } + if n := strings.Count(logs.String(), "readiness evaluated"); n != tt.wantLogs { + t.Fatalf("logged %d readiness evaluations, want %d:\n%s", n, tt.wantLogs, logs.String()) + } + lines := strings.Split(strings.TrimSpace(logs.String()), "\n") + if last := lines[len(lines)-1]; !strings.Contains(last, "level="+tt.wantLevel) { + t.Fatalf("last readiness log %q, want level %s", last, tt.wantLevel) + } + }) + } +} + +func TestServeNotReadyWithoutGatewayKeys(t *testing.T) { + t.Parallel() + cfg := loadStartupConfig(t, "{sb-1: test-agent}", + " gateway_issuer: openshell-gateway:test\n audience: urn:openshell:extension:middleware:ans\n jwks_url: https://127.0.0.1:1/.well-known/jwks.json", + "127.0.0.1:0") + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + logs := newHealthLogWatcher() + done := make(chan error, 1) + go func() { done <- serve(ctx, cfg, slog.New(slog.NewTextHandler(logs, nil))) }() + if code := readyzStatus(ctx, t, logs); code != http.StatusServiceUnavailable { + t.Fatalf("/readyz without gateway keys = %d, want 503", code) + } + cancel() + if err := <-done; err != nil { + t.Fatalf("serve() error = %v", err) + } + if out := logs.String(); !strings.Contains(out, "readiness evaluated") || !strings.Contains(out, "gateway_keys=false") { + t.Fatalf("readiness was not evaluated as lacking gateway keys:\n%s", out) + } +} + +// healthLogWatcher captures the service log and reports the health listener's +// bound address once grpcserver logs it. +type healthLogWatcher struct { + mu sync.Mutex + buf bytes.Buffer + addr chan string + once sync.Once +} + +func newHealthLogWatcher() *healthLogWatcher { return &healthLogWatcher{addr: make(chan string, 1)} } + +func (w *healthLogWatcher) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + w.buf.Write(p) + const marker = `msg="health endpoints listening" addr=` + if i := bytes.Index(p, []byte(marker)); i >= 0 { + rest := p[i+len(marker):] + if end := bytes.IndexAny(rest, " \n"); end >= 0 { + rest = rest[:end] + } + w.once.Do(func() { w.addr <- string(rest) }) + } + return len(p), nil +} + +func (w *healthLogWatcher) String() string { + w.mu.Lock() + defer w.mu.Unlock() + return w.buf.String() +} + +// readyzStatus waits for the health listener to be announced and returns the +// first /readyz status it answers with. +func readyzStatus(ctx context.Context, t *testing.T, logs *healthLogWatcher) int { + t.Helper() + var addr string + select { + case addr = <-logs.addr: + case <-ctx.Done(): + t.Fatalf("health listener was never announced:\n%s", logs.String()) + } + for range 50 { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+addr+"/readyz", nil) + if resp, err := http.DefaultClient.Do(req); err == nil { + _ = resp.Body.Close() + return resp.StatusCode + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("GET /readyz on %s never answered", addr) + return 0 +} diff --git a/cmd/ans-reference-callee/main.go b/cmd/ans-reference-callee/main.go new file mode 100644 index 0000000..195abdb --- /dev/null +++ b/cmd/ans-reference-callee/main.go @@ -0,0 +1,86 @@ +// Command ans-reference-callee runs an HTTP service that authenticates callers +// with ANS-6 Method B and echoes the proven identity. +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/agentnameservice/ans-openshell/internal/callee" +) + +const ( + readHeaderTimeout = 10 * time.Second + shutdownTimeout = 5 * time.Second +) + +type stringList []string + +func (s *stringList) String() string { return strings.Join(*s, ",") } +func (s *stringList) Set(v string) error { *s = append(*s, v); return nil } + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ans-reference-callee:", err) + os.Exit(1) + } +} + +func run() error { + var ( + listen = flag.String("listen", ":8443", "address to listen on") + rootKeysFile = flag.String("root-keys-file", "", "file of newline-delimited C2SP transparency log root keys (required)") + tlsCert = flag.String("tls-cert", "", "serving certificate PEM (plain HTTP when empty)") + tlsKey = flag.String("tls-key", "", "serving key PEM") + trustedHosts stringList + allowedNames stringList + ) + flag.Var(&trustedHosts, "trusted-host", "externally visible authority (host[:port]) callers sign for; repeatable, required") + flag.Var(&allowedNames, "allow-ans-name", "ans:// name to accept; repeatable; all proven agents when empty") + flag.Parse() + + if *rootKeysFile == "" { + return errors.New("--root-keys-file is required") + } + keys, err := callee.ReadRootKeys(*rootKeysFile) + if err != nil { + return err + } + logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + h, err := callee.New(ctx, callee.Config{RootKeys: keys, TrustedHosts: trustedHosts, AllowedAnsNames: allowedNames}, logger) + if err != nil { + return err + } + defer h.Close() + + srv := &http.Server{Addr: *listen, Handler: h, ReadHeaderTimeout: readHeaderTimeout} + errc := make(chan error, 1) + go func() { + logger.InfoContext(ctx, "listening", "addr", *listen, "tls", *tlsCert != "") + if *tlsCert != "" { + errc <- srv.ListenAndServeTLS(*tlsCert, *tlsKey) + return + } + errc <- srv.ListenAndServe() + }() + select { + case err := <-errc: + return err + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + return srv.Shutdown(shutdownCtx) + } +} diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..f3f6cd1 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 +# Build: docker build -f deploy/Dockerfile --build-arg CMD=ans-openshell-middleware --build-arg VERSION=0.1.0 -t ans-openshell-middleware . +ARG GO_VERSION=1.26.9 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build +ARG CMD=ans-openshell-middleware +ARG VERSION=dev +ARG TARGETOS +ARG TARGETARCH +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/${CMD} ./cmd/${CMD} + +FROM gcr.io/distroless/static-debian12:nonroot +ARG CMD=ans-openshell-middleware +COPY --from=build /out/${CMD} /usr/local/bin/app +USER nonroot:nonroot +ENTRYPOINT ["/usr/local/bin/app"] +CMD ["--config", "/etc/ans-openshell/config.yaml"] diff --git a/deploy/deployment_test.go b/deploy/deployment_test.go new file mode 100644 index 0000000..a6bf365 --- /dev/null +++ b/deploy/deployment_test.go @@ -0,0 +1,180 @@ +package deploy_test + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "slices" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/internal/interceptor" +) + +type gatewayRegistration struct { + Name string `json:"name"` + BindingPolicy string `json:"binding_policy"` + Bindings []operatorBinding `json:"bindings"` +} + +type operatorBinding struct { + RPC string `json:"rpc"` + Phases []string `json:"phases"` +} + +func TestGatewayRegistrationCoversInterceptorManifest(t *testing.T) { + t.Parallel() + python := deploymentTool(t, "python3") + const parse = `import json, sys, tomllib +with open(sys.argv[1], "rb") as source: + json.dump(tomllib.load(source)["openshell"]["gateway"]["interceptors"], sys.stdout) +` + data, err := exec.CommandContext(t.Context(), python, "-c", parse, "examples/gateway.toml").CombinedOutput() + if err != nil { + t.Fatalf("parse gateway TOML with Python 3.11+: %v\n%s", err, data) + } + var registrations []gatewayRegistration + if err := json.Unmarshal(data, ®istrations); err != nil { + t.Fatal(err) + } + index := slices.IndexFunc(registrations, func(r gatewayRegistration) bool { return r.Name == "ans-destinations" }) + if index < 0 { + t.Fatal("ANS destination interceptor registration is missing") + } + registration := registrations[index] + if registration.BindingPolicy != "allowlist" && registration.BindingPolicy != "exact" { + t.Fatalf("binding policy = %q; want explicit operator authority", registration.BindingPolicy) + } + service := interceptor.NewService(nil, interceptor.Config{}, nil) + manifest, err := service.Describe(t.Context(), &interceptorv1.DescribeRequest{ + Gateway: &extensionv1.PeerMetadata{ProtocolVersion: &extensionv1.ProtocolVersion{Major: 1}}, + }) + if err != nil { + t.Fatal(err) + } + if len(registration.Bindings) != len(manifest.GetBindings()) { + t.Fatalf("registration has %d bindings, manifest has %d", len(registration.Bindings), len(manifest.GetBindings())) + } + for _, binding := range manifest.GetBindings() { + rpc := binding.GetSelector().GetRpc() + var matches []operatorBinding + for _, configured := range registration.Bindings { + if configured.RPC == rpc { + matches = append(matches, configured) + } + } + if len(matches) != 1 { + t.Fatalf("manifest RPC %s has %d configured bindings; want one", rpc, len(matches)) + } + if !slices.Equal(binding.GetPhases(), []interceptorv1.GatewayInterceptorPhase{ + interceptorv1.GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_VALIDATE, + }) || !slices.Equal(matches[0].Phases, []string{"validate"}) { + t.Fatalf("RPC %s must enable the manifest's validate phase", rpc) + } + } +} + +type renderedResource struct { + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + } `yaml:"metadata"` + Data map[string]string `yaml:"data"` + Spec struct { + Template struct { + Metadata struct { + Annotations map[string]string `yaml:"annotations"` + } `yaml:"metadata"` + } `yaml:"template"` + } `yaml:"spec"` +} + +func TestJWKSURLChangesRollBothServices(t *testing.T) { + t.Parallel() + helm := deploymentTool(t, "helm") + previous := make(map[string]string) + for _, jwksURL := range []string{ + "https://gateway-a.example/.well-known/jwks.json", + "https://gateway-b.example/.well-known/jwks.json", + "", + } { + resources := renderChart(t, helm, jwksURL) + for _, service := range []string{"middleware", "interceptor"} { + name := "regression-" + service + configMap, exists := resources["ConfigMap/"+name] + if !exists { + t.Fatalf("missing ConfigMap %s", name) + } + configText := configMap.Data["config.yaml"] + var config struct { + Auth struct { + JWKSURL string `yaml:"jwks_url"` + } `yaml:"auth"` + } + if err := yaml.Unmarshal([]byte(configText), &config); err != nil { + t.Fatalf("%s config: %v", name, err) + } + if config.Auth.JWKSURL != jwksURL { + t.Fatalf("%s JWKS URL = %q, want %q", name, config.Auth.JWKSURL, jwksURL) + } + deployment, exists := resources["Deployment/"+name] + if !exists { + t.Fatalf("missing Deployment %s", name) + } + checksum := deployment.Spec.Template.Metadata.Annotations["checksum/config"] + want := fmt.Sprintf("%x", sha256.Sum256([]byte(configText))) + if checksum != want { + t.Fatalf("%s checksum = %q, want complete mounted config hash %q", name, checksum, want) + } + if previous[name] == checksum { + t.Fatalf("%s pod template did not change after JWKS URL changed", name) + } + previous[name] = checksum + } + } +} + +func renderChart(t *testing.T, helm, jwksURL string) map[string]renderedResource { + t.Helper() + data, err := exec.CommandContext(t.Context(), helm, "template", "regression", "helm/ans-openshell", + "--set-string", "gateway.jwksURL="+jwksURL).CombinedOutput() + if err != nil { + t.Fatalf("render Helm chart: %v\n%s", err, data) + } + resources := make(map[string]renderedResource) + decoder := yaml.NewDecoder(bytes.NewReader(data)) + for { + var resource renderedResource + if err := decoder.Decode(&resource); errors.Is(err, io.EOF) { + return resources + } else if err != nil { + t.Fatalf("decode Helm output: %v", err) + } + key := resource.Kind + "/" + resource.Metadata.Name + if _, exists := resources[key]; exists { + t.Fatalf("duplicate rendered resource %s", key) + } + resources[key] = resource + } +} + +func deploymentTool(t *testing.T, name string) string { + t.Helper() + path, err := exec.LookPath(name) + if err == nil { + return path + } + if os.Getenv("CI") != "" { + t.Fatalf("deployment regression tests require %s in CI: %v", name, err) + } + t.Skipf("deployment regression tests require %s: %v", name, err) + return "" +} diff --git a/deploy/examples/gateway.toml b/deploy/examples/gateway.toml new file mode 100644 index 0000000..f082fda --- /dev/null +++ b/deploy/examples/gateway.toml @@ -0,0 +1,58 @@ +# OpenShell gateway registration snippets for the ANS extension. +# Merge into the gateway's TOML configuration. Replace hosts, paths and names. + +[[openshell.supervisor.middleware]] +name = "ans" # policies bind the middleware by this name +grpc_endpoint = "https://ans-openshell-middleware.ans.svc:50051" +tls_ca_cert_path = "/etc/openshell/ans-ca.pem" # CA that issued the extension's serving certificate +max_payload_bytes = 4194304 # 4 MiB: the body is hashed into the proof, so the whole body is needed +timeout = "500ms" +# audience defaults to "urn:openshell:extension:middleware:ans"; it must equal auth.audience in the extension config. + +# failure_policy = "fail_closed" makes the interceptor a hard dependency of sandbox creation, +# policy edits, draft approvals and custom provider-profile imports even when its +# destination_policy is "observe". Under require_registered a draft approval the interceptor +# cannot resolve from current_state is refused. Use "fail_open" where availability +# matters more than vetting coverage; in require_registered mode keep it closed. +[[openshell.gateway.interceptors]] +name = "ans-destinations" +grpc_endpoint = "https://ans-openshell-interceptor.ans.svc:50052" +tls_ca_cert_path = "/etc/openshell/ans-ca.pem" +order = 10 +failure_policy = "fail_closed" +binding_policy = "allowlist" +timeout = "2s" +max_response_bytes = 1048576 +max_patches = 0 + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/CreateSandbox" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/UpdateConfig" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/SubmitPolicyAnalysis" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/EditDraftChunk" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/ImportProviderProfiles" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/UpdateProviderProfiles" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/ApproveDraftChunk" +phases = ["validate"] + +[[openshell.gateway.interceptors.bindings]] +rpc = "openshell.v1.OpenShell/ApproveAllDraftChunks" +phases = ["validate"] diff --git a/deploy/examples/hydrator.yaml b/deploy/examples/hydrator.yaml new file mode 100644 index 0000000..41a1b4e --- /dev/null +++ b/deploy/examples/hydrator.yaml @@ -0,0 +1,22 @@ +# ans-ocsf-hydrator --config hydrator.yaml --input /var/log/openshell/supervisor-ocsf.jsonl +# The input is the collected supervisor feed (ocsf_json_enabled): raw OCSF-JSON stderr lines or +# the supervisors' openshell-ocsf.*.jsonl files, one bare record per line (not wrapped in the +# collector's own JSON). The gateway's ocsf_log JSONL carries gateway-origin events only. +# Admin key: trust_index.admin_key_file, or the ANS_TRUST_INDEX_ADMIN_KEY environment variable. +trust_index: + base_url: https://trust-index.example.com + admin_key_file: /var/run/secrets/ans-openshell/trust-index-admin-key +provenance: + aim_id: ans://v1.0.0.platform.example.com # the operator's identity, recorded as provenance.aimId + evidence_url: https://ops.example.com/openshell/audit +signals: + policy_violation_rate: openshell.policyViolationRate # set to "" to disable + protocol_adherence: openshell.protocolAdherence +signing: # optional; signs each batch (X-Ans-Batch-Signature) + key_file: /var/run/secrets/ans-openshell/platform/key.pem + cert_file: /var/run/secrets/ans-openshell/platform/cert.pem +agents: + - agent_id: 7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07 + ans_name: ans://v1.0.0.payments.example.com + sandbox_names: [payments] # container.name in the OCSF record + sandbox_uids: [] # container.uid, the durable association diff --git a/deploy/examples/interceptor.yaml b/deploy/examples/interceptor.yaml new file mode 100644 index 0000000..f94aeac --- /dev/null +++ b/deploy/examples/interceptor.yaml @@ -0,0 +1,16 @@ +# ans-openshell-interceptor --config interceptor.yaml +listen: ":50052" +health_listen: ":8081" # plaintext /healthz and /readyz for probes +tls: + cert_file: /etc/ans-openshell/tls/tls.crt + key_file: /etc/ans-openshell/tls/tls.key +auth: + gateway_issuer: openshell-gateway:prod-1 + audience: urn:openshell:extension:interceptor:ans-destinations + jwks_file: /etc/ans-openshell/gateway-jwks.json +destination_policy: observe # observe (default) | require_registered | off +lookup_wait: 300ms # require_registered only; keep below the gateway registration timeout +vetting: + trusted_tl_domains: [tl.example.com] + cache_ttl: 10m + exempt_hosts: ["api.openai.com", "*.amazonaws.com"] diff --git a/deploy/examples/middleware.yaml b/deploy/examples/middleware.yaml new file mode 100644 index 0000000..e87623e --- /dev/null +++ b/deploy/examples/middleware.yaml @@ -0,0 +1,27 @@ +# ans-openshell-middleware --config middleware.yaml +listen: ":50051" +health_listen: ":8081" # plaintext /healthz and /readyz for probes +tls: + cert_file: /etc/ans-openshell/tls/tls.crt + key_file: /etc/ans-openshell/tls/tls.key +auth: + gateway_issuer: openshell-gateway:prod-1 + audience: urn:openshell:extension:middleware:ans + jwks_file: /etc/ans-openshell/gateway-jwks.json + jwks_url: https://openshell-gateway.openshell.svc/.well-known/jwks.json +transparency_log: + base_url: https://tl.example.com + root_keys_file: /etc/ans-openshell/tl-root-keys.txt +identities: + payments-agent: + ans_name: ans://v1.0.0.payments.example.com + agent_id: 7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07 + key_file: /var/run/secrets/ans-openshell/payments-agent/key.pem + cert_file: /var/run/secrets/ans-openshell/payments-agent/cert.pem +# Approve gateway-issued sandbox IDs here; an empty map denies all signing. +sandbox_identities: {} +vetting: + trusted_tl_domains: [tl.example.com] + cache_ttl: 10m + request_budget: 150ms + exempt_hosts: ["api.openai.com", "*.amazonaws.com"] diff --git a/deploy/examples/policy.yaml b/deploy/examples/policy.yaml new file mode 100644 index 0000000..0918ddd --- /dev/null +++ b/deploy/examples/policy.yaml @@ -0,0 +1,31 @@ +# Sandbox policy binding the ANS middleware. The sandboxed agent may call +# tools.example.com; every allowed request leaves signed as payments-agent. +version: 1 + +network_middlewares: + ans-identity: + name: ANS identity + middleware: ans # the gateway registration name + order: 100 # last in the chain: nothing may rewrite a signed request + on_error: fail_closed + endpoints: + include: + - tools.example.com + config: + mode: sign + identity: payments-agent # an identity from the extension's service configuration + destination_policy: require_registered + +network_policies: + tools: + name: tools + endpoints: + - host: tools.example.com + port: 443 + protocol: rest + rules: + - allow: + method: POST + path: /v1/tasks + binaries: + - path: /usr/bin/curl diff --git a/deploy/helm/ans-openshell/Chart.yaml b/deploy/helm/ans-openshell/Chart.yaml new file mode 100644 index 0000000..bfca23e --- /dev/null +++ b/deploy/helm/ans-openshell/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: ans-openshell +description: ANS identity, destination vetting and Trust Index feed for NVIDIA OpenShell +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/deploy/helm/ans-openshell/templates/_helpers.tpl b/deploy/helm/ans-openshell/templates/_helpers.tpl new file mode 100644 index 0000000..7b54554 --- /dev/null +++ b/deploy/helm/ans-openshell/templates/_helpers.tpl @@ -0,0 +1,122 @@ +{{- define "ans-openshell.image" -}} +{{ .spec.image.repository }}:{{ .spec.image.tag | default .root.Chart.AppVersion }} +{{- end -}} + +{{- define "ans-openshell.service-config" -}} +{{- $config := deepCopy .spec.config -}} +{{- if .root.Values.gateway.jwksURL -}} +{{- $auth := merge (dict "jwks_url" .root.Values.gateway.jwksURL) (default (dict) $config.auth) -}} +{{- $config = merge (dict "auth" $auth) $config -}} +{{- end -}} +listen: ":{{ .spec.port }}" +health_listen: ":{{ .spec.healthPort }}" +tls: + cert_file: /etc/ans-openshell/tls/tls.crt + key_file: /etc/ans-openshell/tls/tls.key +{{ toYaml $config }} +{{- end -}} + +{{/* One gRPC service: ConfigMap, Service, Deployment and PodDisruptionBudget. Called with a dict of root, name, spec. */}} +{{- define "ans-openshell.service" -}} +{{- $root := .root -}} +{{- $name := printf "%s-%s" $root.Release.Name .name -}} +{{- $serviceConfig := printf "%s\n" (include "ans-openshell.service-config" .) -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} +data: + config.yaml: | +{{ $serviceConfig | indent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} +spec: + selector: + app: {{ $name }} + ports: + - name: grpc + port: {{ .spec.port }} + targetPort: grpc +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} +spec: + replicas: {{ .spec.replicas }} + selector: + matchLabels: + app: {{ $name }} + template: + metadata: + labels: + app: {{ $name }} + annotations: + checksum/config: {{ $serviceConfig | sha256sum }} + spec: + terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds }} + securityContext: + runAsNonRoot: true + seccompProfile: {type: RuntimeDefault} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ $name }} + containers: + - name: {{ .name }} + image: {{ include "ans-openshell.image" (dict "root" $root "spec" .spec) }} + imagePullPolicy: {{ $root.Values.imagePullPolicy }} + args: ["--config", "/etc/ans-openshell/config.yaml"] + ports: + - name: grpc + containerPort: {{ .spec.port }} + - name: health + containerPort: {{ .spec.healthPort }} + readinessProbe: + httpGet: {path: /readyz, port: health} + periodSeconds: 10 + livenessProbe: + httpGet: {path: /healthz, port: health} + periodSeconds: 20 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: {{ toYaml $root.Values.resources | nindent 12 }} + volumeMounts: + - {name: config, mountPath: /etc/ans-openshell/config.yaml, subPath: config.yaml, readOnly: true} + - {name: tls, mountPath: /etc/ans-openshell/tls, readOnly: true} + - {name: trust, mountPath: /etc/ans-openshell/trust, readOnly: true} +{{- range $id, $secret := .spec.identitySecrets }} + - {name: identity-{{ $id }}, mountPath: /var/run/secrets/ans-openshell/{{ $id }}, readOnly: true} +{{- end }} + volumes: + - name: config + configMap: {name: {{ $name }}} + - name: tls + secret: {secretName: {{ $root.Values.tls.secretName }}} + - name: trust + configMap: {name: {{ $root.Values.trust.configMapName }}} +{{- range $id, $secret := .spec.identitySecrets }} + - name: identity-{{ $id }} + secret: {secretName: {{ $secret }}} +{{- end }} +{{- if and $root.Values.podDisruptionBudget.enabled (gt (int .spec.replicas) 1) }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $name }} +spec: + minAvailable: {{ $root.Values.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + app: {{ $name }} +{{- end }} +{{- end -}} diff --git a/deploy/helm/ans-openshell/templates/hydrator.yaml b/deploy/helm/ans-openshell/templates/hydrator.yaml new file mode 100644 index 0000000..25eb4dd --- /dev/null +++ b/deploy/helm/ans-openshell/templates/hydrator.yaml @@ -0,0 +1,68 @@ +{{- if .Values.hydrator.enabled }} +{{- $name := printf "%s-hydrator" .Release.Name }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} +data: + config.yaml: | +{{ toYaml .Values.hydrator.config | indent 4 }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ $name }} +spec: + schedule: {{ .Values.hydrator.schedule | quote }} + concurrencyPolicy: Forbid + jobTemplate: + spec: + backoffLimit: 1 + template: + metadata: + labels: + app: {{ $name }} + annotations: + checksum/config: {{ toYaml .Values.hydrator.config | sha256sum }} + spec: + restartPolicy: Never +{{- with .Values.hydrator.nodeSelector }} + nodeSelector: {{ toYaml . | nindent 12 }} +{{- end }} + securityContext: + runAsNonRoot: true + seccompProfile: {type: RuntimeDefault} + containers: + - name: hydrator + image: {{ include "ans-openshell.image" (dict "root" . "spec" .Values.hydrator) }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + args: + - --config + - /etc/ans-openshell/config.yaml + - --input + - {{ .Values.hydrator.input | quote }} +{{- with .Values.hydrator.window }} + - --window-last + - {{ . | quote }} +{{- end }} +{{- if .Values.hydrator.adminKeySecret }} + envFrom: + - secretRef: {name: {{ .Values.hydrator.adminKeySecret }}} +{{- end }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: [ALL]} + resources: {{ toYaml .Values.resources | nindent 16 }} + volumeMounts: + - {name: config, mountPath: /etc/ans-openshell/config.yaml, subPath: config.yaml, readOnly: true} +{{- with .Values.hydrator.extraVolumeMounts }} +{{ toYaml . | indent 16 }} +{{- end }} + volumes: + - name: config + configMap: {name: {{ $name }}} +{{- with .Values.hydrator.extraVolumes }} +{{ toYaml . | indent 12 }} +{{- end }} +{{- end }} diff --git a/deploy/helm/ans-openshell/templates/interceptor.yaml b/deploy/helm/ans-openshell/templates/interceptor.yaml new file mode 100644 index 0000000..e126b2f --- /dev/null +++ b/deploy/helm/ans-openshell/templates/interceptor.yaml @@ -0,0 +1,3 @@ +{{- if .Values.interceptor.enabled }} +{{- include "ans-openshell.service" (dict "root" . "name" "interceptor" "spec" .Values.interceptor) }} +{{- end }} diff --git a/deploy/helm/ans-openshell/templates/middleware.yaml b/deploy/helm/ans-openshell/templates/middleware.yaml new file mode 100644 index 0000000..4f7bd06 --- /dev/null +++ b/deploy/helm/ans-openshell/templates/middleware.yaml @@ -0,0 +1,3 @@ +{{- if .Values.middleware.enabled }} +{{- include "ans-openshell.service" (dict "root" . "name" "middleware" "spec" .Values.middleware) }} +{{- end }} diff --git a/deploy/helm/ans-openshell/values.yaml b/deploy/helm/ans-openshell/values.yaml new file mode 100644 index 0000000..a8b057a --- /dev/null +++ b/deploy/helm/ans-openshell/values.yaml @@ -0,0 +1,111 @@ +imagePullPolicy: IfNotPresent + +# Serving certificate for both gRPC services; OpenShell pins its CA with tls_ca_cert_path. +tls: + secretName: ans-openshell-tls # keys tls.crt and tls.key + +# The gateway's JWKS (cold-start trust anchor) and the transparency log root keys. Rotating a key +# in this ConfigMap requires a rollout restart; gateway.jwksURL lets the services pick up a rotated +# gateway signing key on their own instead. +trust: + configMapName: ans-openshell-trust # keys gateway-jwks.json and tl-root-keys.txt + +gateway: + # Rendered into both services' auth.jwks_url. Must be https; empty disables steady-state refresh. + jwksURL: https://openshell-gateway.openshell.svc/.well-known/jwks.json + +terminationGracePeriodSeconds: 30 + +# Each service is its own image: deploy/Dockerfile builds one binary per CMD build argument. +middleware: + enabled: true + image: + repository: ghcr.io/agentnameservice/ans-openshell-middleware + tag: "" # defaults to the chart appVersion + replicas: 2 + port: 50051 + healthPort: 8081 # plaintext /healthz and /readyz for kubelet probes + # One secret per identity, mounted at /var/run/secrets/ans-openshell//{key.pem,cert.pem} + identitySecrets: + payments-agent: payments-agent-identity + config: + auth: + gateway_issuer: openshell-gateway:prod-1 + audience: urn:openshell:extension:middleware:ans + jwks_file: /etc/ans-openshell/trust/gateway-jwks.json + transparency_log: + base_url: https://tl.example.com + root_keys_file: /etc/ans-openshell/trust/tl-root-keys.txt + identities: + payments-agent: + ans_name: ans://v1.0.0.payments.example.com + agent_id: 7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07 + key_file: /var/run/secrets/ans-openshell/payments-agent/key.pem + cert_file: /var/run/secrets/ans-openshell/payments-agent/cert.pem + # Approve gateway-issued sandbox IDs before enabling signed requests. + sandbox_identities: {} + vetting: + trusted_tl_domains: [tl.example.com] + cache_ttl: 10m + request_budget: 150ms + exempt_hosts: [] + +interceptor: + enabled: true + image: + repository: ghcr.io/agentnameservice/ans-openshell-interceptor + tag: "" + replicas: 2 + port: 50052 + healthPort: 8081 + config: + auth: + gateway_issuer: openshell-gateway:prod-1 + audience: urn:openshell:extension:interceptor:ans-destinations + jwks_file: /etc/ans-openshell/trust/gateway-jwks.json + destination_policy: observe # observe | require_registered | off + lookup_wait: 300ms # keep below the registration timeout in gateway.toml + vetting: + trusted_tl_domains: [tl.example.com] + cache_ttl: 10m + exempt_hosts: [] + +# The hydrator runs as a CronJob over the sandbox supervisors' OCSF records. Enable the export +# with `openshell settings set --global --key ocsf_json_enabled --value true`; on Kubernetes have +# the cluster's log collector keep the supervisor containers' OCSF-JSON stderr lines, written as +# bare lines (not wrapped in the collector's own JSON), in a volume this job can read from any node +# it may run on (or pin it with nodeSelector), mount it with extraVolumes and extraVolumeMounts and +# point input at it. The hydrator signs and posts what it reads, so only the collector may write +# that volume. The gateway's ocsf_log JSONL carries gateway-origin events only, not sandbox +# traffic. Each run aggregates the trailing window, so keep window aligned with schedule. +hydrator: + enabled: false + image: + repository: ghcr.io/agentnameservice/ans-ocsf-hydrator + tag: "" + schedule: "*/15 * * * *" + window: 15m # --window-last; empty aggregates the whole input every run + input: /var/log/openshell/supervisor-ocsf.jsonl + nodeSelector: {} + adminKeySecret: "" # secret with key ANS_TRUST_INDEX_ADMIN_KEY; empty posts without Authorization + extraVolumes: [] + extraVolumeMounts: [] + config: + trust_index: + base_url: https://trust-index.example.com + provenance: + aim_id: ans://v1.0.0.platform.example.com + agents: + - agent_id: 7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07 + ans_name: ans://v1.0.0.payments.example.com + sandbox_names: [payments] + +podDisruptionBudget: + enabled: true + minAvailable: 1 + +# A middleware call holds up to 4 MiB of request body plus an 8 MiB gRPC message buffer, so +# size memory for the expected number of concurrent in-flight requests. +resources: + requests: {cpu: 100m, memory: 128Mi} + limits: {cpu: "1", memory: 512Mi} diff --git a/examples/demo/main.go b/examples/demo/main.go new file mode 100644 index 0000000..1556dc9 --- /dev/null +++ b/examples/demo/main.go @@ -0,0 +1,32 @@ +// Command demo runs the ANS extension end to end without OpenShell: it starts +// a fixture transparency log, the middleware service and the reference callee, +// then plays the sandbox supervisor. It sends EvaluateHttpRequest calls, +// applies the returned header mutations to real HTTP requests and shows the +// callee accepting the signed call and rejecting a replay, a tampered body, +// an unsigned request and a stripped one. It exits non-zero when any +// expectation fails, so it doubles as a smoke test. +package main + +import ( + "context" + "fmt" + "os" +) + +func main() { + if err := runDemo(); err != nil { + fmt.Fprintln(os.Stderr, "demo:", err) + os.Exit(1) + } +} + +func runDemo() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + d, err := newDemo(ctx, os.Stdout) + if err != nil { + return fmt.Errorf("setup failed: %w", err) + } + defer d.close() + return d.run(ctx) +} diff --git a/examples/demo/scenarios.go b/examples/demo/scenarios.go new file mode 100644 index 0000000..bccb19b --- /dev/null +++ b/examples/demo/scenarios.go @@ -0,0 +1,147 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/middleware" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// identityHeaderCount is DPoP, X-SCITT-Receipt and X-ANS-Status-Token. +const identityHeaderCount = 3 + +func (d *demo) run(ctx context.Context) error { + token := d.supervisorToken(sandboxID) + signPolicy := map[string]string{"mode": "sign", "identity": identityKey} + call := outbound{method: http.MethodPost, path: "/v1/tasks", body: `{"task":"settle invoice 42"}`} + + d.section("1. Signed request") + res, err := d.evaluate(ctx, token, signPolicy, call) + if err != nil { + return err + } + allow := res.GetDecision() == middlewarev1.Decision_DECISION_ALLOW + d.check("middleware allows and returns header mutations", allow && len(res.GetHeaderMutations()) == identityHeaderCount, + fmt.Sprintf("decision=%s mutations=%d findings=%s", res.GetDecision(), len(res.GetHeaderMutations()), findings(res))) + for _, m := range res.GetHeaderMutations() { + if w := m.GetWrite(); w != nil { + d.sayf("%-19s %s (%d bytes)", w.GetName(), w.GetOnExisting(), len(w.GetValue())) + } + } + code, body, err := d.forward(ctx, call, res.GetHeaderMutations(), call.body) + if err != nil { + return err + } + d.check("callee authenticates the sandboxed agent", code == http.StatusOK && strings.Contains(body, ansName), + fmt.Sprintf("status=%d body=%s", code, body)) + + d.section("2. Replay, tampering, no identity") + code, _, err = d.forward(ctx, call, res.GetHeaderMutations(), call.body) + if err != nil { + return err + } + d.check("replaying the same proof is rejected", code == http.StatusUnauthorized, fmt.Sprintf("status=%d", code)) + fresh, err := d.evaluate(ctx, token, signPolicy, call) + if err != nil { + return err + } + code, _, err = d.forward(ctx, call, fresh.GetHeaderMutations(), `{"task":"settle invoice 43"}`) + if err != nil { + return err + } + d.check("a body changed after signing is rejected", code == http.StatusUnauthorized, fmt.Sprintf("status=%d", code)) + code, _, err = d.forward(ctx, call, nil, call.body) + if err != nil { + return err + } + d.check("an unsigned request is rejected", code == http.StatusUnauthorized, fmt.Sprintf("status=%d", code)) + + if err := d.policyScenarios(ctx, token, call); err != nil { + return err + } + d.authScenarios(ctx, signPolicy, call) + return d.summary() +} + +func (d *demo) policyScenarios(ctx context.Context, token string, call outbound) error { + d.section("3. Policy modes") + res, err := d.evaluate(ctx, token, map[string]string{"mode": "strip"}, call) + if err != nil { + return err + } + stripped := res.GetDecision() == middlewarev1.Decision_DECISION_ALLOW && removes(res) == identityHeaderCount + d.check("strip mode removes identity headers", stripped, fmt.Sprintf("decision=%s removes=%d", res.GetDecision(), removes(res))) + + res, err = d.evaluate(ctx, token, map[string]string{"identity": staleKey}, call) + if err != nil { + return err + } + d.check("another configured identity is not authorized for this sandbox", + res.GetDecision() == middlewarev1.Decision_DECISION_DENY && res.GetReasonCode() == middleware.ReasonIdentityUnauthorized, + fmt.Sprintf("decision=%s reason_code=%s", res.GetDecision(), res.GetReasonCode())) + + res, err = d.evaluate(ctx, token, map[string]string{"identity": identityKey, "destination_policy": "require_registered"}, call) + if err != nil { + return err + } + d.check("require_registered denies an unregistered destination", + res.GetDecision() == middlewarev1.Decision_DECISION_DENY && res.GetReasonCode() == vetting.ReasonUnregistered, + fmt.Sprintf("decision=%s reason_code=%s", res.GetDecision(), res.GetReasonCode())) + + res, err = d.evaluate(ctx, token, map[string]string{"identity": identityKey, "destination_policy": "observe"}, call) + if err != nil { + return err + } + d.check("observe allows and records the destination finding", + res.GetDecision() == middlewarev1.Decision_DECISION_ALLOW && strings.Contains(findings(res), "ans_destination_unregistered"), + fmt.Sprintf("decision=%s findings=%s", res.GetDecision(), findings(res))) + return nil +} + +func (d *demo) authScenarios(ctx context.Context, policy map[string]string, call outbound) { + d.section("4. Caller authentication") + res, err := d.evaluate(ctx, d.supervisorToken("sb-other"), policy, call) + d.check("a supervisor credential for another sandbox is denied", + err == nil && res.GetDecision() == middlewarev1.Decision_DECISION_DENY && res.GetReasonCode() == middleware.ReasonCallerSandboxMismatch, + fmt.Sprintf("err=%v reason_code=%s", err, res.GetReasonCode())) + _, err = d.evaluate(ctx, "", policy, call) + d.check("a call without a gateway credential is unauthenticated", + status.Code(err) == codes.Unauthenticated, fmt.Sprintf("grpc code=%s", status.Code(err))) +} + +func (d *demo) summary() error { + d.section("Summary") + if d.failures > 0 { + return fmt.Errorf("%d expectation(s) failed", d.failures) + } + d.sayf("all expectations held") + return nil +} + +func findings(res *middlewarev1.HttpRequestResult) string { + names := make([]string, 0, len(res.GetFindings())) + for _, f := range res.GetFindings() { + names = append(names, f.GetType()) + } + if len(names) == 0 { + return "-" + } + return strings.Join(names, ",") +} + +func removes(res *middlewarev1.HttpRequestResult) int { + n := 0 + for _, m := range res.GetHeaderMutations() { + if m.GetRemove() != nil { + n++ + } + } + return n +} diff --git a/examples/demo/setup.go b/examples/demo/setup.go new file mode 100644 index 0000000..57117a6 --- /dev/null +++ b/examples/demo/setup.go @@ -0,0 +1,264 @@ +package main + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/callee" + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/grpcserver" + "github.com/agentnameservice/ans-openshell/internal/identity" + "github.com/agentnameservice/ans-openshell/internal/middleware" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +const ( + agentID = "7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07" + ansName = "ans://v1.0.0.payments.example.com" + identityKey = "payments-agent" + staleKey = "stale-agent" + staleID = "0d2c9d1e-7f3a-4b7e-9c11-2a6e8f4b1c55" + staleName = "ans://v1.0.0.stale.example.com" + + gatewayIssuer = "openshell-gateway:demo" + audience = "urn:openshell:extension:middleware:ans" + gatewayKid = "demo-gateway-key" + sandboxID = "sb-7f2e" + + tokenTTL = 5 * time.Minute + headerTimeout = 5 * time.Second + filePerm = 0o600 +) + +// demo owns every process the supervisor would talk to. +type demo struct { + out io.Writer + logger *slog.Logger + + tl *testfixtures.TL + ident *testfixtures.Identity + tlSrv *httptest.Server + gwKey ed25519.PrivateKey + callee *calleeServer + mw *grpcserver.Server + client middlewarev1.SupervisorMiddlewareClient + conn *grpc.ClientConn + + fileRegistry *identity.Registry + registry *identity.Registry + vet *vetting.Service + tmpDir string + failures int +} + +type calleeServer struct { + authority string + handler *callee.Handler + srv *http.Server +} + +func newDemo(ctx context.Context, out io.Writer) (*demo, error) { + d := &demo{out: out, logger: slog.New(slog.NewTextHandler(io.Discard, nil))} + d.tl = testfixtures.MustNewTL() + d.ident = testfixtures.MustNewIdentity(agentID, ansName) + d.tlSrv = testfixtures.FakeTLServer(d.tl, d.ident) + d.section("Fixtures") + d.sayf("transparency log at %s, root key %s...", d.tlSrv.URL, d.tl.RootKeyC2SP()[:24]) + d.sayf("agent %s registered as %s", agentID, ansName) + + dir, err := os.MkdirTemp("", "ans-openshell-demo-") + if err != nil { + return nil, err + } + d.tmpDir = dir + jwksFile, err := d.writeGatewayJWKS(dir) + if err != nil { + return nil, err + } + if err := d.startCallee(ctx); err != nil { + return nil, err + } + if err := d.startMiddleware(ctx, dir, jwksFile); err != nil { + return nil, err + } + return d, nil +} + +// writeGatewayJWKS mints the gateway's Ed25519 signing key and writes the JWKS +// the middleware trusts, standing in for the operator-provisioned +// /.well-known/jwks.json. +func (d *demo) writeGatewayJWKS(dir string) (string, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", err + } + d.gwKey = priv + jwks := map[string]any{"keys": []map[string]string{{ + "kty": "OKP", "crv": "Ed25519", "kid": gatewayKid, "alg": "EdDSA", + "x": base64.RawURLEncoding.EncodeToString(pub), + }}} + b, err := json.Marshal(jwks) + if err != nil { + return "", err + } + path := filepath.Join(dir, "gateway-jwks.json") + return path, os.WriteFile(path, b, filePerm) +} + +func (d *demo) startCallee(ctx context.Context) error { + lis, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:0") + if err != nil { + return err + } + authority := lis.Addr().String() + h, err := callee.New(ctx, callee.Config{ + RootKeys: []string{d.tl.RootKeyC2SP()}, + TrustedHosts: []string{authority}, + }, d.logger) + if err != nil { + return err + } + srv := &http.Server{Handler: h, ReadHeaderTimeout: headerTimeout} + go func() { _ = srv.Serve(lis) }() + d.callee = &calleeServer{authority: authority, handler: h, srv: srv} + d.section("Reference callee") + d.sayf("listening on http://%s, trusting the fixture log, verifying ANS-6 Method B with pop.Middleware", authority) + return nil +} + +func (d *demo) startMiddleware(ctx context.Context, dir, jwksFile string) error { + keyFile, certFile := filepath.Join(dir, "key.pem"), filepath.Join(dir, "cert.pem") + if err := os.WriteFile(keyFile, d.ident.KeyPEM, filePerm); err != nil { + return err + } + if err := os.WriteFile(certFile, d.ident.CertPEM, filePerm); err != nil { + return err + } + fileRegistry, err := identity.NewRegistry(ctx, map[string]identity.Config{ + identityKey: {AnsName: ansName, AgentID: agentID, KeyFile: keyFile, CertFile: certFile}, + }, config.TransparencyLog{ + BaseURL: d.tlSrv.URL, + RootKeys: []string{d.tl.RootKeyC2SP()}, + AllowInsecureTransport: true, + }, d.logger) + if err != nil { + return fmt.Errorf("identity registry: %w", err) + } + d.fileRegistry = fileRegistry + live, _ := fileRegistry.Identity(identityKey) + stale, err := d.staleIdentity() + if err != nil { + return err + } + d.registry, err = identity.NewRegistryFromIdentities(live, stale) + if err != nil { + return err + } + + d.vet, err = vetting.New(config.Vetting{TrustedTLDomains: []string{"tl.example.com"}}, + vetting.Deps{DNS: verify.NewMockDNSResolver()}, d.logger) + if err != nil { + return err + } + auth, err := extauth.NewVerifier(config.Auth{GatewayIssuer: gatewayIssuer, Audience: audience, JWKSFile: jwksFile}, d.logger) + if err != nil { + return err + } + d.mw, err = grpcserver.New(ctx, grpcserver.Options{ + Listen: "127.0.0.1:0", + Unary: []grpc.UnaryServerInterceptor{auth.UnaryInterceptor()}, + Stream: []grpc.StreamServerInterceptor{auth.StreamInterceptor()}, + }, d.logger) + if err != nil { + return err + } + d.mw.SetHealthy(true) + authz, err := middleware.NewSandboxIdentities(map[string]string{sandboxID: identityKey}, d.registry) + if err != nil { + return err + } + svc := middleware.NewService(d.registry, authz, d.vet, middleware.Config{Audience: audience, ImplementationVersion: "demo"}, d.logger) + middlewarev1.RegisterSupervisorMiddlewareServer(d.mw.Registrar(), svc) + go func() { _ = d.mw.Serve(ctx) }() + + d.conn, err = grpc.NewClient(d.mw.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return err + } + d.client = middlewarev1.NewSupervisorMiddlewareClient(d.conn) + d.section("Middleware") + d.sayf("gRPC on %s; identity %q loaded from PEM files, artifacts fetched from the log; gateway JWTs verified against %s", + d.mw.Addr(), identityKey, filepath.Base(jwksFile)) + return nil +} + +// staleIdentity has a receipt but no unexpired status token, the state the +// SDK's HeaderSupplier reports once a token has lapsed and refresh has failed. +func (d *demo) staleIdentity() (*identity.Identity, error) { + fixture := testfixtures.MustNewIdentity(staleID, staleName) + signer, _, err := identity.NewPEMSigner(fixture.KeyPEM, fixture.CertPEM) + if err != nil { + return nil, err + } + receipt, _ := testfixtures.StaticArtifacts(d.tl, fixture) + return identity.NewIdentity(staleKey, staleName, staleID, signer, + identity.NewStaticSource(identity.Artifacts{ReceiptBase64: receipt})) +} + +func (d *demo) close() { + if d.conn != nil { + _ = d.conn.Close() + } + if d.callee != nil { + _ = d.callee.srv.Close() + d.callee.handler.Close() + } + if d.vet != nil { + d.vet.Close() + } + if d.registry != nil { + d.registry.Close() + } + if d.fileRegistry != nil { + d.fileRegistry.Close() + } + if d.tlSrv != nil { + d.tlSrv.Close() + } + if d.tmpDir != "" { + _ = os.RemoveAll(d.tmpDir) + } +} + +func (d *demo) section(title string) { fmt.Fprintf(d.out, "\n== %s ==\n", title) } + +func (d *demo) sayf(format string, args ...any) { fmt.Fprintf(d.out, " "+format+"\n", args...) } + +// check records one expectation and prints PASS or FAIL. +func (d *demo) check(name string, ok bool, detail string) { + mark := "PASS" + if !ok { + mark = "FAIL" + d.failures++ + } + fmt.Fprintf(d.out, "%s %s (%s)\n", mark, name, detail) +} diff --git a/examples/demo/supervisor.go b/examples/demo/supervisor.go new file mode 100644 index 0000000..a561170 --- /dev/null +++ b/examples/demo/supervisor.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "time" + + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" +) + +// supervisorToken mints the extension JWT a sandbox supervisor presents: +// EdDSA, typ openshell-ext+jwt, caller_kind supervisor, bound to one sandbox. +func (d *demo) supervisorToken(sandbox string) string { + now := time.Now() + header, _ := json.Marshal(map[string]string{"alg": "EdDSA", "kid": gatewayKid, "typ": "openshell-ext+jwt"}) + claims, _ := json.Marshal(map[string]any{ + "iss": gatewayIssuer, "aud": audience, "sub": "spiffe://openshell/sandbox/" + sandbox, + "iat": now.Unix(), "exp": now.Add(tokenTTL).Unix(), "jti": strconv.FormatInt(now.UnixNano(), 10), + "caller_kind": "supervisor", "sandbox_id": sandbox, + }) + signing := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(claims) + sig := ed25519.Sign(d.gwKey, []byte(signing)) + return signing + "." + base64.RawURLEncoding.EncodeToString(sig) +} + +// outbound is one request the sandboxed agent is making. +type outbound struct { + method string + path string + body string +} + +// evaluate plays the supervisor's call into the middleware for one request. +func (d *demo) evaluate(ctx context.Context, token string, policy map[string]string, req outbound) (*middlewarev1.HttpRequestResult, error) { + cfg, err := structpb.NewStruct(stringMap(policy)) + if err != nil { + return nil, err + } + host, port := splitAuthority(d.callee.authority) + if token != "" { + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token) + } + return d.client.EvaluateHttpRequest(ctx, &middlewarev1.HttpRequestEvaluation{ + Phase: middlewarev1.SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS, + Context: &middlewarev1.RequestContext{ + RequestId: fmt.Sprintf("req-%d", time.Now().UnixNano()), SandboxId: sandboxID, + Sandbox: "payments", Workspace: "default", + }, + Config: cfg, + Target: &middlewarev1.HttpRequestTarget{Scheme: "http", Host: host, Port: port, Method: req.method, Path: req.path}, + Headers: []*middlewarev1.HttpHeader{{Name: "content-type", Value: "application/json"}}, + Body: []byte(req.body), + MiddlewareName: "ans", + }) +} + +// forward applies the middleware's header mutations the way the supervisor +// would and sends the request to the callee. +func (d *demo) forward(ctx context.Context, req outbound, mutations []*middlewarev1.HeaderMutation, body string) (int, string, error) { + httpReq, err := http.NewRequestWithContext(ctx, req.method, "http://"+d.callee.authority+req.path, strings.NewReader(body)) + if err != nil { + return 0, "", err + } + httpReq.Header.Set("Content-Type", "application/json") + applyMutations(httpReq.Header, mutations) + resp, err := http.DefaultClient.Do(httpReq) //nolint:gosec // G704: the authority is the demo's own loopback callee + if err != nil { + return 0, "", err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return resp.StatusCode, strings.TrimSpace(string(b)), err +} + +func applyMutations(h http.Header, mutations []*middlewarev1.HeaderMutation) { + for _, m := range mutations { + switch op := m.GetOperation().(type) { + case *middlewarev1.HeaderMutation_Write: + w := op.Write + switch w.GetOnExisting() { + case middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_APPEND: + h.Add(w.GetName(), w.GetValue()) + case middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_SKIP: + if h.Get(w.GetName()) == "" { + h.Set(w.GetName(), w.GetValue()) + } + case middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_OVERWRITE, + middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_UNSPECIFIED: + h.Set(w.GetName(), w.GetValue()) + } + case *middlewarev1.HeaderMutation_Remove: + h.Del(op.Remove.GetName()) + } + } +} + +func stringMap(m map[string]string) map[string]any { + out := make(map[string]any, len(m)) + for k, v := range m { + out[k] = v + } + return out +} + +func splitAuthority(authority string) (string, uint32) { + host, portStr, err := net.SplitHostPort(authority) + if err != nil { + return authority, 0 + } + port, err := strconv.ParseUint(portStr, 10, 32) + if err != nil { + return host, 0 + } + return host, uint32(port) +} diff --git a/gen/openshell/datamodelv1/datamodel.pb.go b/gen/openshell/datamodelv1/datamodel.pb.go new file mode 100644 index 0000000..81d3a21 --- /dev/null +++ b/gen/openshell/datamodelv1/datamodel.pb.go @@ -0,0 +1,747 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: datamodel.proto + +package datamodelv1 + +import ( + _ "github.com/agentnameservice/ans-openshell/gen/openshell/optionsv1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Phase of a workspace's lifecycle. +type WorkspacePhase int32 + +const ( + WorkspacePhase_WORKSPACE_PHASE_UNSPECIFIED WorkspacePhase = 0 + WorkspacePhase_WORKSPACE_PHASE_ACTIVE WorkspacePhase = 1 + WorkspacePhase_WORKSPACE_PHASE_TERMINATING WorkspacePhase = 2 +) + +// Enum value maps for WorkspacePhase. +var ( + WorkspacePhase_name = map[int32]string{ + 0: "WORKSPACE_PHASE_UNSPECIFIED", + 1: "WORKSPACE_PHASE_ACTIVE", + 2: "WORKSPACE_PHASE_TERMINATING", + } + WorkspacePhase_value = map[string]int32{ + "WORKSPACE_PHASE_UNSPECIFIED": 0, + "WORKSPACE_PHASE_ACTIVE": 1, + "WORKSPACE_PHASE_TERMINATING": 2, + } +) + +func (x WorkspacePhase) Enum() *WorkspacePhase { + p := new(WorkspacePhase) + *p = x + return p +} + +func (x WorkspacePhase) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkspacePhase) Descriptor() protoreflect.EnumDescriptor { + return file_datamodel_proto_enumTypes[0].Descriptor() +} + +func (WorkspacePhase) Type() protoreflect.EnumType { + return &file_datamodel_proto_enumTypes[0] +} + +func (x WorkspacePhase) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkspacePhase.Descriptor instead. +func (WorkspacePhase) EnumDescriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{0} +} + +// Selects the workspace scope for a public API request. +// +// Requests that operate on one workspace require a non-empty `workspace`. +// Cross-workspace list requests additionally accept `all_workspaces`. The +// containing request documents which selections it supports; an omitted +// selector is invalid for workspace-scoped operations. +type WorkspaceSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selection: + // + // *WorkspaceSelector_Workspace + // *WorkspaceSelector_AllWorkspaces + Selection isWorkspaceSelector_Selection `protobuf_oneof:"selection"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceSelector) Reset() { + *x = WorkspaceSelector{} + mi := &file_datamodel_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceSelector) ProtoMessage() {} + +func (x *WorkspaceSelector) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[0] + 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 WorkspaceSelector.ProtoReflect.Descriptor instead. +func (*WorkspaceSelector) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{0} +} + +func (x *WorkspaceSelector) GetSelection() isWorkspaceSelector_Selection { + if x != nil { + return x.Selection + } + return nil +} + +func (x *WorkspaceSelector) GetWorkspace() string { + if x != nil { + if x, ok := x.Selection.(*WorkspaceSelector_Workspace); ok { + return x.Workspace + } + } + return "" +} + +func (x *WorkspaceSelector) GetAllWorkspaces() *AllWorkspaces { + if x != nil { + if x, ok := x.Selection.(*WorkspaceSelector_AllWorkspaces); ok { + return x.AllWorkspaces + } + } + return nil +} + +type isWorkspaceSelector_Selection interface { + isWorkspaceSelector_Selection() +} + +type WorkspaceSelector_Workspace struct { + // One explicitly named workspace. Use `default` to select the gateway's + // default workspace; an empty name is invalid. + Workspace string `protobuf:"bytes,1,opt,name=workspace,proto3,oneof"` +} + +type WorkspaceSelector_AllWorkspaces struct { + // All workspaces the caller is authorized to access. Only supported by + // requests that explicitly document cross-workspace behavior. + AllWorkspaces *AllWorkspaces `protobuf:"bytes,2,opt,name=all_workspaces,json=allWorkspaces,proto3,oneof"` +} + +func (*WorkspaceSelector_Workspace) isWorkspaceSelector_Selection() {} + +func (*WorkspaceSelector_AllWorkspaces) isWorkspaceSelector_Selection() {} + +// Marker for the all-workspaces selector variant. +type AllWorkspaces struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AllWorkspaces) Reset() { + *x = AllWorkspaces{} + mi := &file_datamodel_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AllWorkspaces) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AllWorkspaces) ProtoMessage() {} + +func (x *AllWorkspaces) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[1] + 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 AllWorkspaces.ProtoReflect.Descriptor instead. +func (*AllWorkspaces) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{1} +} + +// Kubernetes-style metadata shared by all top-level OpenShell domain objects. +// +// This structure provides consistent metadata (identity, labels, annotations, +// timestamps, resource versioning) across Sandbox, Provider, SshSession, and +// other resources. +type ObjectMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable object ID generated by the gateway. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Human-readable object name (unique per object type). + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Time when the object was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + // Key-value labels for filtering and organization. + // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. + Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optimistic concurrency control version. + // Incremented by the gateway on each update. Clients can use this for compare-and-swap operations. + ResourceVersion uint64 `protobuf:"varint,5,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + // Opaque key-value metadata that is not used for selectors. + // Annotation keys use the same qualified-key shape as labels, but values may be longer. + Annotations map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Workspace that owns this resource. Empty is normalized to "default" by the + // gateway. Immutable after creation. + Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Time when graceful deletion was initiated. Absence means the object is + // not being deleted. Once set, this field is + // immutable — the only path forward is completing deletion. + DeletionTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=deletion_time,json=deletionTime,proto3" json:"deletion_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectMeta) Reset() { + *x = ObjectMeta{} + mi := &file_datamodel_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectMeta) ProtoMessage() {} + +func (x *ObjectMeta) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[2] + 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 ObjectMeta.ProtoReflect.Descriptor instead. +func (*ObjectMeta) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{2} +} + +func (x *ObjectMeta) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ObjectMeta) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ObjectMeta) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *ObjectMeta) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *ObjectMeta) GetResourceVersion() uint64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + +func (x *ObjectMeta) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *ObjectMeta) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ObjectMeta) GetDeletionTime() *timestamppb.Timestamp { + if x != nil { + return x.DeletionTime + } + return nil +} + +// Status of a workspace. +type WorkspaceStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Phase WorkspacePhase `protobuf:"varint,1,opt,name=phase,proto3,enum=openshell.datamodel.v1.WorkspacePhase" json:"phase,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceStatus) Reset() { + *x = WorkspaceStatus{} + mi := &file_datamodel_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceStatus) ProtoMessage() {} + +func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[3] + 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 WorkspaceStatus.ProtoReflect.Descriptor instead. +func (*WorkspaceStatus) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{3} +} + +func (x *WorkspaceStatus) GetPhase() WorkspacePhase { + if x != nil { + return x.Phase + } + return WorkspacePhase_WORKSPACE_PHASE_UNSPECIFIED +} + +// Workspace resource. A hard isolation boundary for sandboxes, providers, and +// other workspace-scoped resources. +type Workspace struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + // The workspace field in this ObjectMeta is unused (a workspace does not + // belong to another workspace). + Metadata *ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Current lifecycle status. + Status *WorkspaceStatus `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Workspace) Reset() { + *x = Workspace{} + mi := &file_datamodel_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Workspace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Workspace) ProtoMessage() {} + +func (x *Workspace) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[4] + 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 Workspace.ProtoReflect.Descriptor instead. +func (*Workspace) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{4} +} + +func (x *Workspace) GetMetadata() *ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Workspace) GetStatus() *WorkspaceStatus { + if x != nil { + return x.Status + } + return nil +} + +// Opaque handle for a provider credential stored by gateway credential storage. +// Handles are created by OpenShell and must not be authored by users. +type CredentialHandle struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal storage owner or credential driver that owns this handle. + Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"` + // Owner-owned opaque handle string. + Handle string `protobuf:"bytes,2,opt,name=handle,proto3" json:"handle,omitempty"` + // Owner-owned non-secret metadata. + Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CredentialHandle) Reset() { + *x = CredentialHandle{} + mi := &file_datamodel_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CredentialHandle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialHandle) ProtoMessage() {} + +func (x *CredentialHandle) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[5] + 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 CredentialHandle.ProtoReflect.Descriptor instead. +func (*CredentialHandle) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{5} +} + +func (x *CredentialHandle) GetDriver() string { + if x != nil { + return x.Driver + } + return "" +} + +func (x *CredentialHandle) GetHandle() string { + if x != nil { + return x.Handle + } + return "" +} + +func (x *CredentialHandle) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// Provider model stored by OpenShell. +type Provider struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // ID of the provider profile this provider was created from, matched + // exactly (for example: "claude-code", "github"). + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // Secret values used for authentication. + Credentials map[string]string `protobuf:"bytes,3,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Non-secret provider configuration. + Config map[string]string `protobuf:"bytes,4,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Expiration times for credential values, keyed by credential/env var name. + // A missing key means the credential does not expire. + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,105,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Workspace where this provider's type profile is stored. + // Empty string = platform/global scope. Must be empty or match + // metadata.workspace; cross-workspace references are rejected. + ProfileWorkspace string `protobuf:"bytes,6,opt,name=profile_workspace,json=profileWorkspace,proto3" json:"profile_workspace,omitempty"` + // Opaque handles for secret values stored through gateway credential storage. + // This map is internal gateway state and is not accepted as user-authored input. + CredentialHandles map[string]*CredentialHandle `protobuf:"bytes,7,rep,name=credential_handles,json=credentialHandles,proto3" json:"credential_handles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Provider) Reset() { + *x = Provider{} + mi := &file_datamodel_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Provider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Provider) ProtoMessage() {} + +func (x *Provider) ProtoReflect() protoreflect.Message { + mi := &file_datamodel_proto_msgTypes[6] + 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 Provider.ProtoReflect.Descriptor instead. +func (*Provider) Descriptor() ([]byte, []int) { + return file_datamodel_proto_rawDescGZIP(), []int{6} +} + +func (x *Provider) GetMetadata() *ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Provider) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Provider) GetCredentials() map[string]string { + if x != nil { + return x.Credentials + } + return nil +} + +func (x *Provider) GetConfig() map[string]string { + if x != nil { + return x.Config + } + return nil +} + +func (x *Provider) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { + if x != nil { + return x.CredentialExpirationTimes + } + return nil +} + +func (x *Provider) GetProfileWorkspace() string { + if x != nil { + return x.ProfileWorkspace + } + return "" +} + +func (x *Provider) GetCredentialHandles() map[string]*CredentialHandle { + if x != nil { + return x.CredentialHandles + } + return nil +} + +var File_datamodel_proto protoreflect.FileDescriptor + +const file_datamodel_proto_rawDesc = "" + + "\n" + + "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x90\x01\n" + + "\x11WorkspaceSelector\x12\x1e\n" + + "\tworkspace\x18\x01 \x01(\tH\x00R\tworkspace\x12N\n" + + "\x0eall_workspaces\x18\x02 \x01(\v2%.openshell.datamodel.v1.AllWorkspacesH\x00R\rallWorkspacesB\v\n" + + "\tselection\"\x0f\n" + + "\rAllWorkspaces\"\xc5\x04\n" + + "\n" + + "ObjectMeta\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12=\n" + + "\fcreated_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12F\n" + + "\x06labels\x18\x04 \x03(\v2..openshell.datamodel.v1.ObjectMeta.LabelsEntryR\x06labels\x12)\n" + + "\x10resource_version\x18\x05 \x01(\x04R\x0fresourceVersion\x12U\n" + + "\vannotations\x18\x06 \x03(\v23.openshell.datamodel.v1.ObjectMeta.AnnotationsEntryR\vannotations\x12\x1c\n" + + "\tworkspace\x18\a \x01(\tR\tworkspace\x12?\n" + + "\rdeletion_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\fdeletionTime\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x03\x10\x04J\x04\b\b\x10\tR\rcreated_at_msR\x15deletion_timestamp_ms\"O\n" + + "\x0fWorkspaceStatus\x12<\n" + + "\x05phase\x18\x01 \x01(\x0e2&.openshell.datamodel.v1.WorkspacePhaseR\x05phase\"\x8c\x01\n" + + "\tWorkspace\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12?\n" + + "\x06status\x18\x02 \x01(\v2'.openshell.datamodel.v1.WorkspaceStatusR\x06status\"\xd3\x01\n" + + "\x10CredentialHandle\x12\x16\n" + + "\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" + + "\x06handle\x18\x02 \x01(\tR\x06handle\x12R\n" + + "\bmetadata\x18\x03 \x03(\v26.openshell.datamodel.v1.CredentialHandle.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8a\a\n" + + "\bProvider\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12Y\n" + + "\vcredentials\x18\x03 \x03(\v21.openshell.datamodel.v1.Provider.CredentialsEntryB\x04\x88\xb5\x18\x01R\vcredentials\x12D\n" + + "\x06config\x18\x04 \x03(\v2,.openshell.datamodel.v1.Provider.ConfigEntryR\x06config\x12\x7f\n" + + "\x1bcredential_expiration_times\x18i \x03(\v2?.openshell.datamodel.v1.Provider.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12+\n" + + "\x11profile_workspace\x18\x06 \x01(\tR\x10profileWorkspace\x12f\n" + + "\x12credential_handles\x18\a \x03(\v27.openshell.datamodel.v1.Provider.CredentialHandlesEntryR\x11credentialHandles\x1a>\n" + + "\x10CredentialsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + + "\vConfigEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01\x1an\n" + + "\x16CredentialHandlesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12>\n" + + "\x05value\x18\x02 \x01(\v2(.openshell.datamodel.v1.CredentialHandleR\x05value:\x028\x01J\x04\b\x05\x10\x06R\x18credential_expires_at_ms*n\n" + + "\x0eWorkspacePhase\x12\x1f\n" + + "\x1bWORKSPACE_PHASE_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16WORKSPACE_PHASE_ACTIVE\x10\x01\x12\x1f\n" + + "\x1bWORKSPACE_PHASE_TERMINATING\x10\x02B\xf7\x01\n" + + "\x1acom.openshell.datamodel.v1B\x0eDatamodelProtoP\x01ZOgithub.com/agentnameservice/ans-openshell/gen/openshell/datamodelv1;datamodelv1\xa2\x02\x03ODX\xaa\x02\x16Openshell.Datamodel.V1\xca\x02\x16Openshell\\Datamodel\\V1\xe2\x02\"Openshell\\Datamodel\\V1\\GPBMetadata\xea\x02\x18Openshell::Datamodel::V1b\x06proto3" + +var ( + file_datamodel_proto_rawDescOnce sync.Once + file_datamodel_proto_rawDescData []byte +) + +func file_datamodel_proto_rawDescGZIP() []byte { + file_datamodel_proto_rawDescOnce.Do(func() { + file_datamodel_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_datamodel_proto_rawDesc), len(file_datamodel_proto_rawDesc))) + }) + return file_datamodel_proto_rawDescData +} + +var file_datamodel_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_datamodel_proto_goTypes = []any{ + (WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase + (*WorkspaceSelector)(nil), // 1: openshell.datamodel.v1.WorkspaceSelector + (*AllWorkspaces)(nil), // 2: openshell.datamodel.v1.AllWorkspaces + (*ObjectMeta)(nil), // 3: openshell.datamodel.v1.ObjectMeta + (*WorkspaceStatus)(nil), // 4: openshell.datamodel.v1.WorkspaceStatus + (*Workspace)(nil), // 5: openshell.datamodel.v1.Workspace + (*CredentialHandle)(nil), // 6: openshell.datamodel.v1.CredentialHandle + (*Provider)(nil), // 7: openshell.datamodel.v1.Provider + nil, // 8: openshell.datamodel.v1.ObjectMeta.LabelsEntry + nil, // 9: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + nil, // 10: openshell.datamodel.v1.CredentialHandle.MetadataEntry + nil, // 11: openshell.datamodel.v1.Provider.CredentialsEntry + nil, // 12: openshell.datamodel.v1.Provider.ConfigEntry + nil, // 13: openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry + nil, // 14: openshell.datamodel.v1.Provider.CredentialHandlesEntry + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp +} +var file_datamodel_proto_depIdxs = []int32{ + 2, // 0: openshell.datamodel.v1.WorkspaceSelector.all_workspaces:type_name -> openshell.datamodel.v1.AllWorkspaces + 15, // 1: openshell.datamodel.v1.ObjectMeta.created_time:type_name -> google.protobuf.Timestamp + 8, // 2: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry + 9, // 3: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry + 15, // 4: openshell.datamodel.v1.ObjectMeta.deletion_time:type_name -> google.protobuf.Timestamp + 0, // 5: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase + 3, // 6: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 4, // 7: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus + 10, // 8: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry + 3, // 9: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 11, // 10: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry + 12, // 11: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry + 13, // 12: openshell.datamodel.v1.Provider.credential_expiration_times:type_name -> openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry + 14, // 13: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry + 15, // 14: openshell.datamodel.v1.Provider.CredentialExpirationTimesEntry.value:type_name -> google.protobuf.Timestamp + 6, // 15: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name +} + +func init() { file_datamodel_proto_init() } +func file_datamodel_proto_init() { + if File_datamodel_proto != nil { + return + } + file_datamodel_proto_msgTypes[0].OneofWrappers = []any{ + (*WorkspaceSelector_Workspace)(nil), + (*WorkspaceSelector_AllWorkspaces)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_datamodel_proto_rawDesc), len(file_datamodel_proto_rawDesc)), + NumEnums: 1, + NumMessages: 14, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_datamodel_proto_goTypes, + DependencyIndexes: file_datamodel_proto_depIdxs, + EnumInfos: file_datamodel_proto_enumTypes, + MessageInfos: file_datamodel_proto_msgTypes, + }.Build() + File_datamodel_proto = out.File + file_datamodel_proto_goTypes = nil + file_datamodel_proto_depIdxs = nil +} diff --git a/gen/openshell/extensionv1/extension.pb.go b/gen/openshell/extensionv1/extension.pb.go new file mode 100644 index 0000000..40a1514 --- /dev/null +++ b/gen/openshell/extensionv1/extension.pb.go @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: extension.proto + +package extensionv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Version of an OpenShell extension protocol contract. +// +// The major version changes when peers cannot safely interoperate. Minor +// versions add optional fields or capabilities and remain compatible when both +// peers' required capabilities are satisfied. +type ProtocolVersion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Major uint32 `protobuf:"varint,1,opt,name=major,proto3" json:"major,omitempty"` + Minor uint32 `protobuf:"varint,2,opt,name=minor,proto3" json:"minor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProtocolVersion) Reset() { + *x = ProtocolVersion{} + mi := &file_extension_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProtocolVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProtocolVersion) ProtoMessage() {} + +func (x *ProtocolVersion) ProtoReflect() protoreflect.Message { + mi := &file_extension_proto_msgTypes[0] + 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 ProtocolVersion.ProtoReflect.Descriptor instead. +func (*ProtocolVersion) Descriptor() ([]byte, []int) { + return file_extension_proto_rawDescGZIP(), []int{0} +} + +func (x *ProtocolVersion) GetMajor() uint32 { + if x != nil { + return x.Major + } + return 0 +} + +func (x *ProtocolVersion) GetMinor() uint32 { + if x != nil { + return x.Minor + } + return 0 +} + +// Non-secret metadata exchanged by the gateway and an extension before use. +type PeerMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Extension-family protocol contract version, distinct from the build. + ProtocolVersion *ProtocolVersion `protobuf:"bytes,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` + // Stable human-readable implementation identity, such as "openshell/docker". + ImplementationName string `protobuf:"bytes,2,opt,name=implementation_name,json=implementationName,proto3" json:"implementation_name,omitempty"` + // Implementation or build version used only for diagnostics. + ImplementationVersion string `protobuf:"bytes,3,opt,name=implementation_version,json=implementationVersion,proto3" json:"implementation_version,omitempty"` + // Optional namespaced capabilities this peer understands. + SupportedCapabilities []string `protobuf:"bytes,4,rep,name=supported_capabilities,json=supportedCapabilities,proto3" json:"supported_capabilities,omitempty"` + // Capabilities that the opposite peer must advertise. + RequiredCapabilities []string `protobuf:"bytes,5,rep,name=required_capabilities,json=requiredCapabilities,proto3" json:"required_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerMetadata) Reset() { + *x = PeerMetadata{} + mi := &file_extension_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerMetadata) ProtoMessage() {} + +func (x *PeerMetadata) ProtoReflect() protoreflect.Message { + mi := &file_extension_proto_msgTypes[1] + 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 PeerMetadata.ProtoReflect.Descriptor instead. +func (*PeerMetadata) Descriptor() ([]byte, []int) { + return file_extension_proto_rawDescGZIP(), []int{1} +} + +func (x *PeerMetadata) GetProtocolVersion() *ProtocolVersion { + if x != nil { + return x.ProtocolVersion + } + return nil +} + +func (x *PeerMetadata) GetImplementationName() string { + if x != nil { + return x.ImplementationName + } + return "" +} + +func (x *PeerMetadata) GetImplementationVersion() string { + if x != nil { + return x.ImplementationVersion + } + return "" +} + +func (x *PeerMetadata) GetSupportedCapabilities() []string { + if x != nil { + return x.SupportedCapabilities + } + return nil +} + +func (x *PeerMetadata) GetRequiredCapabilities() []string { + if x != nil { + return x.RequiredCapabilities + } + return nil +} + +var File_extension_proto protoreflect.FileDescriptor + +const file_extension_proto_rawDesc = "" + + "\n" + + "\x0fextension.proto\x12\x16openshell.extension.v1\"=\n" + + "\x0fProtocolVersion\x12\x14\n" + + "\x05major\x18\x01 \x01(\rR\x05major\x12\x14\n" + + "\x05minor\x18\x02 \x01(\rR\x05minor\"\xb6\x02\n" + + "\fPeerMetadata\x12R\n" + + "\x10protocol_version\x18\x01 \x01(\v2'.openshell.extension.v1.ProtocolVersionR\x0fprotocolVersion\x12/\n" + + "\x13implementation_name\x18\x02 \x01(\tR\x12implementationName\x125\n" + + "\x16implementation_version\x18\x03 \x01(\tR\x15implementationVersion\x125\n" + + "\x16supported_capabilities\x18\x04 \x03(\tR\x15supportedCapabilities\x123\n" + + "\x15required_capabilities\x18\x05 \x03(\tR\x14requiredCapabilitiesB\xf7\x01\n" + + "\x1acom.openshell.extension.v1B\x0eExtensionProtoP\x01ZOgithub.com/agentnameservice/ans-openshell/gen/openshell/extensionv1;extensionv1\xa2\x02\x03OEX\xaa\x02\x16Openshell.Extension.V1\xca\x02\x16Openshell\\Extension\\V1\xe2\x02\"Openshell\\Extension\\V1\\GPBMetadata\xea\x02\x18Openshell::Extension::V1b\x06proto3" + +var ( + file_extension_proto_rawDescOnce sync.Once + file_extension_proto_rawDescData []byte +) + +func file_extension_proto_rawDescGZIP() []byte { + file_extension_proto_rawDescOnce.Do(func() { + file_extension_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_extension_proto_rawDesc), len(file_extension_proto_rawDesc))) + }) + return file_extension_proto_rawDescData +} + +var file_extension_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_extension_proto_goTypes = []any{ + (*ProtocolVersion)(nil), // 0: openshell.extension.v1.ProtocolVersion + (*PeerMetadata)(nil), // 1: openshell.extension.v1.PeerMetadata +} +var file_extension_proto_depIdxs = []int32{ + 0, // 0: openshell.extension.v1.PeerMetadata.protocol_version:type_name -> openshell.extension.v1.ProtocolVersion + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_extension_proto_init() } +func file_extension_proto_init() { + if File_extension_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_extension_proto_rawDesc), len(file_extension_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_extension_proto_goTypes, + DependencyIndexes: file_extension_proto_depIdxs, + MessageInfos: file_extension_proto_msgTypes, + }.Build() + File_extension_proto = out.File + file_extension_proto_goTypes = nil + file_extension_proto_depIdxs = nil +} diff --git a/gen/openshell/interceptorv1/gateway_interceptor.pb.go b/gen/openshell/interceptorv1/gateway_interceptor.pb.go new file mode 100644 index 0000000..3a38e58 --- /dev/null +++ b/gen/openshell/interceptorv1/gateway_interceptor.pb.go @@ -0,0 +1,1062 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: gateway_interceptor.proto + +package interceptorv1 + +import ( + extensionv1 "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + openshellv1 "github.com/agentnameservice/ans-openshell/gen/openshell/openshellv1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GatewayInterceptorPhase int32 + +const ( + GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED GatewayInterceptorPhase = 0 + GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION GatewayInterceptorPhase = 2 + GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_VALIDATE GatewayInterceptorPhase = 3 + GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT GatewayInterceptorPhase = 4 +) + +// Enum value maps for GatewayInterceptorPhase. +var ( + GatewayInterceptorPhase_name = map[int32]string{ + 0: "GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED", + 2: "GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION", + 3: "GATEWAY_INTERCEPTOR_PHASE_VALIDATE", + 4: "GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT", + } + GatewayInterceptorPhase_value = map[string]int32{ + "GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED": 0, + "GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION": 2, + "GATEWAY_INTERCEPTOR_PHASE_VALIDATE": 3, + "GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT": 4, + } +) + +func (x GatewayInterceptorPhase) Enum() *GatewayInterceptorPhase { + p := new(GatewayInterceptorPhase) + *p = x + return p +} + +func (x GatewayInterceptorPhase) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GatewayInterceptorPhase) Descriptor() protoreflect.EnumDescriptor { + return file_gateway_interceptor_proto_enumTypes[0].Descriptor() +} + +func (GatewayInterceptorPhase) Type() protoreflect.EnumType { + return &file_gateway_interceptor_proto_enumTypes[0] +} + +func (x GatewayInterceptorPhase) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GatewayInterceptorPhase.Descriptor instead. +func (GatewayInterceptorPhase) EnumDescriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{0} +} + +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway protocol metadata. Interceptors must reject unmet requirements. + Gateway *extensionv1.PeerMetadata `protobuf:"bytes,1,opt,name=gateway,proto3" json:"gateway,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_gateway_interceptor_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[0] + 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 DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{0} +} + +func (x *DescribeRequest) GetGateway() *extensionv1.PeerMetadata { + if x != nil { + return x.Gateway + } + return nil +} + +type ProviderProfileSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileSnapshotRequest) Reset() { + *x = ProviderProfileSnapshotRequest{} + mi := &file_gateway_interceptor_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileSnapshotRequest) ProtoMessage() {} + +func (x *ProviderProfileSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[1] + 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 ProviderProfileSnapshotRequest.ProtoReflect.Descriptor instead. +func (*ProviderProfileSnapshotRequest) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{1} +} + +type InterceptorEvaluation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Configured interceptor instance name. + InterceptorName string `protobuf:"bytes,1,opt,name=interceptor_name,json=interceptorName,proto3" json:"interceptor_name,omitempty"` + // Manifest binding id selected for this evaluation. + BindingId string `protobuf:"bytes,2,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + // Public gRPC service name, e.g. "openshell.v1.OpenShell". + Service string `protobuf:"bytes,3,opt,name=service,proto3" json:"service,omitempty"` + // Public gRPC method name, e.g. "CreateSandbox". + Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` + // Caller identity summary. Values are intentionally non-secret. + Principal map[string]string `protobuf:"bytes,5,rep,name=principal,proto3" json:"principal,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Exactly one phase payload is set for each evaluation. + // + // Types that are valid to be assigned to Phase: + // + // *InterceptorEvaluation_ModifyOperation + // *InterceptorEvaluation_Validate + // *InterceptorEvaluation_PostCommit + Phase isInterceptorEvaluation_Phase `protobuf_oneof:"phase"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterceptorEvaluation) Reset() { + *x = InterceptorEvaluation{} + mi := &file_gateway_interceptor_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterceptorEvaluation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterceptorEvaluation) ProtoMessage() {} + +func (x *InterceptorEvaluation) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[2] + 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 InterceptorEvaluation.ProtoReflect.Descriptor instead. +func (*InterceptorEvaluation) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{2} +} + +func (x *InterceptorEvaluation) GetInterceptorName() string { + if x != nil { + return x.InterceptorName + } + return "" +} + +func (x *InterceptorEvaluation) GetBindingId() string { + if x != nil { + return x.BindingId + } + return "" +} + +func (x *InterceptorEvaluation) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *InterceptorEvaluation) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *InterceptorEvaluation) GetPrincipal() map[string]string { + if x != nil { + return x.Principal + } + return nil +} + +func (x *InterceptorEvaluation) GetPhase() isInterceptorEvaluation_Phase { + if x != nil { + return x.Phase + } + return nil +} + +func (x *InterceptorEvaluation) GetModifyOperation() *ModifyOperationEvaluation { + if x != nil { + if x, ok := x.Phase.(*InterceptorEvaluation_ModifyOperation); ok { + return x.ModifyOperation + } + } + return nil +} + +func (x *InterceptorEvaluation) GetValidate() *ValidateEvaluation { + if x != nil { + if x, ok := x.Phase.(*InterceptorEvaluation_Validate); ok { + return x.Validate + } + } + return nil +} + +func (x *InterceptorEvaluation) GetPostCommit() *PostCommitEvaluation { + if x != nil { + if x, ok := x.Phase.(*InterceptorEvaluation_PostCommit); ok { + return x.PostCommit + } + } + return nil +} + +type isInterceptorEvaluation_Phase interface { + isInterceptorEvaluation_Phase() +} + +type InterceptorEvaluation_ModifyOperation struct { + // Evaluation that may modify the prepared gateway operation. + ModifyOperation *ModifyOperationEvaluation `protobuf:"bytes,6,opt,name=modify_operation,json=modifyOperation,proto3,oneof"` +} + +type InterceptorEvaluation_Validate struct { + // Evaluation that may reject, but not mutate, the prepared operation. + Validate *ValidateEvaluation `protobuf:"bytes,7,opt,name=validate,proto3,oneof"` +} + +type InterceptorEvaluation_PostCommit struct { + // Evaluation after the gateway operation has completed successfully. + PostCommit *PostCommitEvaluation `protobuf:"bytes,8,opt,name=post_commit,json=postCommit,proto3,oneof"` +} + +func (*InterceptorEvaluation_ModifyOperation) isInterceptorEvaluation_Phase() {} + +func (*InterceptorEvaluation_Validate) isInterceptorEvaluation_Phase() {} + +func (*InterceptorEvaluation_PostCommit) isInterceptorEvaluation_Phase() {} + +type ModifyOperationEvaluation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-prepared operation the gateway proposes to execute. + ProposedOperation *structpb.Struct `protobuf:"bytes,1,opt,name=proposed_operation,json=proposedOperation,proto3" json:"proposed_operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModifyOperationEvaluation) Reset() { + *x = ModifyOperationEvaluation{} + mi := &file_gateway_interceptor_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModifyOperationEvaluation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModifyOperationEvaluation) ProtoMessage() {} + +func (x *ModifyOperationEvaluation) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[3] + 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 ModifyOperationEvaluation.ProtoReflect.Descriptor instead. +func (*ModifyOperationEvaluation) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{3} +} + +func (x *ModifyOperationEvaluation) GetProposedOperation() *structpb.Struct { + if x != nil { + return x.ProposedOperation + } + return nil +} + +type ValidateEvaluation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-prepared operation after earlier interceptor modifications. + ProposedOperation *structpb.Struct `protobuf:"bytes,1,opt,name=proposed_operation,json=proposedOperation,proto3" json:"proposed_operation,omitempty"` + // Optional read-only gateway state loaded before applying the operation. + CurrentState *structpb.Struct `protobuf:"bytes,2,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEvaluation) Reset() { + *x = ValidateEvaluation{} + mi := &file_gateway_interceptor_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEvaluation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEvaluation) ProtoMessage() {} + +func (x *ValidateEvaluation) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[4] + 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 ValidateEvaluation.ProtoReflect.Descriptor instead. +func (*ValidateEvaluation) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{4} +} + +func (x *ValidateEvaluation) GetProposedOperation() *structpb.Struct { + if x != nil { + return x.ProposedOperation + } + return nil +} + +func (x *ValidateEvaluation) GetCurrentState() *structpb.Struct { + if x != nil { + return x.CurrentState + } + return nil +} + +type PostCommitEvaluation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Protobuf JSON-shaped response returned by the committed gateway operation. + CommittedResponse *structpb.Struct `protobuf:"bytes,1,opt,name=committed_response,json=committedResponse,proto3" json:"committed_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostCommitEvaluation) Reset() { + *x = PostCommitEvaluation{} + mi := &file_gateway_interceptor_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostCommitEvaluation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostCommitEvaluation) ProtoMessage() {} + +func (x *PostCommitEvaluation) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[5] + 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 PostCommitEvaluation.ProtoReflect.Descriptor instead. +func (*PostCommitEvaluation) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{5} +} + +func (x *PostCommitEvaluation) GetCommittedResponse() *structpb.Struct { + if x != nil { + return x.CommittedResponse + } + return nil +} + +type InterceptorResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // False denies the operation before side effects for modify_operation and + // validate. Post-commit denial is invalid. + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + // Human-readable reason for logs and denied gRPC status messages. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional gRPC status code name for denials, e.g. "PERMISSION_DENIED". + StatusCode string `protobuf:"bytes,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + // RFC 6902 JSON patches. Only valid during modify_operation. + Patches []*JsonPatch `protobuf:"bytes,4,rep,name=patches,proto3" json:"patches,omitempty"` + // Non-secret annotations included in gateway logs. + LogAnnotations map[string]string `protobuf:"bytes,5,rep,name=log_annotations,json=logAnnotations,proto3" json:"log_annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterceptorResult) Reset() { + *x = InterceptorResult{} + mi := &file_gateway_interceptor_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterceptorResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterceptorResult) ProtoMessage() {} + +func (x *InterceptorResult) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[6] + 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 InterceptorResult.ProtoReflect.Descriptor instead. +func (*InterceptorResult) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{6} +} + +func (x *InterceptorResult) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *InterceptorResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *InterceptorResult) GetStatusCode() string { + if x != nil { + return x.StatusCode + } + return "" +} + +func (x *InterceptorResult) GetPatches() []*JsonPatch { + if x != nil { + return x.Patches + } + return nil +} + +func (x *InterceptorResult) GetLogAnnotations() map[string]string { + if x != nil { + return x.LogAnnotations + } + return nil +} + +type InterceptorManifest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable interceptor name declared by the service. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Bindings declared by the interceptor service. + Bindings []*InterceptorBinding `protobuf:"bytes,2,rep,name=bindings,proto3" json:"bindings,omitempty"` + // Optional default failure policy for bindings without their own policy. + // Supported values are "fail_closed" and "fail_open". + FailurePolicy string `protobuf:"bytes,3,opt,name=failure_policy,json=failurePolicy,proto3" json:"failure_policy,omitempty"` + // True when this interceptor implements SnapshotProviderProfiles. + ProviderProfiles bool `protobuf:"varint,4,opt,name=provider_profiles,json=providerProfiles,proto3" json:"provider_profiles,omitempty"` + // Exact JWT audience this service verifies on inbound OpenShell calls. + // After authenticated Describe succeeds, the gateway refuses to start unless + // this matches the operator-configured audience. A strict verifier may reject + // an incorrect audience before returning this manifest. Empty skips this + // post-authentication consistency check. + ExpectedAudience string `protobuf:"bytes,5,opt,name=expected_audience,json=expectedAudience,proto3" json:"expected_audience,omitempty"` + // Gateway-interceptor protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.gateway-interceptor.contract" in + // supported_capabilities. + Extension *extensionv1.PeerMetadata `protobuf:"bytes,6,opt,name=extension,proto3" json:"extension,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterceptorManifest) Reset() { + *x = InterceptorManifest{} + mi := &file_gateway_interceptor_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterceptorManifest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterceptorManifest) ProtoMessage() {} + +func (x *InterceptorManifest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[7] + 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 InterceptorManifest.ProtoReflect.Descriptor instead. +func (*InterceptorManifest) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{7} +} + +func (x *InterceptorManifest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *InterceptorManifest) GetBindings() []*InterceptorBinding { + if x != nil { + return x.Bindings + } + return nil +} + +func (x *InterceptorManifest) GetFailurePolicy() string { + if x != nil { + return x.FailurePolicy + } + return "" +} + +func (x *InterceptorManifest) GetProviderProfiles() bool { + if x != nil { + return x.ProviderProfiles + } + return false +} + +func (x *InterceptorManifest) GetExpectedAudience() string { + if x != nil { + return x.ExpectedAudience + } + return "" +} + +func (x *InterceptorManifest) GetExtension() *extensionv1.PeerMetadata { + if x != nil { + return x.Extension + } + return nil +} + +type ProviderProfileSnapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Opaque source revision used for cache freshness and sandbox reload checks. + Revision string `protobuf:"bytes,1,opt,name=revision,proto3" json:"revision,omitempty"` + // Complete profile snapshot vended by this source. + Profiles []*openshellv1.ProviderProfile `protobuf:"bytes,2,rep,name=profiles,proto3" json:"profiles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileSnapshot) Reset() { + *x = ProviderProfileSnapshot{} + mi := &file_gateway_interceptor_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileSnapshot) ProtoMessage() {} + +func (x *ProviderProfileSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[8] + 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 ProviderProfileSnapshot.ProtoReflect.Descriptor instead. +func (*ProviderProfileSnapshot) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{8} +} + +func (x *ProviderProfileSnapshot) GetRevision() string { + if x != nil { + return x.Revision + } + return "" +} + +func (x *ProviderProfileSnapshot) GetProfiles() []*openshellv1.ProviderProfile { + if x != nil { + return x.Profiles + } + return nil +} + +type InterceptorBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable binding id used for config overrides and audit logs. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // RPC selector. Selectors are intentionally tied to the public API shape. + Selector *InterceptorSelector `protobuf:"bytes,2,opt,name=selector,proto3" json:"selector,omitempty"` + // Phases this binding wants to evaluate. + Phases []GatewayInterceptorPhase `protobuf:"varint,3,rep,packed,name=phases,proto3,enum=openshell.gateway_interceptor.v1.GatewayInterceptorPhase" json:"phases,omitempty"` + // Optional binding-specific failure policy. + // Supported values are "fail_closed" and "fail_open". + // Bindings that include post_commit must resolve to "fail_open" because the + // gateway operation has already committed before that phase runs. + FailurePolicy string `protobuf:"bytes,4,opt,name=failure_policy,json=failurePolicy,proto3" json:"failure_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterceptorBinding) Reset() { + *x = InterceptorBinding{} + mi := &file_gateway_interceptor_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterceptorBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterceptorBinding) ProtoMessage() {} + +func (x *InterceptorBinding) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[9] + 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 InterceptorBinding.ProtoReflect.Descriptor instead. +func (*InterceptorBinding) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{9} +} + +func (x *InterceptorBinding) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *InterceptorBinding) GetSelector() *InterceptorSelector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *InterceptorBinding) GetPhases() []GatewayInterceptorPhase { + if x != nil { + return x.Phases + } + return nil +} + +func (x *InterceptorBinding) GetFailurePolicy() string { + if x != nil { + return x.FailurePolicy + } + return "" +} + +type InterceptorSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Full selector form: "openshell.v1.OpenShell/CreateSandbox". + Rpc string `protobuf:"bytes,1,opt,name=rpc,proto3" json:"rpc,omitempty"` + // Structured service/method form. If rpc is set, it takes precedence. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InterceptorSelector) Reset() { + *x = InterceptorSelector{} + mi := &file_gateway_interceptor_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InterceptorSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InterceptorSelector) ProtoMessage() {} + +func (x *InterceptorSelector) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[10] + 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 InterceptorSelector.ProtoReflect.Descriptor instead. +func (*InterceptorSelector) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{10} +} + +func (x *InterceptorSelector) GetRpc() string { + if x != nil { + return x.Rpc + } + return "" +} + +func (x *InterceptorSelector) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *InterceptorSelector) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +type JsonPatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + Op string `protobuf:"bytes,1,opt,name=op,proto3" json:"op,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Value *structpb.Value `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + From string `protobuf:"bytes,4,opt,name=from,proto3" json:"from,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JsonPatch) Reset() { + *x = JsonPatch{} + mi := &file_gateway_interceptor_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JsonPatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JsonPatch) ProtoMessage() {} + +func (x *JsonPatch) ProtoReflect() protoreflect.Message { + mi := &file_gateway_interceptor_proto_msgTypes[11] + 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 JsonPatch.ProtoReflect.Descriptor instead. +func (*JsonPatch) Descriptor() ([]byte, []int) { + return file_gateway_interceptor_proto_rawDescGZIP(), []int{11} +} + +func (x *JsonPatch) GetOp() string { + if x != nil { + return x.Op + } + return "" +} + +func (x *JsonPatch) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *JsonPatch) GetValue() *structpb.Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *JsonPatch) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +var File_gateway_interceptor_proto protoreflect.FileDescriptor + +const file_gateway_interceptor_proto_rawDesc = "" + + "\n" + + "\x19gateway_interceptor.proto\x12 openshell.gateway_interceptor.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x0fextension.proto\x1a\x0fopenshell.proto\"Q\n" + + "\x0fDescribeRequest\x12>\n" + + "\agateway\x18\x01 \x01(\v2$.openshell.extension.v1.PeerMetadataR\agateway\" \n" + + "\x1eProviderProfileSnapshotRequest\"\xd9\x04\n" + + "\x15InterceptorEvaluation\x12)\n" + + "\x10interceptor_name\x18\x01 \x01(\tR\x0finterceptorName\x12\x1d\n" + + "\n" + + "binding_id\x18\x02 \x01(\tR\tbindingId\x12\x18\n" + + "\aservice\x18\x03 \x01(\tR\aservice\x12\x16\n" + + "\x06method\x18\x04 \x01(\tR\x06method\x12d\n" + + "\tprincipal\x18\x05 \x03(\v2F.openshell.gateway_interceptor.v1.InterceptorEvaluation.PrincipalEntryR\tprincipal\x12h\n" + + "\x10modify_operation\x18\x06 \x01(\v2;.openshell.gateway_interceptor.v1.ModifyOperationEvaluationH\x00R\x0fmodifyOperation\x12R\n" + + "\bvalidate\x18\a \x01(\v24.openshell.gateway_interceptor.v1.ValidateEvaluationH\x00R\bvalidate\x12Y\n" + + "\vpost_commit\x18\b \x01(\v26.openshell.gateway_interceptor.v1.PostCommitEvaluationH\x00R\n" + + "postCommit\x1a<\n" + + "\x0ePrincipalEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\a\n" + + "\x05phase\"c\n" + + "\x19ModifyOperationEvaluation\x12F\n" + + "\x12proposed_operation\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x11proposedOperation\"\x9a\x01\n" + + "\x12ValidateEvaluation\x12F\n" + + "\x12proposed_operation\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x11proposedOperation\x12<\n" + + "\rcurrent_state\x18\x02 \x01(\v2\x17.google.protobuf.StructR\fcurrentState\"^\n" + + "\x14PostCommitEvaluation\x12F\n" + + "\x12committed_response\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x11committedResponse\"\xe2\x02\n" + + "\x11InterceptorResult\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x1f\n" + + "\vstatus_code\x18\x03 \x01(\tR\n" + + "statusCode\x12E\n" + + "\apatches\x18\x04 \x03(\v2+.openshell.gateway_interceptor.v1.JsonPatchR\apatches\x12p\n" + + "\x0flog_annotations\x18\x05 \x03(\v2G.openshell.gateway_interceptor.v1.InterceptorResult.LogAnnotationsEntryR\x0elogAnnotations\x1aA\n" + + "\x13LogAnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc0\x02\n" + + "\x13InterceptorManifest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12P\n" + + "\bbindings\x18\x02 \x03(\v24.openshell.gateway_interceptor.v1.InterceptorBindingR\bbindings\x12%\n" + + "\x0efailure_policy\x18\x03 \x01(\tR\rfailurePolicy\x12+\n" + + "\x11provider_profiles\x18\x04 \x01(\bR\x10providerProfiles\x12+\n" + + "\x11expected_audience\x18\x05 \x01(\tR\x10expectedAudience\x12B\n" + + "\textension\x18\x06 \x01(\v2$.openshell.extension.v1.PeerMetadataR\textension\"p\n" + + "\x17ProviderProfileSnapshot\x12\x1a\n" + + "\brevision\x18\x01 \x01(\tR\brevision\x129\n" + + "\bprofiles\x18\x02 \x03(\v2\x1d.openshell.v1.ProviderProfileR\bprofiles\"\xf1\x01\n" + + "\x12InterceptorBinding\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12Q\n" + + "\bselector\x18\x02 \x01(\v25.openshell.gateway_interceptor.v1.InterceptorSelectorR\bselector\x12Q\n" + + "\x06phases\x18\x03 \x03(\x0e29.openshell.gateway_interceptor.v1.GatewayInterceptorPhaseR\x06phases\x12%\n" + + "\x0efailure_policy\x18\x04 \x01(\tR\rfailurePolicy\"Y\n" + + "\x13InterceptorSelector\x12\x10\n" + + "\x03rpc\x18\x01 \x01(\tR\x03rpc\x12\x18\n" + + "\aservice\x18\x02 \x01(\tR\aservice\x12\x16\n" + + "\x06method\x18\x03 \x01(\tR\x06method\"q\n" + + "\tJsonPatch\x12\x0e\n" + + "\x02op\x18\x01 \x01(\tR\x02op\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12,\n" + + "\x05value\x18\x03 \x01(\v2\x16.google.protobuf.ValueR\x05value\x12\x12\n" + + "\x04from\x18\x04 \x01(\tR\x04from*\xc7\x01\n" + + "\x17GatewayInterceptorPhase\x12)\n" + + "%GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED\x10\x00\x12.\n" + + "*GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION\x10\x02\x12&\n" + + "\"GATEWAY_INTERCEPTOR_PHASE_VALIDATE\x10\x03\x12)\n" + + "%GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT\x10\x042\x9e\x03\n" + + "\x12GatewayInterceptor\x12t\n" + + "\bDescribe\x121.openshell.gateway_interceptor.v1.DescribeRequest\x1a5.openshell.gateway_interceptor.v1.InterceptorManifest\x12\x97\x01\n" + + "\x18SnapshotProviderProfiles\x12@.openshell.gateway_interceptor.v1.ProviderProfileSnapshotRequest\x1a9.openshell.gateway_interceptor.v1.ProviderProfileSnapshot\x12x\n" + + "\bEvaluate\x127.openshell.gateway_interceptor.v1.InterceptorEvaluation\x1a3.openshell.gateway_interceptor.v1.InterceptorResultB\xb2\x02\n" + + "$com.openshell.gateway_interceptor.v1B\x17GatewayInterceptorProtoP\x01ZSgithub.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1;interceptorv1\xa2\x02\x03OGX\xaa\x02\x1fOpenshell.GatewayInterceptor.V1\xca\x02\x1fOpenshell\\GatewayInterceptor\\V1\xe2\x02+Openshell\\GatewayInterceptor\\V1\\GPBMetadata\xea\x02!Openshell::GatewayInterceptor::V1b\x06proto3" + +var ( + file_gateway_interceptor_proto_rawDescOnce sync.Once + file_gateway_interceptor_proto_rawDescData []byte +) + +func file_gateway_interceptor_proto_rawDescGZIP() []byte { + file_gateway_interceptor_proto_rawDescOnce.Do(func() { + file_gateway_interceptor_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gateway_interceptor_proto_rawDesc), len(file_gateway_interceptor_proto_rawDesc))) + }) + return file_gateway_interceptor_proto_rawDescData +} + +var file_gateway_interceptor_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_gateway_interceptor_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_gateway_interceptor_proto_goTypes = []any{ + (GatewayInterceptorPhase)(0), // 0: openshell.gateway_interceptor.v1.GatewayInterceptorPhase + (*DescribeRequest)(nil), // 1: openshell.gateway_interceptor.v1.DescribeRequest + (*ProviderProfileSnapshotRequest)(nil), // 2: openshell.gateway_interceptor.v1.ProviderProfileSnapshotRequest + (*InterceptorEvaluation)(nil), // 3: openshell.gateway_interceptor.v1.InterceptorEvaluation + (*ModifyOperationEvaluation)(nil), // 4: openshell.gateway_interceptor.v1.ModifyOperationEvaluation + (*ValidateEvaluation)(nil), // 5: openshell.gateway_interceptor.v1.ValidateEvaluation + (*PostCommitEvaluation)(nil), // 6: openshell.gateway_interceptor.v1.PostCommitEvaluation + (*InterceptorResult)(nil), // 7: openshell.gateway_interceptor.v1.InterceptorResult + (*InterceptorManifest)(nil), // 8: openshell.gateway_interceptor.v1.InterceptorManifest + (*ProviderProfileSnapshot)(nil), // 9: openshell.gateway_interceptor.v1.ProviderProfileSnapshot + (*InterceptorBinding)(nil), // 10: openshell.gateway_interceptor.v1.InterceptorBinding + (*InterceptorSelector)(nil), // 11: openshell.gateway_interceptor.v1.InterceptorSelector + (*JsonPatch)(nil), // 12: openshell.gateway_interceptor.v1.JsonPatch + nil, // 13: openshell.gateway_interceptor.v1.InterceptorEvaluation.PrincipalEntry + nil, // 14: openshell.gateway_interceptor.v1.InterceptorResult.LogAnnotationsEntry + (*extensionv1.PeerMetadata)(nil), // 15: openshell.extension.v1.PeerMetadata + (*structpb.Struct)(nil), // 16: google.protobuf.Struct + (*openshellv1.ProviderProfile)(nil), // 17: openshell.v1.ProviderProfile + (*structpb.Value)(nil), // 18: google.protobuf.Value +} +var file_gateway_interceptor_proto_depIdxs = []int32{ + 15, // 0: openshell.gateway_interceptor.v1.DescribeRequest.gateway:type_name -> openshell.extension.v1.PeerMetadata + 13, // 1: openshell.gateway_interceptor.v1.InterceptorEvaluation.principal:type_name -> openshell.gateway_interceptor.v1.InterceptorEvaluation.PrincipalEntry + 4, // 2: openshell.gateway_interceptor.v1.InterceptorEvaluation.modify_operation:type_name -> openshell.gateway_interceptor.v1.ModifyOperationEvaluation + 5, // 3: openshell.gateway_interceptor.v1.InterceptorEvaluation.validate:type_name -> openshell.gateway_interceptor.v1.ValidateEvaluation + 6, // 4: openshell.gateway_interceptor.v1.InterceptorEvaluation.post_commit:type_name -> openshell.gateway_interceptor.v1.PostCommitEvaluation + 16, // 5: openshell.gateway_interceptor.v1.ModifyOperationEvaluation.proposed_operation:type_name -> google.protobuf.Struct + 16, // 6: openshell.gateway_interceptor.v1.ValidateEvaluation.proposed_operation:type_name -> google.protobuf.Struct + 16, // 7: openshell.gateway_interceptor.v1.ValidateEvaluation.current_state:type_name -> google.protobuf.Struct + 16, // 8: openshell.gateway_interceptor.v1.PostCommitEvaluation.committed_response:type_name -> google.protobuf.Struct + 12, // 9: openshell.gateway_interceptor.v1.InterceptorResult.patches:type_name -> openshell.gateway_interceptor.v1.JsonPatch + 14, // 10: openshell.gateway_interceptor.v1.InterceptorResult.log_annotations:type_name -> openshell.gateway_interceptor.v1.InterceptorResult.LogAnnotationsEntry + 10, // 11: openshell.gateway_interceptor.v1.InterceptorManifest.bindings:type_name -> openshell.gateway_interceptor.v1.InterceptorBinding + 15, // 12: openshell.gateway_interceptor.v1.InterceptorManifest.extension:type_name -> openshell.extension.v1.PeerMetadata + 17, // 13: openshell.gateway_interceptor.v1.ProviderProfileSnapshot.profiles:type_name -> openshell.v1.ProviderProfile + 11, // 14: openshell.gateway_interceptor.v1.InterceptorBinding.selector:type_name -> openshell.gateway_interceptor.v1.InterceptorSelector + 0, // 15: openshell.gateway_interceptor.v1.InterceptorBinding.phases:type_name -> openshell.gateway_interceptor.v1.GatewayInterceptorPhase + 18, // 16: openshell.gateway_interceptor.v1.JsonPatch.value:type_name -> google.protobuf.Value + 1, // 17: openshell.gateway_interceptor.v1.GatewayInterceptor.Describe:input_type -> openshell.gateway_interceptor.v1.DescribeRequest + 2, // 18: openshell.gateway_interceptor.v1.GatewayInterceptor.SnapshotProviderProfiles:input_type -> openshell.gateway_interceptor.v1.ProviderProfileSnapshotRequest + 3, // 19: openshell.gateway_interceptor.v1.GatewayInterceptor.Evaluate:input_type -> openshell.gateway_interceptor.v1.InterceptorEvaluation + 8, // 20: openshell.gateway_interceptor.v1.GatewayInterceptor.Describe:output_type -> openshell.gateway_interceptor.v1.InterceptorManifest + 9, // 21: openshell.gateway_interceptor.v1.GatewayInterceptor.SnapshotProviderProfiles:output_type -> openshell.gateway_interceptor.v1.ProviderProfileSnapshot + 7, // 22: openshell.gateway_interceptor.v1.GatewayInterceptor.Evaluate:output_type -> openshell.gateway_interceptor.v1.InterceptorResult + 20, // [20:23] is the sub-list for method output_type + 17, // [17:20] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name +} + +func init() { file_gateway_interceptor_proto_init() } +func file_gateway_interceptor_proto_init() { + if File_gateway_interceptor_proto != nil { + return + } + file_gateway_interceptor_proto_msgTypes[2].OneofWrappers = []any{ + (*InterceptorEvaluation_ModifyOperation)(nil), + (*InterceptorEvaluation_Validate)(nil), + (*InterceptorEvaluation_PostCommit)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gateway_interceptor_proto_rawDesc), len(file_gateway_interceptor_proto_rawDesc)), + NumEnums: 1, + NumMessages: 14, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gateway_interceptor_proto_goTypes, + DependencyIndexes: file_gateway_interceptor_proto_depIdxs, + EnumInfos: file_gateway_interceptor_proto_enumTypes, + MessageInfos: file_gateway_interceptor_proto_msgTypes, + }.Build() + File_gateway_interceptor_proto = out.File + file_gateway_interceptor_proto_goTypes = nil + file_gateway_interceptor_proto_depIdxs = nil +} diff --git a/gen/openshell/interceptorv1/gateway_interceptor_grpc.pb.go b/gen/openshell/interceptorv1/gateway_interceptor_grpc.pb.go new file mode 100644 index 0000000..ce6ae81 --- /dev/null +++ b/gen/openshell/interceptorv1/gateway_interceptor_grpc.pb.go @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: gateway_interceptor.proto + +package interceptorv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GatewayInterceptor_Describe_FullMethodName = "/openshell.gateway_interceptor.v1.GatewayInterceptor/Describe" + GatewayInterceptor_SnapshotProviderProfiles_FullMethodName = "/openshell.gateway_interceptor.v1.GatewayInterceptor/SnapshotProviderProfiles" + GatewayInterceptor_Evaluate_FullMethodName = "/openshell.gateway_interceptor.v1.GatewayInterceptor/Evaluate" +) + +// GatewayInterceptorClient is the client API for GatewayInterceptor service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// GatewayInterceptor lets an external governance service evaluate gateway +// control-plane operations after OpenShell admission and before or after the +// gateway applies the operation. +type GatewayInterceptorClient interface { + // Describe returns the interceptor manifest and declared bindings. + Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*InterceptorManifest, error) + // SnapshotProviderProfiles returns the interceptor's current provider + // profile snapshot when the manifest advertises provider_profiles = true. + SnapshotProviderProfiles(ctx context.Context, in *ProviderProfileSnapshotRequest, opts ...grpc.CallOption) (*ProviderProfileSnapshot, error) + // Evaluate returns an allow, deny, or mutation decision for one operation + // phase. + Evaluate(ctx context.Context, in *InterceptorEvaluation, opts ...grpc.CallOption) (*InterceptorResult, error) +} + +type gatewayInterceptorClient struct { + cc grpc.ClientConnInterface +} + +func NewGatewayInterceptorClient(cc grpc.ClientConnInterface) GatewayInterceptorClient { + return &gatewayInterceptorClient{cc} +} + +func (c *gatewayInterceptorClient) Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*InterceptorManifest, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(InterceptorManifest) + err := c.cc.Invoke(ctx, GatewayInterceptor_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayInterceptorClient) SnapshotProviderProfiles(ctx context.Context, in *ProviderProfileSnapshotRequest, opts ...grpc.CallOption) (*ProviderProfileSnapshot, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderProfileSnapshot) + err := c.cc.Invoke(ctx, GatewayInterceptor_SnapshotProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayInterceptorClient) Evaluate(ctx context.Context, in *InterceptorEvaluation, opts ...grpc.CallOption) (*InterceptorResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(InterceptorResult) + err := c.cc.Invoke(ctx, GatewayInterceptor_Evaluate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GatewayInterceptorServer is the server API for GatewayInterceptor service. +// All implementations must embed UnimplementedGatewayInterceptorServer +// for forward compatibility. +// +// GatewayInterceptor lets an external governance service evaluate gateway +// control-plane operations after OpenShell admission and before or after the +// gateway applies the operation. +type GatewayInterceptorServer interface { + // Describe returns the interceptor manifest and declared bindings. + Describe(context.Context, *DescribeRequest) (*InterceptorManifest, error) + // SnapshotProviderProfiles returns the interceptor's current provider + // profile snapshot when the manifest advertises provider_profiles = true. + SnapshotProviderProfiles(context.Context, *ProviderProfileSnapshotRequest) (*ProviderProfileSnapshot, error) + // Evaluate returns an allow, deny, or mutation decision for one operation + // phase. + Evaluate(context.Context, *InterceptorEvaluation) (*InterceptorResult, error) + mustEmbedUnimplementedGatewayInterceptorServer() +} + +// UnimplementedGatewayInterceptorServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGatewayInterceptorServer struct{} + +func (UnimplementedGatewayInterceptorServer) Describe(context.Context, *DescribeRequest) (*InterceptorManifest, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} +func (UnimplementedGatewayInterceptorServer) SnapshotProviderProfiles(context.Context, *ProviderProfileSnapshotRequest) (*ProviderProfileSnapshot, error) { + return nil, status.Error(codes.Unimplemented, "method SnapshotProviderProfiles not implemented") +} +func (UnimplementedGatewayInterceptorServer) Evaluate(context.Context, *InterceptorEvaluation) (*InterceptorResult, error) { + return nil, status.Error(codes.Unimplemented, "method Evaluate not implemented") +} +func (UnimplementedGatewayInterceptorServer) mustEmbedUnimplementedGatewayInterceptorServer() {} +func (UnimplementedGatewayInterceptorServer) testEmbeddedByValue() {} + +// UnsafeGatewayInterceptorServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayInterceptorServer will +// result in compilation errors. +type UnsafeGatewayInterceptorServer interface { + mustEmbedUnimplementedGatewayInterceptorServer() +} + +func RegisterGatewayInterceptorServer(s grpc.ServiceRegistrar, srv GatewayInterceptorServer) { + // If the following call panics, it indicates UnimplementedGatewayInterceptorServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GatewayInterceptor_ServiceDesc, srv) +} + +func _GatewayInterceptor_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayInterceptorServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayInterceptor_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayInterceptorServer).Describe(ctx, req.(*DescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayInterceptor_SnapshotProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProviderProfileSnapshotRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayInterceptorServer).SnapshotProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayInterceptor_SnapshotProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayInterceptorServer).SnapshotProviderProfiles(ctx, req.(*ProviderProfileSnapshotRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayInterceptor_Evaluate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InterceptorEvaluation) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayInterceptorServer).Evaluate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayInterceptor_Evaluate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayInterceptorServer).Evaluate(ctx, req.(*InterceptorEvaluation)) + } + return interceptor(ctx, in, info, handler) +} + +// GatewayInterceptor_ServiceDesc is the grpc.ServiceDesc for GatewayInterceptor service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GatewayInterceptor_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "openshell.gateway_interceptor.v1.GatewayInterceptor", + HandlerType: (*GatewayInterceptorServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Describe", + Handler: _GatewayInterceptor_Describe_Handler, + }, + { + MethodName: "SnapshotProviderProfiles", + Handler: _GatewayInterceptor_SnapshotProviderProfiles_Handler, + }, + { + MethodName: "Evaluate", + Handler: _GatewayInterceptor_Evaluate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gateway_interceptor.proto", +} diff --git a/gen/openshell/middlewarev1/supervisor_middleware.pb.go b/gen/openshell/middlewarev1/supervisor_middleware.pb.go new file mode 100644 index 0000000..643c0d3 --- /dev/null +++ b/gen/openshell/middlewarev1/supervisor_middleware.pb.go @@ -0,0 +1,4241 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: supervisor_middleware.proto + +package middlewarev1 + +import ( + extensionv1 "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Controls which response-body units a stage receives. +type HttpResponseBodyMode int32 + +const ( + // Invalid value handled according to on_error. + HttpResponseBodyMode_HTTP_RESPONSE_BODY_MODE_UNSPECIFIED HttpResponseBodyMode = 0 + // Inspect only the response head. + HttpResponseBodyMode_HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY HttpResponseBodyMode = 1 + // Buffer the normalized body as one final unit before committing the head. + // Input and replacement must fit max_payload_bytes. Capacity failures use + // whole_body_over_capacity and follow this stage's on_error. + HttpResponseBodyMode_HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES HttpResponseBodyMode = 2 + // Receive normalized units ending with end_of_stream. Each input is at most + // min(64 KiB, max_payload_bytes), and each replacement must fit + // max_payload_bytes. Each result fully accounts for its input unit; V1 does + // not permit retaining input across units. The full body may exceed the + // limit. STREAM_BYTES has no total response-lifetime deadline. + HttpResponseBodyMode_HTTP_RESPONSE_BODY_MODE_STREAM_BYTES HttpResponseBodyMode = 3 +) + +// Enum value maps for HttpResponseBodyMode. +var ( + HttpResponseBodyMode_name = map[int32]string{ + 0: "HTTP_RESPONSE_BODY_MODE_UNSPECIFIED", + 1: "HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY", + 2: "HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES", + 3: "HTTP_RESPONSE_BODY_MODE_STREAM_BYTES", + } + HttpResponseBodyMode_value = map[string]int32{ + "HTTP_RESPONSE_BODY_MODE_UNSPECIFIED": 0, + "HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY": 1, + "HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES": 2, + "HTTP_RESPONSE_BODY_MODE_STREAM_BYTES": 3, + } +) + +func (x HttpResponseBodyMode) Enum() *HttpResponseBodyMode { + p := new(HttpResponseBodyMode) + *p = x + return p +} + +func (x HttpResponseBodyMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HttpResponseBodyMode) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[0].Descriptor() +} + +func (HttpResponseBodyMode) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[0] +} + +func (x HttpResponseBodyMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HttpResponseBodyMode.Descriptor instead. +func (HttpResponseBodyMode) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{0} +} + +// Stable reason OpenShell ended a middleware stage stream. +type MiddlewareSessionEndReason int32 + +const ( + // Invalid reason. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED MiddlewareSessionEndReason = 0 + // Evaluation completed. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_NORMAL MiddlewareSessionEndReason = 1 + // The sandbox peer disconnected. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT MiddlewareSessionEndReason = 2 + // A policy reload replaced the active middleware chain. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD MiddlewareSessionEndReason = 3 + // A stage denied the operation or blocked the response. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL MiddlewareSessionEndReason = 4 + // A selected stage failed. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE MiddlewareSessionEndReason = 5 + // A proxied or middleware protocol was violated. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR MiddlewareSessionEndReason = 6 + // Evaluation was canceled for another reason. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_CANCELLATION MiddlewareSessionEndReason = 7 + // Upstream rejected or failed before a valid response or upgrade. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE MiddlewareSessionEndReason = 8 + // Network policy denied the operation. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL MiddlewareSessionEndReason = 9 + // The stage successfully declined inspection during preflight. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED MiddlewareSessionEndReason = 10 + // Upstream disconnected after a valid response or upgrade. + MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT MiddlewareSessionEndReason = 11 +) + +// Enum value maps for MiddlewareSessionEndReason. +var ( + MiddlewareSessionEndReason_name = map[int32]string{ + 0: "MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED", + 1: "MIDDLEWARE_SESSION_END_REASON_NORMAL", + 2: "MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT", + 3: "MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD", + 4: "MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL", + 5: "MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE", + 6: "MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR", + 7: "MIDDLEWARE_SESSION_END_REASON_CANCELLATION", + 8: "MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE", + 9: "MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL", + 10: "MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED", + 11: "MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT", + } + MiddlewareSessionEndReason_value = map[string]int32{ + "MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED": 0, + "MIDDLEWARE_SESSION_END_REASON_NORMAL": 1, + "MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT": 2, + "MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD": 3, + "MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL": 4, + "MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE": 5, + "MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR": 6, + "MIDDLEWARE_SESSION_END_REASON_CANCELLATION": 7, + "MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE": 8, + "MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL": 9, + "MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED": 10, + "MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT": 11, + } +) + +func (x MiddlewareSessionEndReason) Enum() *MiddlewareSessionEndReason { + p := new(MiddlewareSessionEndReason) + *p = x + return p +} + +func (x MiddlewareSessionEndReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MiddlewareSessionEndReason) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[1].Descriptor() +} + +func (MiddlewareSessionEndReason) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[1] +} + +func (x MiddlewareSessionEndReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MiddlewareSessionEndReason.Descriptor instead. +func (MiddlewareSessionEndReason) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{1} +} + +// Supervisor operation selected for middleware evaluation. +type SupervisorMiddlewareOperation int32 + +const ( + SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED SupervisorMiddlewareOperation = 0 + SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST SupervisorMiddlewareOperation = 1 + SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE SupervisorMiddlewareOperation = 2 + SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE SupervisorMiddlewareOperation = 3 +) + +// Enum value maps for SupervisorMiddlewareOperation. +var ( + SupervisorMiddlewareOperation_name = map[int32]string{ + 0: "SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED", + 1: "SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST", + 2: "SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE", + 3: "SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE", + } + SupervisorMiddlewareOperation_value = map[string]int32{ + "SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED": 0, + "SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST": 1, + "SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE": 2, + "SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE": 3, + } +) + +func (x SupervisorMiddlewareOperation) Enum() *SupervisorMiddlewareOperation { + p := new(SupervisorMiddlewareOperation) + *p = x + return p +} + +func (x SupervisorMiddlewareOperation) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SupervisorMiddlewareOperation) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[2].Descriptor() +} + +func (SupervisorMiddlewareOperation) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[2] +} + +func (x SupervisorMiddlewareOperation) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SupervisorMiddlewareOperation.Descriptor instead. +func (SupervisorMiddlewareOperation) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{2} +} + +// Ordered phase within a supervisor operation. +type SupervisorMiddlewarePhase int32 + +const ( + SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED SupervisorMiddlewarePhase = 0 + SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS SupervisorMiddlewarePhase = 1 + SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN SupervisorMiddlewarePhase = 2 +) + +// Enum value maps for SupervisorMiddlewarePhase. +var ( + SupervisorMiddlewarePhase_name = map[int32]string{ + 0: "SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED", + 1: "SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS", + 2: "SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN", + } + SupervisorMiddlewarePhase_value = map[string]int32{ + "SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED": 0, + "SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS": 1, + "SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN": 2, + } +) + +func (x SupervisorMiddlewarePhase) Enum() *SupervisorMiddlewarePhase { + p := new(SupervisorMiddlewarePhase) + *p = x + return p +} + +func (x SupervisorMiddlewarePhase) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SupervisorMiddlewarePhase) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[3].Descriptor() +} + +func (SupervisorMiddlewarePhase) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[3] +} + +func (x SupervisorMiddlewarePhase) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SupervisorMiddlewarePhase.Descriptor instead. +func (SupervisorMiddlewarePhase) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{3} +} + +// WebSocketPreflightAction is the service's one-time scoping decision. +type WebSocketPreflightAction int32 + +const ( + // Invalid response value handled according to the policy failure mode. + WebSocketPreflightAction_WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED WebSocketPreflightAction = 0 + // Inspect this session after the upstream accepts the upgrade. + WebSocketPreflightAction_WEB_SOCKET_PREFLIGHT_ACTION_INSPECT WebSocketPreflightAction = 1 + // Voluntarily decline inspection without denying the upgrade. This is a + // successful decision and does not engage on_error. + WebSocketPreflightAction_WEB_SOCKET_PREFLIGHT_ACTION_SKIP WebSocketPreflightAction = 2 + // Authoritatively deny the upgrade before upstream contact. This is a + // successful decision and is enforced regardless of on_error. + WebSocketPreflightAction_WEB_SOCKET_PREFLIGHT_ACTION_DENY WebSocketPreflightAction = 3 +) + +// Enum value maps for WebSocketPreflightAction. +var ( + WebSocketPreflightAction_name = map[int32]string{ + 0: "WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED", + 1: "WEB_SOCKET_PREFLIGHT_ACTION_INSPECT", + 2: "WEB_SOCKET_PREFLIGHT_ACTION_SKIP", + 3: "WEB_SOCKET_PREFLIGHT_ACTION_DENY", + } + WebSocketPreflightAction_value = map[string]int32{ + "WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED": 0, + "WEB_SOCKET_PREFLIGHT_ACTION_INSPECT": 1, + "WEB_SOCKET_PREFLIGHT_ACTION_SKIP": 2, + "WEB_SOCKET_PREFLIGHT_ACTION_DENY": 3, + } +) + +func (x WebSocketPreflightAction) Enum() *WebSocketPreflightAction { + p := new(WebSocketPreflightAction) + *p = x + return p +} + +func (x WebSocketPreflightAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WebSocketPreflightAction) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[4].Descriptor() +} + +func (WebSocketPreflightAction) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[4] +} + +func (x WebSocketPreflightAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WebSocketPreflightAction.Descriptor instead. +func (WebSocketPreflightAction) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{4} +} + +// Decision controls whether OpenShell continues processing the current +// evaluation unit. +type Decision int32 + +const ( + // Invalid response value handled according to the policy failure mode. + Decision_DECISION_UNSPECIFIED Decision = 0 + // Continue processing the current request or message and apply any returned + // mutations. + Decision_DECISION_ALLOW Decision = 1 + // Reject the current request or message. The operation-specific result + // defines the enclosing protocol behavior. + Decision_DECISION_DENY Decision = 2 +) + +// Enum value maps for Decision. +var ( + Decision_name = map[int32]string{ + 0: "DECISION_UNSPECIFIED", + 1: "DECISION_ALLOW", + 2: "DECISION_DENY", + } + Decision_value = map[string]int32{ + "DECISION_UNSPECIFIED": 0, + "DECISION_ALLOW": 1, + "DECISION_DENY": 2, + } +) + +func (x Decision) Enum() *Decision { + p := new(Decision) + *p = x + return p +} + +func (x Decision) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Decision) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[5].Descriptor() +} + +func (Decision) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[5] +} + +func (x Decision) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Decision.Descriptor instead. +func (Decision) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{5} +} + +// ExistingHeaderAction controls how a header write behaves when the +// case-insensitive header name is already present. Every action writes the +// value when the header is absent. +type ExistingHeaderAction int32 + +const ( + ExistingHeaderAction_EXISTING_HEADER_ACTION_UNSPECIFIED ExistingHeaderAction = 0 + // Add another field value without changing existing values. + ExistingHeaderAction_EXISTING_HEADER_ACTION_APPEND ExistingHeaderAction = 1 + // Remove every existing value, then add the new value. + ExistingHeaderAction_EXISTING_HEADER_ACTION_OVERWRITE ExistingHeaderAction = 2 + // Leave the existing values unchanged. + ExistingHeaderAction_EXISTING_HEADER_ACTION_SKIP ExistingHeaderAction = 3 +) + +// Enum value maps for ExistingHeaderAction. +var ( + ExistingHeaderAction_name = map[int32]string{ + 0: "EXISTING_HEADER_ACTION_UNSPECIFIED", + 1: "EXISTING_HEADER_ACTION_APPEND", + 2: "EXISTING_HEADER_ACTION_OVERWRITE", + 3: "EXISTING_HEADER_ACTION_SKIP", + } + ExistingHeaderAction_value = map[string]int32{ + "EXISTING_HEADER_ACTION_UNSPECIFIED": 0, + "EXISTING_HEADER_ACTION_APPEND": 1, + "EXISTING_HEADER_ACTION_OVERWRITE": 2, + "EXISTING_HEADER_ACTION_SKIP": 3, + } +) + +func (x ExistingHeaderAction) Enum() *ExistingHeaderAction { + p := new(ExistingHeaderAction) + *p = x + return p +} + +func (x ExistingHeaderAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExistingHeaderAction) Descriptor() protoreflect.EnumDescriptor { + return file_supervisor_middleware_proto_enumTypes[6].Descriptor() +} + +func (ExistingHeaderAction) Type() protoreflect.EnumType { + return &file_supervisor_middleware_proto_enumTypes[6] +} + +func (x ExistingHeaderAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExistingHeaderAction.Descriptor instead. +func (ExistingHeaderAction) EnumDescriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{6} +} + +type MiddlewareDescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway or supervisor protocol metadata. Middleware must reject unmet + // requirements before accepting traffic. + Gateway *extensionv1.PeerMetadata `protobuf:"bytes,1,opt,name=gateway,proto3" json:"gateway,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareDescribeRequest) Reset() { + *x = MiddlewareDescribeRequest{} + mi := &file_supervisor_middleware_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareDescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareDescribeRequest) ProtoMessage() {} + +func (x *MiddlewareDescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[0] + 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 MiddlewareDescribeRequest.ProtoReflect.Descriptor instead. +func (*MiddlewareDescribeRequest) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{0} +} + +func (x *MiddlewareDescribeRequest) GetGateway() *extensionv1.PeerMetadata { + if x != nil { + return x.Gateway + } + return nil +} + +// MiddlewareManifest describes one middleware service and its bindings. +type MiddlewareManifest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable middleware service name used only for diagnostics. This is + // not required to match an operator-owned registration name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Deprecated diagnostic compatibility field. Use + // extension.implementation_version. + ServiceVersion string `protobuf:"bytes,2,opt,name=service_version,json=serviceVersion,proto3" json:"service_version,omitempty"` + // Bindings exposed by this middleware service. + Bindings []*MiddlewareBinding `protobuf:"bytes,3,rep,name=bindings,proto3" json:"bindings,omitempty"` + // Exact JWT audience this service verifies on inbound OpenShell calls. + // After authenticated Describe succeeds, OpenShell rejects the registration + // unless this matches the operator-configured audience. A strict verifier may + // reject an incorrect audience before returning this manifest. Empty skips + // this post-authentication consistency check. + ExpectedAudience string `protobuf:"bytes,4,opt,name=expected_audience,json=expectedAudience,proto3" json:"expected_audience,omitempty"` + // Supervisor-middleware protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.supervisor-middleware.contract" + // in supported_capabilities. + Extension *extensionv1.PeerMetadata `protobuf:"bytes,5,opt,name=extension,proto3" json:"extension,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareManifest) Reset() { + *x = MiddlewareManifest{} + mi := &file_supervisor_middleware_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareManifest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareManifest) ProtoMessage() {} + +func (x *MiddlewareManifest) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[1] + 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 MiddlewareManifest.ProtoReflect.Descriptor instead. +func (*MiddlewareManifest) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{1} +} + +func (x *MiddlewareManifest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MiddlewareManifest) GetServiceVersion() string { + if x != nil { + return x.ServiceVersion + } + return "" +} + +func (x *MiddlewareManifest) GetBindings() []*MiddlewareBinding { + if x != nil { + return x.Bindings + } + return nil +} + +func (x *MiddlewareManifest) GetExpectedAudience() string { + if x != nil { + return x.ExpectedAudience + } + return "" +} + +func (x *MiddlewareManifest) GetExtension() *extensionv1.PeerMetadata { + if x != nil { + return x.Extension + } + return nil +} + +// MiddlewareBinding declares one operation and phase supported by a service. +type MiddlewareBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Supported operation. + Operation SupervisorMiddlewareOperation `protobuf:"varint,1,opt,name=operation,proto3,enum=openshell.middleware.v1.SupervisorMiddlewareOperation" json:"operation,omitempty"` + // Supported phase. + Phase SupervisorMiddlewarePhase `protobuf:"varint,2,opt,name=phase,proto3,enum=openshell.middleware.v1.SupervisorMiddlewarePhase" json:"phase,omitempty"` + // Maximum request body, WebSocket message, or response body unit/replacement. + // Required for payload-bearing operations. + MaxPayloadBytes uint64 `protobuf:"varint,3,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` + // Optional binding-specific RPC timeout. Empty uses the operator-configured + // service timeout, or the 500ms platform default when that is also omitted. + // A non-empty value may shorten but cannot extend the operator timeout. + // Values must be between 10ms and 30s. + RequestTimeout *durationpb.Duration `protobuf:"bytes,104,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareBinding) Reset() { + *x = MiddlewareBinding{} + mi := &file_supervisor_middleware_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareBinding) ProtoMessage() {} + +func (x *MiddlewareBinding) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[2] + 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 MiddlewareBinding.ProtoReflect.Descriptor instead. +func (*MiddlewareBinding) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{2} +} + +func (x *MiddlewareBinding) GetOperation() SupervisorMiddlewareOperation { + if x != nil { + return x.Operation + } + return SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED +} + +func (x *MiddlewareBinding) GetPhase() SupervisorMiddlewarePhase { + if x != nil { + return x.Phase + } + return SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED +} + +func (x *MiddlewareBinding) GetMaxPayloadBytes() uint64 { + if x != nil { + return x.MaxPayloadBytes + } + return 0 +} + +func (x *MiddlewareBinding) GetRequestTimeout() *durationpb.Duration { + if x != nil { + return x.RequestTimeout + } + return nil +} + +// ValidateConfigRequest contains one policy configuration to validate. +type ValidateConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Service-specific policy configuration. + Config *structpb.Struct `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + // Built-in middleware name or operator-owned registration name. + MiddlewareName string `protobuf:"bytes,2,opt,name=middleware_name,json=middlewareName,proto3" json:"middleware_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateConfigRequest) Reset() { + *x = ValidateConfigRequest{} + mi := &file_supervisor_middleware_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateConfigRequest) ProtoMessage() {} + +func (x *ValidateConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[3] + 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 ValidateConfigRequest.ProtoReflect.Descriptor instead. +func (*ValidateConfigRequest) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidateConfigRequest) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +func (x *ValidateConfigRequest) GetMiddlewareName() string { + if x != nil { + return x.MiddlewareName + } + return "" +} + +// ValidateConfigResponse reports whether a policy configuration is accepted. +type ValidateConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // True when the service accepts the configuration. + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + // Human-readable validation failure reason. Empty when valid is true. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateConfigResponse) Reset() { + *x = ValidateConfigResponse{} + mi := &file_supervisor_middleware_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateConfigResponse) ProtoMessage() {} + +func (x *ValidateConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[4] + 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 ValidateConfigResponse.ProtoReflect.Descriptor instead. +func (*ValidateConfigResponse) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{4} +} + +func (x *ValidateConfigResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *ValidateConfigResponse) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// HttpRequestEvaluation contains one buffered HTTP request to evaluate. +type HttpRequestEvaluation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Evaluation phase selected for this request. + Phase SupervisorMiddlewarePhase `protobuf:"varint,1,opt,name=phase,proto3,enum=openshell.middleware.v1.SupervisorMiddlewarePhase" json:"phase,omitempty"` + // Sandbox and request identity available to the supervisor. + // The encoded context is limited to 4 KiB. + Context *RequestContext `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"` + // Validated service-specific policy configuration. + // The encoded configuration is limited to 64 KiB. + Config *structpb.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + // Destination and HTTP request target. + // The encoded target is limited to 32 KiB. + Target *HttpRequestTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + // HTTP request headers before OpenShell injects credentials, in wire + // order. Repeated header names are preserved as separate entries. Protected + // credential, routing, framing, and hop-by-hop headers are omitted. + // At most 128 lines and 64 KiB of encoded headers are included. + Headers []*HttpHeader `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty"` + // Buffered request body, limited to 4 MiB. Empty for a bodyless request. + Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"` + // Built-in middleware name or operator-owned registration name. + MiddlewareName string `protobuf:"bytes,7,opt,name=middleware_name,json=middlewareName,proto3" json:"middleware_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpRequestEvaluation) Reset() { + *x = HttpRequestEvaluation{} + mi := &file_supervisor_middleware_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpRequestEvaluation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpRequestEvaluation) ProtoMessage() {} + +func (x *HttpRequestEvaluation) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[5] + 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 HttpRequestEvaluation.ProtoReflect.Descriptor instead. +func (*HttpRequestEvaluation) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{5} +} + +func (x *HttpRequestEvaluation) GetPhase() SupervisorMiddlewarePhase { + if x != nil { + return x.Phase + } + return SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED +} + +func (x *HttpRequestEvaluation) GetContext() *RequestContext { + if x != nil { + return x.Context + } + return nil +} + +func (x *HttpRequestEvaluation) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +func (x *HttpRequestEvaluation) GetTarget() *HttpRequestTarget { + if x != nil { + return x.Target + } + return nil +} + +func (x *HttpRequestEvaluation) GetHeaders() []*HttpHeader { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HttpRequestEvaluation) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *HttpRequestEvaluation) GetMiddlewareName() string { + if x != nil { + return x.MiddlewareName + } + return "" +} + +// HttpHeader is one HTTP header line. +type HttpHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Lowercased header name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Header value with surrounding whitespace trimmed. + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpHeader) Reset() { + *x = HttpHeader{} + mi := &file_supervisor_middleware_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpHeader) ProtoMessage() {} + +func (x *HttpHeader) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[6] + 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 HttpHeader.ProtoReflect.Descriptor instead. +func (*HttpHeader) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{6} +} + +func (x *HttpHeader) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HttpHeader) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +// One ordered response event. A stream starts with preflight, may continue with +// body units ending in end_of_stream, may then include trailers, and may end +// with one best-effort session_end. +type HttpResponseEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *HttpResponseEvent_Preflight + // *HttpResponseEvent_Body + // *HttpResponseEvent_Trailers + // *HttpResponseEvent_SessionEnd + Event isHttpResponseEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseEvent) Reset() { + *x = HttpResponseEvent{} + mi := &file_supervisor_middleware_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseEvent) ProtoMessage() {} + +func (x *HttpResponseEvent) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[7] + 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 HttpResponseEvent.ProtoReflect.Descriptor instead. +func (*HttpResponseEvent) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{7} +} + +func (x *HttpResponseEvent) GetEvent() isHttpResponseEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *HttpResponseEvent) GetPreflight() *HttpResponsePreflight { + if x != nil { + if x, ok := x.Event.(*HttpResponseEvent_Preflight); ok { + return x.Preflight + } + } + return nil +} + +func (x *HttpResponseEvent) GetBody() *HttpResponseBodyUnit { + if x != nil { + if x, ok := x.Event.(*HttpResponseEvent_Body); ok { + return x.Body + } + } + return nil +} + +func (x *HttpResponseEvent) GetTrailers() *HttpResponseTrailers { + if x != nil { + if x, ok := x.Event.(*HttpResponseEvent_Trailers); ok { + return x.Trailers + } + } + return nil +} + +func (x *HttpResponseEvent) GetSessionEnd() *MiddlewareSessionEnd { + if x != nil { + if x, ok := x.Event.(*HttpResponseEvent_SessionEnd); ok { + return x.SessionEnd + } + } + return nil +} + +type isHttpResponseEvent_Event interface { + isHttpResponseEvent_Event() +} + +type HttpResponseEvent_Preflight struct { + // Initial response head and request context. + Preflight *HttpResponsePreflight `protobuf:"bytes,1,opt,name=preflight,proto3,oneof"` +} + +type HttpResponseEvent_Body struct { + // Next normalized body unit. + Body *HttpResponseBodyUnit `protobuf:"bytes,2,opt,name=body,proto3,oneof"` +} + +type HttpResponseEvent_Trailers struct { + // Normalized trailers after the final body result. + Trailers *HttpResponseTrailers `protobuf:"bytes,4,opt,name=trailers,proto3,oneof"` +} + +type HttpResponseEvent_SessionEnd struct { + // Optional terminal notification. + SessionEnd *MiddlewareSessionEnd `protobuf:"bytes,3,opt,name=session_end,json=sessionEnd,proto3,oneof"` +} + +func (*HttpResponseEvent_Preflight) isHttpResponseEvent_Event() {} + +func (*HttpResponseEvent_Body) isHttpResponseEvent_Event() {} + +func (*HttpResponseEvent_Trailers) isHttpResponseEvent_Event() {} + +func (*HttpResponseEvent_SessionEnd) isHttpResponseEvent_Event() {} + +// Each preflight, body, and trailers event requires one ordered result. +// session_end has no result. +type HttpResponseEventResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *HttpResponseEventResult_PreflightResult + // *HttpResponseEventResult_BodyResult + // *HttpResponseEventResult_TrailersResult + Result isHttpResponseEventResult_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseEventResult) Reset() { + *x = HttpResponseEventResult{} + mi := &file_supervisor_middleware_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseEventResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseEventResult) ProtoMessage() {} + +func (x *HttpResponseEventResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[8] + 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 HttpResponseEventResult.ProtoReflect.Descriptor instead. +func (*HttpResponseEventResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{8} +} + +func (x *HttpResponseEventResult) GetResult() isHttpResponseEventResult_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *HttpResponseEventResult) GetPreflightResult() *HttpResponsePreflightResult { + if x != nil { + if x, ok := x.Result.(*HttpResponseEventResult_PreflightResult); ok { + return x.PreflightResult + } + } + return nil +} + +func (x *HttpResponseEventResult) GetBodyResult() *HttpResponseBodyResult { + if x != nil { + if x, ok := x.Result.(*HttpResponseEventResult_BodyResult); ok { + return x.BodyResult + } + } + return nil +} + +func (x *HttpResponseEventResult) GetTrailersResult() *HttpResponseTrailersResult { + if x != nil { + if x, ok := x.Result.(*HttpResponseEventResult_TrailersResult); ok { + return x.TrailersResult + } + } + return nil +} + +type isHttpResponseEventResult_Result interface { + isHttpResponseEventResult_Result() +} + +type HttpResponseEventResult_PreflightResult struct { + // Result for preflight. + PreflightResult *HttpResponsePreflightResult `protobuf:"bytes,1,opt,name=preflight_result,json=preflightResult,proto3,oneof"` +} + +type HttpResponseEventResult_BodyResult struct { + // Result for the next body unit. + BodyResult *HttpResponseBodyResult `protobuf:"bytes,2,opt,name=body_result,json=bodyResult,proto3,oneof"` +} + +type HttpResponseEventResult_TrailersResult struct { + // Result for response trailers. + TrailersResult *HttpResponseTrailersResult `protobuf:"bytes,3,opt,name=trailers_result,json=trailersResult,proto3,oneof"` +} + +func (*HttpResponseEventResult_PreflightResult) isHttpResponseEventResult_Result() {} + +func (*HttpResponseEventResult_BodyResult) isHttpResponseEventResult_Result() {} + +func (*HttpResponseEventResult_TrailersResult) isHttpResponseEventResult_Result() {} + +// HttpResponsePreflight exposes the current final response head to one stage. +type HttpResponsePreflight struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Request identity. request_id links request and response evaluations. + // Limited to 4 KiB encoded. + Context *RequestContext `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` + // Admitted request target with a redacted query. Limited to 32 KiB encoded. + Target *HttpRequestTarget `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + // Final non-informational upstream status. Upgrades are not evaluated. + StatusCode uint32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + // Response headers after prior stages, in wire order. Repeated names remain + // separate. Credential, routing, and hop-by-hop headers are omitted. + // Content-Length, Content-Encoding, and Content-Range retain their read-only + // upstream values. OpenShell may recompute or remove Content-Length later. + // Limited to 128 lines and 64 KiB encoded. + Headers []*HttpHeader `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"` + // Built-in middleware name or operator-owned registration name. + MiddlewareName string `protobuf:"bytes,5,opt,name=middleware_name,json=middlewareName,proto3" json:"middleware_name,omitempty"` + // Validated service configuration. Limited to 64 KiB encoded. + Config *structpb.Struct `protobuf:"bytes,6,opt,name=config,proto3" json:"config,omitempty"` + // Effective minimum of platform, registration, and binding limits. Applies to + // whole-body input/replacement and each stream input/replacement. Stream + // inputs use at most min(64 KiB, max_payload_bytes). + MaxPayloadBytes uint64 `protobuf:"varint,7,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` + // Modes derived independently for this stage. OpenShell first determines + // response-shape eligibility from the original final response head, then + // applies this stage's effective max_payload_bytes. Different stages may + // receive different lists. HEADERS_ONLY is always present and is the only + // mode for bodyless, partial, encoded, or no-transform responses. For an + // otherwise eligible response, a known body larger than this stage's limit + // omits WHOLE_BODY_BYTES. An eligible unknown-length response may select + // WHOLE_BODY_BYTES and later fail with whole_body_over_capacity according to + // this stage's on_error. STREAM_BYTES is omitted when + // max_payload_bytes is zero. Selecting an unlisted mode fails according to + // on_error. + PermittedBodyModes []HttpResponseBodyMode `protobuf:"varint,8,rep,packed,name=permitted_body_modes,json=permittedBodyModes,proto3,enum=openshell.middleware.v1.HttpResponseBodyMode" json:"permitted_body_modes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponsePreflight) Reset() { + *x = HttpResponsePreflight{} + mi := &file_supervisor_middleware_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponsePreflight) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponsePreflight) ProtoMessage() {} + +func (x *HttpResponsePreflight) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[9] + 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 HttpResponsePreflight.ProtoReflect.Descriptor instead. +func (*HttpResponsePreflight) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{9} +} + +func (x *HttpResponsePreflight) GetContext() *RequestContext { + if x != nil { + return x.Context + } + return nil +} + +func (x *HttpResponsePreflight) GetTarget() *HttpRequestTarget { + if x != nil { + return x.Target + } + return nil +} + +func (x *HttpResponsePreflight) GetStatusCode() uint32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *HttpResponsePreflight) GetHeaders() []*HttpHeader { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HttpResponsePreflight) GetMiddlewareName() string { + if x != nil { + return x.MiddlewareName + } + return "" +} + +func (x *HttpResponsePreflight) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +func (x *HttpResponsePreflight) GetMaxPayloadBytes() uint64 { + if x != nil { + return x.MaxPayloadBytes + } + return 0 +} + +func (x *HttpResponsePreflight) GetPermittedBodyModes() []HttpResponseBodyMode { + if x != nil { + return x.PermittedBodyModes + } + return nil +} + +// Selects skip, inspect, or block. Diagnostic fields apply to every action. +// Invalid diagnostics make the entire result a middleware failure handled +// according to on_error. +type HttpResponsePreflightResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Action: + // + // *HttpResponsePreflightResult_Skip + // *HttpResponsePreflightResult_Inspect + // *HttpResponsePreflightResult_BlockDelivery + Action isHttpResponsePreflightResult_Action `protobuf_oneof:"action"` + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. Returned to the sandbox only for block_delivery. + ReasonCode string `protobuf:"bytes,4,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + Findings []*Finding `protobuf:"bytes,5,rep,name=findings,proto3" json:"findings,omitempty"` + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponsePreflightResult) Reset() { + *x = HttpResponsePreflightResult{} + mi := &file_supervisor_middleware_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponsePreflightResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponsePreflightResult) ProtoMessage() {} + +func (x *HttpResponsePreflightResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[10] + 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 HttpResponsePreflightResult.ProtoReflect.Descriptor instead. +func (*HttpResponsePreflightResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{10} +} + +func (x *HttpResponsePreflightResult) GetAction() isHttpResponsePreflightResult_Action { + if x != nil { + return x.Action + } + return nil +} + +func (x *HttpResponsePreflightResult) GetSkip() *HttpResponsePreflightSkip { + if x != nil { + if x, ok := x.Action.(*HttpResponsePreflightResult_Skip); ok { + return x.Skip + } + } + return nil +} + +func (x *HttpResponsePreflightResult) GetInspect() *HttpResponsePreflightInspect { + if x != nil { + if x, ok := x.Action.(*HttpResponsePreflightResult_Inspect); ok { + return x.Inspect + } + } + return nil +} + +func (x *HttpResponsePreflightResult) GetBlockDelivery() *HttpResponseBlockDelivery { + if x != nil { + if x, ok := x.Action.(*HttpResponsePreflightResult_BlockDelivery); ok { + return x.BlockDelivery + } + } + return nil +} + +func (x *HttpResponsePreflightResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *HttpResponsePreflightResult) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *HttpResponsePreflightResult) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *HttpResponsePreflightResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isHttpResponsePreflightResult_Action interface { + isHttpResponsePreflightResult_Action() +} + +type HttpResponsePreflightResult_Skip struct { + // Deliver unchanged without invoking on_error. + Skip *HttpResponsePreflightSkip `protobuf:"bytes,1,opt,name=skip,proto3,oneof"` +} + +type HttpResponsePreflightResult_Inspect struct { + // Inspect with the selected body mode and mutations. + Inspect *HttpResponsePreflightInspect `protobuf:"bytes,2,opt,name=inspect,proto3,oneof"` +} + +type HttpResponsePreflightResult_BlockDelivery struct { + // Prevent delivery to the sandbox. + BlockDelivery *HttpResponseBlockDelivery `protobuf:"bytes,7,opt,name=block_delivery,json=blockDelivery,proto3,oneof"` +} + +func (*HttpResponsePreflightResult_Skip) isHttpResponsePreflightResult_Action() {} + +func (*HttpResponsePreflightResult_Inspect) isHttpResponsePreflightResult_Action() {} + +func (*HttpResponsePreflightResult_BlockDelivery) isHttpResponsePreflightResult_Action() {} + +// Ends this stage successfully without body inspection. +type HttpResponsePreflightSkip struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponsePreflightSkip) Reset() { + *x = HttpResponsePreflightSkip{} + mi := &file_supervisor_middleware_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponsePreflightSkip) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponsePreflightSkip) ProtoMessage() {} + +func (x *HttpResponsePreflightSkip) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[11] + 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 HttpResponsePreflightSkip.ProtoReflect.Descriptor instead. +func (*HttpResponsePreflightSkip) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{11} +} + +// Blocks delivery as a successful decision regardless of on_error. OpenShell +// evaluates results in policy order. Once it accepts a valid block, it stops +// later middleware evaluation and ends every still-writable opened stage with +// MIDDLEWARE_DENIAL. A failure handled earlier may already have stopped +// evaluation, so a later block does not override it. An invalid block result +// is a middleware failure handled according to on_error. The upstream request +// has already run; blocking its response does not reject or roll back that +// request. +// +// Before response commitment, including at preflight and during +// WHOLE_BODY_BYTES, OpenShell replaces the upstream response with the canonical +// 403 Forbidden middleware-denial response. Its JSON body has +// error = "middleware_denied" and includes a validated reason_code when the +// result supplies one. OpenShell never returns the free-form reason or writes it +// to security logs. For HEAD, OpenShell sends the canonical response headers +// and Content-Length but no body. It closes the downstream connection after the +// denial response. +// +// After response commitment, including during STREAM_BYTES, OpenShell aborts +// downstream delivery. It does not inject an error body, a terminating chunk, +// or an error trailer. OpenShell does not reuse the upstream connection. +type HttpResponseBlockDelivery struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBlockDelivery) Reset() { + *x = HttpResponseBlockDelivery{} + mi := &file_supervisor_middleware_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBlockDelivery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBlockDelivery) ProtoMessage() {} + +func (x *HttpResponseBlockDelivery) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[12] + 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 HttpResponseBlockDelivery.ProtoReflect.Descriptor instead. +func (*HttpResponseBlockDelivery) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{12} +} + +// Selects body inspection and response-header mutations. +type HttpResponsePreflightInspect struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required mode from permitted_body_modes. Invalid values fail according to + // on_error. + BodyMode HttpResponseBodyMode `protobuf:"varint,1,opt,name=body_mode,json=bodyMode,proto3,enum=openshell.middleware.v1.HttpResponseBodyMode" json:"body_mode,omitempty"` + // Ordered mutations applied atomically before the next stage. Only visible + // end-to-end headers may change. Routing, credential, framing, coding, range, + // and hop-by-hop headers are protected; integrity headers may only be removed. + // Limited to 64 operations, 32 KiB of name/value data, and 64 KiB encoded. + HeaderMutations []*HeaderMutation `protobuf:"bytes,2,rep,name=header_mutations,json=headerMutations,proto3" json:"header_mutations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponsePreflightInspect) Reset() { + *x = HttpResponsePreflightInspect{} + mi := &file_supervisor_middleware_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponsePreflightInspect) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponsePreflightInspect) ProtoMessage() {} + +func (x *HttpResponsePreflightInspect) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[13] + 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 HttpResponsePreflightInspect.ProtoReflect.Descriptor instead. +func (*HttpResponsePreflightInspect) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{13} +} + +func (x *HttpResponsePreflightInspect) GetBodyMode() HttpResponseBodyMode { + if x != nil { + return x.BodyMode + } + return HttpResponseBodyMode_HTTP_RESPONSE_BODY_MODE_UNSPECIFIED +} + +func (x *HttpResponsePreflightInspect) GetHeaderMutations() []*HeaderMutation { + if x != nil { + return x.HeaderMutations + } + return nil +} + +// One normalized body unit. Boundaries have no transport or application +// meaning. +type HttpResponseBodyUnit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Contiguous and stage-local, starting at 1. + Sequence uint64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + // Types that are valid to be assigned to Payload: + // + // *HttpResponseBodyUnit_Data + Payload isHttpResponseBodyUnit_Payload `protobuf_oneof:"payload"` + // Marks the final body unit. Every normally completed body inspection receives + // exactly one. For a body-capable response with no body bytes, this is the + // empty sequence-1 unit. OpenShell does not read ahead, so it may send an empty + // final unit after the last nonempty unit. Trailers and session_end may + // follow. A stage ended by skip_remaining, block, or failure receives no + // later final unit. + EndOfStream bool `protobuf:"varint,3,opt,name=end_of_stream,json=endOfStream,proto3" json:"end_of_stream,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBodyUnit) Reset() { + *x = HttpResponseBodyUnit{} + mi := &file_supervisor_middleware_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBodyUnit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBodyUnit) ProtoMessage() {} + +func (x *HttpResponseBodyUnit) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[14] + 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 HttpResponseBodyUnit.ProtoReflect.Descriptor instead. +func (*HttpResponseBodyUnit) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{14} +} + +func (x *HttpResponseBodyUnit) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *HttpResponseBodyUnit) GetPayload() isHttpResponseBodyUnit_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *HttpResponseBodyUnit) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*HttpResponseBodyUnit_Data); ok { + return x.Data + } + } + return nil +} + +func (x *HttpResponseBodyUnit) GetEndOfStream() bool { + if x != nil { + return x.EndOfStream + } + return false +} + +type isHttpResponseBodyUnit_Payload interface { + isHttpResponseBodyUnit_Payload() +} + +type HttpResponseBodyUnit_Data struct { + // Bytes without transfer framing. A body-capable response with no body bytes + // has present empty data in sequence 1. STREAM_BYTES input size is at most + // min(64 KiB, max_payload_bytes). A unit may be shorter to preserve + // flushing. + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*HttpResponseBodyUnit_Data) isHttpResponseBodyUnit_Payload() {} + +// Result for one body unit. Units are processed in lockstep; V1 does not +// support ownership transfer or cross-unit retention. Diagnostic fields apply +// to every action. Invalid diagnostics make the entire result a middleware +// failure handled according to on_error. OpenShell retains the current input +// until it validates the result, so fail-open can continue from the last input +// OpenShell still owns. +type HttpResponseBodyResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Must match the next unit. Zero, gaps, duplicates, and regressions fail. + Sequence uint64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + // Exactly one explicit action is required. + // + // Types that are valid to be assigned to Action: + // + // *HttpResponseBodyResult_PassThrough + // *HttpResponseBodyResult_Transform + // *HttpResponseBodyResult_BlockDelivery + // *HttpResponseBodyResult_SkipRemaining + Action isHttpResponseBodyResult_Action `protobuf_oneof:"action"` + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + Reason string `protobuf:"bytes,4,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. When OpenShell accepts block_delivery before response + // commitment, it includes this code in the canonical denial response. It is + // never returned after commitment. + ReasonCode string `protobuf:"bytes,5,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + Findings []*Finding `protobuf:"bytes,6,rep,name=findings,proto3" json:"findings,omitempty"` + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBodyResult) Reset() { + *x = HttpResponseBodyResult{} + mi := &file_supervisor_middleware_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBodyResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBodyResult) ProtoMessage() {} + +func (x *HttpResponseBodyResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[15] + 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 HttpResponseBodyResult.ProtoReflect.Descriptor instead. +func (*HttpResponseBodyResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{15} +} + +func (x *HttpResponseBodyResult) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *HttpResponseBodyResult) GetAction() isHttpResponseBodyResult_Action { + if x != nil { + return x.Action + } + return nil +} + +func (x *HttpResponseBodyResult) GetPassThrough() *HttpResponseBodyPassThrough { + if x != nil { + if x, ok := x.Action.(*HttpResponseBodyResult_PassThrough); ok { + return x.PassThrough + } + } + return nil +} + +func (x *HttpResponseBodyResult) GetTransform() *HttpResponseBodyTransform { + if x != nil { + if x, ok := x.Action.(*HttpResponseBodyResult_Transform); ok { + return x.Transform + } + } + return nil +} + +func (x *HttpResponseBodyResult) GetBlockDelivery() *HttpResponseBlockDelivery { + if x != nil { + if x, ok := x.Action.(*HttpResponseBodyResult_BlockDelivery); ok { + return x.BlockDelivery + } + } + return nil +} + +func (x *HttpResponseBodyResult) GetSkipRemaining() *HttpResponseBodySkipRemaining { + if x != nil { + if x, ok := x.Action.(*HttpResponseBodyResult_SkipRemaining); ok { + return x.SkipRemaining + } + } + return nil +} + +func (x *HttpResponseBodyResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *HttpResponseBodyResult) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *HttpResponseBodyResult) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *HttpResponseBodyResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isHttpResponseBodyResult_Action interface { + isHttpResponseBodyResult_Action() +} + +type HttpResponseBodyResult_PassThrough struct { + // Forward the input unit unchanged. + PassThrough *HttpResponseBodyPassThrough `protobuf:"bytes,2,opt,name=pass_through,json=passThrough,proto3,oneof"` +} + +type HttpResponseBodyResult_Transform struct { + // Replace the complete input unit. + Transform *HttpResponseBodyTransform `protobuf:"bytes,3,opt,name=transform,proto3,oneof"` +} + +type HttpResponseBodyResult_BlockDelivery struct { + // Stop delivery. See HttpResponseBlockDelivery. + BlockDelivery *HttpResponseBlockDelivery `protobuf:"bytes,8,opt,name=block_delivery,json=blockDelivery,proto3,oneof"` +} + +type HttpResponseBodyResult_SkipRemaining struct { + // Finalize this unit and stop inspecting. + SkipRemaining *HttpResponseBodySkipRemaining `protobuf:"bytes,9,opt,name=skip_remaining,json=skipRemaining,proto3,oneof"` +} + +func (*HttpResponseBodyResult_PassThrough) isHttpResponseBodyResult_Action() {} + +func (*HttpResponseBodyResult_Transform) isHttpResponseBodyResult_Action() {} + +func (*HttpResponseBodyResult_BlockDelivery) isHttpResponseBodyResult_Action() {} + +func (*HttpResponseBodyResult_SkipRemaining) isHttpResponseBodyResult_Action() {} + +// Preserves the input unit. +type HttpResponseBodyPassThrough struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBodyPassThrough) Reset() { + *x = HttpResponseBodyPassThrough{} + mi := &file_supervisor_middleware_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBodyPassThrough) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBodyPassThrough) ProtoMessage() {} + +func (x *HttpResponseBodyPassThrough) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[16] + 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 HttpResponseBodyPassThrough.ProtoReflect.Descriptor instead. +func (*HttpResponseBodyPassThrough) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{16} +} + +// Finalizes this unit and ends the stage. This stage receives no later body or +// trailer events. The current and later units continue through other stages. +// For WHOLE_BODY_BYTES, this equals its nested action. +type HttpResponseBodySkipRemaining struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Exactly one action for the current unit. + // + // Types that are valid to be assigned to Current: + // + // *HttpResponseBodySkipRemaining_PassThrough + // *HttpResponseBodySkipRemaining_Transform + Current isHttpResponseBodySkipRemaining_Current `protobuf_oneof:"current"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBodySkipRemaining) Reset() { + *x = HttpResponseBodySkipRemaining{} + mi := &file_supervisor_middleware_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBodySkipRemaining) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBodySkipRemaining) ProtoMessage() {} + +func (x *HttpResponseBodySkipRemaining) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[17] + 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 HttpResponseBodySkipRemaining.ProtoReflect.Descriptor instead. +func (*HttpResponseBodySkipRemaining) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{17} +} + +func (x *HttpResponseBodySkipRemaining) GetCurrent() isHttpResponseBodySkipRemaining_Current { + if x != nil { + return x.Current + } + return nil +} + +func (x *HttpResponseBodySkipRemaining) GetPassThrough() *HttpResponseBodyPassThrough { + if x != nil { + if x, ok := x.Current.(*HttpResponseBodySkipRemaining_PassThrough); ok { + return x.PassThrough + } + } + return nil +} + +func (x *HttpResponseBodySkipRemaining) GetTransform() *HttpResponseBodyTransform { + if x != nil { + if x, ok := x.Current.(*HttpResponseBodySkipRemaining_Transform); ok { + return x.Transform + } + } + return nil +} + +type isHttpResponseBodySkipRemaining_Current interface { + isHttpResponseBodySkipRemaining_Current() +} + +type HttpResponseBodySkipRemaining_PassThrough struct { + // Forward the current unit unchanged. + PassThrough *HttpResponseBodyPassThrough `protobuf:"bytes,1,opt,name=pass_through,json=passThrough,proto3,oneof"` +} + +type HttpResponseBodySkipRemaining_Transform struct { + // Replace the current unit. + Transform *HttpResponseBodyTransform `protobuf:"bytes,2,opt,name=transform,proto3,oneof"` +} + +func (*HttpResponseBodySkipRemaining_PassThrough) isHttpResponseBodySkipRemaining_Current() {} + +func (*HttpResponseBodySkipRemaining_Transform) isHttpResponseBodySkipRemaining_Current() {} + +// Replaces the complete input unit. +type HttpResponseBodyTransform struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required replacement, limited to max_payload_bytes. Present empty data + // deletes the input unit. The replacement fully accounts for this input unit; + // middleware must not retain input bytes for a later unit in V1. + // + // Types that are valid to be assigned to Replacement: + // + // *HttpResponseBodyTransform_Data + Replacement isHttpResponseBodyTransform_Replacement `protobuf_oneof:"replacement"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseBodyTransform) Reset() { + *x = HttpResponseBodyTransform{} + mi := &file_supervisor_middleware_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseBodyTransform) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseBodyTransform) ProtoMessage() {} + +func (x *HttpResponseBodyTransform) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[18] + 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 HttpResponseBodyTransform.ProtoReflect.Descriptor instead. +func (*HttpResponseBodyTransform) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{18} +} + +func (x *HttpResponseBodyTransform) GetReplacement() isHttpResponseBodyTransform_Replacement { + if x != nil { + return x.Replacement + } + return nil +} + +func (x *HttpResponseBodyTransform) GetData() []byte { + if x != nil { + if x, ok := x.Replacement.(*HttpResponseBodyTransform_Data); ok { + return x.Data + } + } + return nil +} + +type isHttpResponseBodyTransform_Replacement interface { + isHttpResponseBodyTransform_Replacement() +} + +type HttpResponseBodyTransform_Data struct { + // Normalized replacement bytes. + Data []byte `protobuf:"bytes,1,opt,name=data,proto3,oneof"` +} + +func (*HttpResponseBodyTransform_Data) isHttpResponseBodyTransform_Replacement() {} + +// The current normalized response trailers in wire order. Repeated names stay +// as separate fields. A stage that completes WHOLE_BODY_BYTES or STREAM_BYTES +// receives exactly one trailers event after its final body result, including +// when this set is empty. SKIP, HEADERS_ONLY, semantically bodyless responses, +// and stages ended by block, failure, or skip_remaining receive no trailers. +type HttpResponseTrailers struct { + state protoimpl.MessageState `protogen:"open.v1"` + Headers []*HttpHeader `protobuf:"bytes,1,rep,name=headers,proto3" json:"headers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseTrailers) Reset() { + *x = HttpResponseTrailers{} + mi := &file_supervisor_middleware_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseTrailers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseTrailers) ProtoMessage() {} + +func (x *HttpResponseTrailers) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[19] + 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 HttpResponseTrailers.ProtoReflect.Descriptor instead. +func (*HttpResponseTrailers) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{19} +} + +func (x *HttpResponseTrailers) GetHeaders() []*HttpHeader { + if x != nil { + return x.Headers + } + return nil +} + +// Applies ordered trailer mutations atomically. An empty mutation list +// preserves the current trailers. A write may target only a case-insensitive +// name present in the trailers event; V1 cannot create a trailer name. Removal +// of an absent name is a no-op. Credential, routing, framing, coding, range, +// hop-by-hop, and connection-nominated fields are protected. Diagnostic fields +// apply whether mutations are empty or nonempty. Invalid diagnostics or +// mutations make the entire result a middleware failure handled according to +// on_error. +type HttpResponseTrailersResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // At most 64 operations, 32 KiB of validated name/value data, and 64 KiB + // encoded are accepted. + TrailerMutations []*HeaderMutation `protobuf:"bytes,1,rep,name=trailer_mutations,json=trailerMutations,proto3" json:"trailer_mutations,omitempty"` + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. Never sent to the sandbox. + ReasonCode string `protobuf:"bytes,3,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + Findings []*Finding `protobuf:"bytes,4,rep,name=findings,proto3" json:"findings,omitempty"` + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponseTrailersResult) Reset() { + *x = HttpResponseTrailersResult{} + mi := &file_supervisor_middleware_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponseTrailersResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponseTrailersResult) ProtoMessage() {} + +func (x *HttpResponseTrailersResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[20] + 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 HttpResponseTrailersResult.ProtoReflect.Descriptor instead. +func (*HttpResponseTrailersResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{20} +} + +func (x *HttpResponseTrailersResult) GetTrailerMutations() []*HeaderMutation { + if x != nil { + return x.TrailerMutations + } + return nil +} + +func (x *HttpResponseTrailersResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *HttpResponseTrailersResult) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *HttpResponseTrailersResult) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *HttpResponseTrailersResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// Best-effort terminal notification. A stage receives at most one and sends no +// result. +type MiddlewareSessionEnd struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Terminal reason. Producers never send UNSPECIFIED. + Reason MiddlewareSessionEndReason `protobuf:"varint,1,opt,name=reason,proto3,enum=openshell.middleware.v1.MiddlewareSessionEndReason" json:"reason,omitempty"` + // Set only for PROTOCOL_ERROR. Missing or unknown details mean a generic + // protocol error. + ProtocolError *MiddlewareSessionProtocolError `protobuf:"bytes,2,opt,name=protocol_error,json=protocolError,proto3" json:"protocol_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareSessionEnd) Reset() { + *x = MiddlewareSessionEnd{} + mi := &file_supervisor_middleware_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareSessionEnd) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareSessionEnd) ProtoMessage() {} + +func (x *MiddlewareSessionEnd) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[21] + 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 MiddlewareSessionEnd.ProtoReflect.Descriptor instead. +func (*MiddlewareSessionEnd) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{21} +} + +func (x *MiddlewareSessionEnd) GetReason() MiddlewareSessionEndReason { + if x != nil { + return x.Reason + } + return MiddlewareSessionEndReason_MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED +} + +func (x *MiddlewareSessionEnd) GetProtocolError() *MiddlewareSessionProtocolError { + if x != nil { + return x.ProtocolError + } + return nil +} + +// Details for a protocol-error session end. +type MiddlewareSessionProtocolError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Domain: + // + // *MiddlewareSessionProtocolError_WebSocket + // *MiddlewareSessionProtocolError_MiddlewareExchange + Domain isMiddlewareSessionProtocolError_Domain `protobuf_oneof:"domain"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareSessionProtocolError) Reset() { + *x = MiddlewareSessionProtocolError{} + mi := &file_supervisor_middleware_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareSessionProtocolError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareSessionProtocolError) ProtoMessage() {} + +func (x *MiddlewareSessionProtocolError) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MiddlewareSessionProtocolError.ProtoReflect.Descriptor instead. +func (*MiddlewareSessionProtocolError) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{22} +} + +func (x *MiddlewareSessionProtocolError) GetDomain() isMiddlewareSessionProtocolError_Domain { + if x != nil { + return x.Domain + } + return nil +} + +func (x *MiddlewareSessionProtocolError) GetWebSocket() *WebSocketProtocolError { + if x != nil { + if x, ok := x.Domain.(*MiddlewareSessionProtocolError_WebSocket); ok { + return x.WebSocket + } + } + return nil +} + +func (x *MiddlewareSessionProtocolError) GetMiddlewareExchange() *MiddlewareExchangeProtocolError { + if x != nil { + if x, ok := x.Domain.(*MiddlewareSessionProtocolError_MiddlewareExchange); ok { + return x.MiddlewareExchange + } + } + return nil +} + +type isMiddlewareSessionProtocolError_Domain interface { + isMiddlewareSessionProtocolError_Domain() +} + +type MiddlewareSessionProtocolError_WebSocket struct { + // WebSocket protocol violation. + WebSocket *WebSocketProtocolError `protobuf:"bytes,1,opt,name=web_socket,json=webSocket,proto3,oneof"` +} + +type MiddlewareSessionProtocolError_MiddlewareExchange struct { + // Middleware event/result protocol violation. + MiddlewareExchange *MiddlewareExchangeProtocolError `protobuf:"bytes,2,opt,name=middleware_exchange,json=middlewareExchange,proto3,oneof"` +} + +func (*MiddlewareSessionProtocolError_WebSocket) isMiddlewareSessionProtocolError_Domain() {} + +func (*MiddlewareSessionProtocolError_MiddlewareExchange) isMiddlewareSessionProtocolError_Domain() {} + +// WebSocket protocol error details, reserved for future categories. +type WebSocketProtocolError struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketProtocolError) Reset() { + *x = WebSocketProtocolError{} + mi := &file_supervisor_middleware_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketProtocolError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketProtocolError) ProtoMessage() {} + +func (x *WebSocketProtocolError) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketProtocolError.ProtoReflect.Descriptor instead. +func (*WebSocketProtocolError) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{23} +} + +// Middleware exchange error details, reserved for future categories. +type MiddlewareExchangeProtocolError struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareExchangeProtocolError) Reset() { + *x = MiddlewareExchangeProtocolError{} + mi := &file_supervisor_middleware_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareExchangeProtocolError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareExchangeProtocolError) ProtoMessage() {} + +func (x *MiddlewareExchangeProtocolError) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MiddlewareExchangeProtocolError.ProtoReflect.Descriptor instead. +func (*MiddlewareExchangeProtocolError) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{24} +} + +// WebSocketSessionEvent is one ordered event in a stage-local stream. +// Message sequence numbers identify logical messages session-wide. A stage +// receives a strictly increasing subset of those numbers; gaps are valid when +// session messages are not delivered to that stage. +type WebSocketSessionEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *WebSocketSessionEvent_Preflight + // *WebSocketSessionEvent_SessionStart + // *WebSocketSessionEvent_Message + // *WebSocketSessionEvent_SessionEnd + Event isWebSocketSessionEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketSessionEvent) Reset() { + *x = WebSocketSessionEvent{} + mi := &file_supervisor_middleware_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketSessionEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketSessionEvent) ProtoMessage() {} + +func (x *WebSocketSessionEvent) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketSessionEvent.ProtoReflect.Descriptor instead. +func (*WebSocketSessionEvent) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{25} +} + +func (x *WebSocketSessionEvent) GetEvent() isWebSocketSessionEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *WebSocketSessionEvent) GetPreflight() *WebSocketPreflight { + if x != nil { + if x, ok := x.Event.(*WebSocketSessionEvent_Preflight); ok { + return x.Preflight + } + } + return nil +} + +func (x *WebSocketSessionEvent) GetSessionStart() *WebSocketSessionStart { + if x != nil { + if x, ok := x.Event.(*WebSocketSessionEvent_SessionStart); ok { + return x.SessionStart + } + } + return nil +} + +func (x *WebSocketSessionEvent) GetMessage() *WebSocketMessage { + if x != nil { + if x, ok := x.Event.(*WebSocketSessionEvent_Message); ok { + return x.Message + } + } + return nil +} + +func (x *WebSocketSessionEvent) GetSessionEnd() *MiddlewareSessionEnd { + if x != nil { + if x, ok := x.Event.(*WebSocketSessionEvent_SessionEnd); ok { + return x.SessionEnd + } + } + return nil +} + +type isWebSocketSessionEvent_Event interface { + isWebSocketSessionEvent_Event() +} + +type WebSocketSessionEvent_Preflight struct { + Preflight *WebSocketPreflight `protobuf:"bytes,1,opt,name=preflight,proto3,oneof"` +} + +type WebSocketSessionEvent_SessionStart struct { + SessionStart *WebSocketSessionStart `protobuf:"bytes,2,opt,name=session_start,json=sessionStart,proto3,oneof"` +} + +type WebSocketSessionEvent_Message struct { + Message *WebSocketMessage `protobuf:"bytes,3,opt,name=message,proto3,oneof"` +} + +type WebSocketSessionEvent_SessionEnd struct { + SessionEnd *MiddlewareSessionEnd `protobuf:"bytes,4,opt,name=session_end,json=sessionEnd,proto3,oneof"` +} + +func (*WebSocketSessionEvent_Preflight) isWebSocketSessionEvent_Event() {} + +func (*WebSocketSessionEvent_SessionStart) isWebSocketSessionEvent_Event() {} + +func (*WebSocketSessionEvent_Message) isWebSocketSessionEvent_Event() {} + +func (*WebSocketSessionEvent_SessionEnd) isWebSocketSessionEvent_Event() {} + +// WebSocketPreflight lets a service decline this upgrade before OpenShell +// contacts upstream. It deliberately excludes query data, arbitrary request +// headers, and message payloads. +type WebSocketPreflight struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Phase SupervisorMiddlewarePhase `protobuf:"varint,2,opt,name=phase,proto3,enum=openshell.middleware.v1.SupervisorMiddlewarePhase" json:"phase,omitempty"` + Context *RequestContext `protobuf:"bytes,3,opt,name=context,proto3" json:"context,omitempty"` + // Admitted HTTP WebSocket-upgrade target. The method is GET, query is always + // empty, and path never includes a query string. + Target *HttpRequestTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + RequestedSubprotocols []string `protobuf:"bytes,5,rep,name=requested_subprotocols,json=requestedSubprotocols,proto3" json:"requested_subprotocols,omitempty"` + // Built-in middleware name or operator-owned registration name. + MiddlewareName string `protobuf:"bytes,6,opt,name=middleware_name,json=middlewareName,proto3" json:"middleware_name,omitempty"` + Config *structpb.Struct `protobuf:"bytes,7,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketPreflight) Reset() { + *x = WebSocketPreflight{} + mi := &file_supervisor_middleware_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketPreflight) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketPreflight) ProtoMessage() {} + +func (x *WebSocketPreflight) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketPreflight.ProtoReflect.Descriptor instead. +func (*WebSocketPreflight) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{26} +} + +func (x *WebSocketPreflight) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *WebSocketPreflight) GetPhase() SupervisorMiddlewarePhase { + if x != nil { + return x.Phase + } + return SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED +} + +func (x *WebSocketPreflight) GetContext() *RequestContext { + if x != nil { + return x.Context + } + return nil +} + +func (x *WebSocketPreflight) GetTarget() *HttpRequestTarget { + if x != nil { + return x.Target + } + return nil +} + +func (x *WebSocketPreflight) GetRequestedSubprotocols() []string { + if x != nil { + return x.RequestedSubprotocols + } + return nil +} + +func (x *WebSocketPreflight) GetMiddlewareName() string { + if x != nil { + return x.MiddlewareName + } + return "" +} + +func (x *WebSocketPreflight) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +// WebSocketSessionStart reports bounded metadata known only after the +// upstream 101 response validates. Empty selected_subprotocol means none. +type WebSocketSessionStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + SelectedSubprotocol string `protobuf:"bytes,1,opt,name=selected_subprotocol,json=selectedSubprotocol,proto3" json:"selected_subprotocol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketSessionStart) Reset() { + *x = WebSocketSessionStart{} + mi := &file_supervisor_middleware_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketSessionStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketSessionStart) ProtoMessage() {} + +func (x *WebSocketSessionStart) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketSessionStart.ProtoReflect.Descriptor instead. +func (*WebSocketSessionStart) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{27} +} + +func (x *WebSocketSessionStart) GetSelectedSubprotocol() string { + if x != nil { + return x.SelectedSubprotocol + } + return "" +} + +// WebSocketMessage contains one complete reconstructed logical message. +type WebSocketMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Session-global sequence starting at 1. Values delivered to one stage must + // strictly increase but need not be contiguous. Reject zero, duplicates, and + // regressions; accept gaps. + Sequence uint64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + // One complete logical payload. Protobuf string decoding enforces UTF-8 for + // text messages. Raw frame mechanics are never exposed. Limited to 4 MiB by + // the platform and the binding-specific cap. + // + // Types that are valid to be assigned to Payload: + // + // *WebSocketMessage_Text + // *WebSocketMessage_Binary + Payload isWebSocketMessage_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketMessage) Reset() { + *x = WebSocketMessage{} + mi := &file_supervisor_middleware_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketMessage) ProtoMessage() {} + +func (x *WebSocketMessage) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketMessage.ProtoReflect.Descriptor instead. +func (*WebSocketMessage) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{28} +} + +func (x *WebSocketMessage) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *WebSocketMessage) GetPayload() isWebSocketMessage_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *WebSocketMessage) GetText() string { + if x != nil { + if x, ok := x.Payload.(*WebSocketMessage_Text); ok { + return x.Text + } + } + return "" +} + +func (x *WebSocketMessage) GetBinary() []byte { + if x != nil { + if x, ok := x.Payload.(*WebSocketMessage_Binary); ok { + return x.Binary + } + } + return nil +} + +type isWebSocketMessage_Payload interface { + isWebSocketMessage_Payload() +} + +type WebSocketMessage_Text struct { + Text string `protobuf:"bytes,2,opt,name=text,proto3,oneof"` +} + +type WebSocketMessage_Binary struct { + Binary []byte `protobuf:"bytes,3,opt,name=binary,proto3,oneof"` +} + +func (*WebSocketMessage_Text) isWebSocketMessage_Payload() {} + +func (*WebSocketMessage_Binary) isWebSocketMessage_Payload() {} + +type WebSocketPreflightDecision struct { + state protoimpl.MessageState `protogen:"open.v1"` + Action WebSocketPreflightAction `protobuf:"varint,1,opt,name=action,proto3,enum=openshell.middleware.v1.WebSocketPreflightAction" json:"action,omitempty"` + // Free-form service diagnostic. OpenShell never exposes this to the + // workload or security logs. Limited to 4 KiB before discarding. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional stable machine-readable code for a deny decision. Because + // preflight runs before the HTTP upgrade completes, OpenShell may return + // this code to the requester. Codes follow the same format and 64-byte + // maximum as HttpRequestResult.reason_code. + ReasonCode string `protobuf:"bytes,3,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + // Audit-safe findings produced during preflight. At most 32 findings of at + // most 4 KiB encoded each are accepted. + Findings []*Finding `protobuf:"bytes,4,rep,name=findings,proto3" json:"findings,omitempty"` + // Non-secret service-defined metadata included in diagnostics. At most 64 + // entries and 32 KiB of combined key/value data are accepted. + Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketPreflightDecision) Reset() { + *x = WebSocketPreflightDecision{} + mi := &file_supervisor_middleware_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketPreflightDecision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketPreflightDecision) ProtoMessage() {} + +func (x *WebSocketPreflightDecision) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketPreflightDecision.ProtoReflect.Descriptor instead. +func (*WebSocketPreflightDecision) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{29} +} + +func (x *WebSocketPreflightDecision) GetAction() WebSocketPreflightAction { + if x != nil { + return x.Action + } + return WebSocketPreflightAction_WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED +} + +func (x *WebSocketPreflightDecision) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *WebSocketPreflightDecision) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *WebSocketPreflightDecision) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *WebSocketPreflightDecision) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// WebSocketMessageResult contains the decision and optional replacement for +// one message. A replacement must use the same variant as the input payload. +type WebSocketMessageResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Must exactly match the sequence of the corresponding WebSocketMessage. + Sequence uint64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + Decision Decision `protobuf:"varint,2,opt,name=decision,proto3,enum=openshell.middleware.v1.Decision" json:"decision,omitempty"` + // Absence preserves the input unchanged. Oneof presence distinguishes an + // empty replacement from no replacement, and string decoding enforces UTF-8. + // + // Types that are valid to be assigned to Replacement: + // + // *WebSocketMessageResult_Text + // *WebSocketMessageResult_Binary + Replacement isWebSocketMessageResult_Replacement `protobuf_oneof:"replacement"` + // Free-form service diagnostic. OpenShell never exposes this to the + // workload or security logs. Limited to 4 KiB before discarding. + Reason string `protobuf:"bytes,5,opt,name=reason,proto3" json:"reason,omitempty"` + // Optional stable machine-readable code for OCSF only. Unlike the HTTP + // reason_code, this value is never put in a WebSocket close frame. + ReasonCode string `protobuf:"bytes,6,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + Findings []*Finding `protobuf:"bytes,7,rep,name=findings,proto3" json:"findings,omitempty"` + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketMessageResult) Reset() { + *x = WebSocketMessageResult{} + mi := &file_supervisor_middleware_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketMessageResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketMessageResult) ProtoMessage() {} + +func (x *WebSocketMessageResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketMessageResult.ProtoReflect.Descriptor instead. +func (*WebSocketMessageResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{30} +} + +func (x *WebSocketMessageResult) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *WebSocketMessageResult) GetDecision() Decision { + if x != nil { + return x.Decision + } + return Decision_DECISION_UNSPECIFIED +} + +func (x *WebSocketMessageResult) GetReplacement() isWebSocketMessageResult_Replacement { + if x != nil { + return x.Replacement + } + return nil +} + +func (x *WebSocketMessageResult) GetText() string { + if x != nil { + if x, ok := x.Replacement.(*WebSocketMessageResult_Text); ok { + return x.Text + } + } + return "" +} + +func (x *WebSocketMessageResult) GetBinary() []byte { + if x != nil { + if x, ok := x.Replacement.(*WebSocketMessageResult_Binary); ok { + return x.Binary + } + } + return nil +} + +func (x *WebSocketMessageResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *WebSocketMessageResult) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *WebSocketMessageResult) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *WebSocketMessageResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isWebSocketMessageResult_Replacement interface { + isWebSocketMessageResult_Replacement() +} + +type WebSocketMessageResult_Text struct { + Text string `protobuf:"bytes,3,opt,name=text,proto3,oneof"` +} + +type WebSocketMessageResult_Binary struct { + Binary []byte `protobuf:"bytes,4,opt,name=binary,proto3,oneof"` +} + +func (*WebSocketMessageResult_Text) isWebSocketMessageResult_Replacement() {} + +func (*WebSocketMessageResult_Binary) isWebSocketMessageResult_Replacement() {} + +// WebSocketSessionEventResult is an evaluation result for a preflight or message +// event. Session start and end events do not produce results. +type WebSocketSessionEventResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *WebSocketSessionEventResult_PreflightDecision + // *WebSocketSessionEventResult_MessageResult + Result isWebSocketSessionEventResult_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebSocketSessionEventResult) Reset() { + *x = WebSocketSessionEventResult{} + mi := &file_supervisor_middleware_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebSocketSessionEventResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebSocketSessionEventResult) ProtoMessage() {} + +func (x *WebSocketSessionEventResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebSocketSessionEventResult.ProtoReflect.Descriptor instead. +func (*WebSocketSessionEventResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{31} +} + +func (x *WebSocketSessionEventResult) GetResult() isWebSocketSessionEventResult_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *WebSocketSessionEventResult) GetPreflightDecision() *WebSocketPreflightDecision { + if x != nil { + if x, ok := x.Result.(*WebSocketSessionEventResult_PreflightDecision); ok { + return x.PreflightDecision + } + } + return nil +} + +func (x *WebSocketSessionEventResult) GetMessageResult() *WebSocketMessageResult { + if x != nil { + if x, ok := x.Result.(*WebSocketSessionEventResult_MessageResult); ok { + return x.MessageResult + } + } + return nil +} + +type isWebSocketSessionEventResult_Result interface { + isWebSocketSessionEventResult_Result() +} + +type WebSocketSessionEventResult_PreflightDecision struct { + PreflightDecision *WebSocketPreflightDecision `protobuf:"bytes,1,opt,name=preflight_decision,json=preflightDecision,proto3,oneof"` +} + +type WebSocketSessionEventResult_MessageResult struct { + MessageResult *WebSocketMessageResult `protobuf:"bytes,2,opt,name=message_result,json=messageResult,proto3,oneof"` +} + +func (*WebSocketSessionEventResult_PreflightDecision) isWebSocketSessionEventResult_Result() {} + +func (*WebSocketSessionEventResult_MessageResult) isWebSocketSessionEventResult_Result() {} + +// RequestContext identifies the sandbox request being evaluated. +type RequestContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Request id used to correlate middleware and supervisor logs. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Sandbox id that originated the request. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Workload process that originated the request, when available. + OriginatingProcess *Process `protobuf:"bytes,3,opt,name=originating_process,json=originatingProcess,proto3" json:"originating_process,omitempty"` + // Sandbox name that originated the request. For display and logging only. + // Names are workspace-scoped and may be reused for different sandbox + // instances, so consumers must use sandbox_id for authorization, persistence, + // durable correlation, and identity. + Sandbox string `protobuf:"bytes,4,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Workspace the sandbox belongs to. For display and logging only; see the + // sandbox guidance above. + Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestContext) Reset() { + *x = RequestContext{} + mi := &file_supervisor_middleware_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestContext) ProtoMessage() {} + +func (x *RequestContext) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestContext.ProtoReflect.Descriptor instead. +func (*RequestContext) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{32} +} + +func (x *RequestContext) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *RequestContext) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *RequestContext) GetOriginatingProcess() *Process { + if x != nil { + return x.OriginatingProcess + } + return nil +} + +func (x *RequestContext) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *RequestContext) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// HttpRequestTarget describes the admitted HTTP destination and request target. +type HttpRequestTarget struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Request scheme, such as "http", "https", "ws", or "wss". + Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"` + // Destination hostname selected by network policy. + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + // Destination TCP port. + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + // HTTP request method. + Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` + // Request path without the query string. + Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` + // Raw request query string without the leading question mark. + Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpRequestTarget) Reset() { + *x = HttpRequestTarget{} + mi := &file_supervisor_middleware_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpRequestTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpRequestTarget) ProtoMessage() {} + +func (x *HttpRequestTarget) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HttpRequestTarget.ProtoReflect.Descriptor instead. +func (*HttpRequestTarget) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{33} +} + +func (x *HttpRequestTarget) GetScheme() string { + if x != nil { + return x.Scheme + } + return "" +} + +func (x *HttpRequestTarget) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *HttpRequestTarget) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *HttpRequestTarget) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HttpRequestTarget) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *HttpRequestTarget) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +// Process identifies a workload process and its executable ancestry. +type Process struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Executable path for the originating process. + Binary string `protobuf:"bytes,1,opt,name=binary,proto3" json:"binary,omitempty"` + // Process id within the sandbox. + Pid uint32 `protobuf:"varint,2,opt,name=pid,proto3" json:"pid,omitempty"` + // Executable paths for ancestor processes, nearest parent first. + Ancestors []string `protobuf:"bytes,3,rep,name=ancestors,proto3" json:"ancestors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Process) Reset() { + *x = Process{} + mi := &file_supervisor_middleware_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Process) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Process) ProtoMessage() {} + +func (x *Process) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[34] + 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 Process.ProtoReflect.Descriptor instead. +func (*Process) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{34} +} + +func (x *Process) GetBinary() string { + if x != nil { + return x.Binary + } + return "" +} + +func (x *Process) GetPid() uint32 { + if x != nil { + return x.Pid + } + return 0 +} + +func (x *Process) GetAncestors() []string { + if x != nil { + return x.Ancestors + } + return nil +} + +// Finding is an audit-safe observation produced during evaluation. +type Finding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable, service-defined finding type. + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Human-readable finding label that does not contain request content. + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + // Number of matching observations represented by this finding. + Count uint32 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` + // Service-defined confidence level. + Confidence string `protobuf:"bytes,4,opt,name=confidence,proto3" json:"confidence,omitempty"` + // Service-defined severity level. + Severity string `protobuf:"bytes,5,opt,name=severity,proto3" json:"severity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Finding) Reset() { + *x = Finding{} + mi := &file_supervisor_middleware_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Finding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Finding) ProtoMessage() {} + +func (x *Finding) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[35] + 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 Finding.ProtoReflect.Descriptor instead. +func (*Finding) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{35} +} + +func (x *Finding) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Finding) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *Finding) GetCount() uint32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *Finding) GetConfidence() string { + if x != nil { + return x.Confidence + } + return "" +} + +func (x *Finding) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +// WriteHeader proposes one header value and defines collision behavior. +type WriteHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + OnExisting ExistingHeaderAction `protobuf:"varint,3,opt,name=on_existing,json=onExisting,proto3,enum=openshell.middleware.v1.ExistingHeaderAction" json:"on_existing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WriteHeader) Reset() { + *x = WriteHeader{} + mi := &file_supervisor_middleware_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WriteHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WriteHeader) ProtoMessage() {} + +func (x *WriteHeader) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[36] + 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 WriteHeader.ProtoReflect.Descriptor instead. +func (*WriteHeader) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{36} +} + +func (x *WriteHeader) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *WriteHeader) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *WriteHeader) GetOnExisting() ExistingHeaderAction { + if x != nil { + return x.OnExisting + } + return ExistingHeaderAction_EXISTING_HEADER_ACTION_UNSPECIFIED +} + +// RemoveHeader removes every value for a case-insensitive header name. +type RemoveHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveHeader) Reset() { + *x = RemoveHeader{} + mi := &file_supervisor_middleware_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveHeader) ProtoMessage() {} + +func (x *RemoveHeader) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[37] + 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 RemoveHeader.ProtoReflect.Descriptor instead. +func (*RemoveHeader) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{37} +} + +func (x *RemoveHeader) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// HeaderMutation is one ordered HTTP header operation. +type HeaderMutation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *HeaderMutation_Write + // *HeaderMutation_Remove + Operation isHeaderMutation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderMutation) Reset() { + *x = HeaderMutation{} + mi := &file_supervisor_middleware_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderMutation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderMutation) ProtoMessage() {} + +func (x *HeaderMutation) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[38] + 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 HeaderMutation.ProtoReflect.Descriptor instead. +func (*HeaderMutation) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{38} +} + +func (x *HeaderMutation) GetOperation() isHeaderMutation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *HeaderMutation) GetWrite() *WriteHeader { + if x != nil { + if x, ok := x.Operation.(*HeaderMutation_Write); ok { + return x.Write + } + } + return nil +} + +func (x *HeaderMutation) GetRemove() *RemoveHeader { + if x != nil { + if x, ok := x.Operation.(*HeaderMutation_Remove); ok { + return x.Remove + } + } + return nil +} + +type isHeaderMutation_Operation interface { + isHeaderMutation_Operation() +} + +type HeaderMutation_Write struct { + Write *WriteHeader `protobuf:"bytes,1,opt,name=write,proto3,oneof"` +} + +type HeaderMutation_Remove struct { + Remove *RemoveHeader `protobuf:"bytes,2,opt,name=remove,proto3,oneof"` +} + +func (*HeaderMutation_Write) isHeaderMutation_Operation() {} + +func (*HeaderMutation_Remove) isHeaderMutation_Operation() {} + +// HttpRequestResult contains the decision and optional request mutations. +type HttpRequestResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Allow or deny decision for this request. + Decision Decision `protobuf:"varint,1,opt,name=decision,proto3,enum=openshell.middleware.v1.Decision" json:"decision,omitempty"` + // Free-form service diagnostic. OpenShell does not relay this text into + // denied responses or security logs. Limited to 4 KiB before discarding. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + // Replacement request body when has_body is true. Limited to 4 MiB. + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` + // True when body should replace the request body, including with an empty body. + HasBody bool `protobuf:"varint,4,opt,name=has_body,json=hasBody,proto3" json:"has_body,omitempty"` + // Ordered request-header mutations applied before the next middleware and + // before forwarding. Writes and removals may target visible end-to-end + // request headers, but credential, routing, framing, and hop-by-hop headers + // are always protected. Written values cannot contain OpenShell credential + // placeholder syntax. A violating result is a middleware failure handled + // according to the policy failure mode. At most 64 operations, 32 KiB of + // validated name/value data, and 64 KiB encoded are accepted. + HeaderMutations []*HeaderMutation `protobuf:"bytes,5,rep,name=header_mutations,json=headerMutations,proto3" json:"header_mutations,omitempty"` + // Audit-safe findings produced during evaluation. For operator-run services, + // OpenShell logs platform-owned fields derived from the operator-owned + // registration name rather than service-provided type, label, confidence, + // or metadata text. + // At most 32 findings of at most 4 KiB encoded each are accepted per stage. + // A policy selects at most 10 stages, so one chain retains at most 320. + Findings []*Finding `protobuf:"bytes,6,rep,name=findings,proto3" json:"findings,omitempty"` + // Non-secret service-defined metadata included in diagnostics. At most 64 + // entries and 32 KiB of combined key/value data are accepted. + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional stable machine-readable code for a deny decision. Codes must + // start with a lowercase ASCII letter and contain only lowercase ASCII + // letters, digits, and underscores, with a maximum length of 64 bytes. + // OpenShell may return this code to the requester, unlike free-form reason. + ReasonCode string `protobuf:"bytes,8,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpRequestResult) Reset() { + *x = HttpRequestResult{} + mi := &file_supervisor_middleware_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpRequestResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpRequestResult) ProtoMessage() {} + +func (x *HttpRequestResult) ProtoReflect() protoreflect.Message { + mi := &file_supervisor_middleware_proto_msgTypes[39] + 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 HttpRequestResult.ProtoReflect.Descriptor instead. +func (*HttpRequestResult) Descriptor() ([]byte, []int) { + return file_supervisor_middleware_proto_rawDescGZIP(), []int{39} +} + +func (x *HttpRequestResult) GetDecision() Decision { + if x != nil { + return x.Decision + } + return Decision_DECISION_UNSPECIFIED +} + +func (x *HttpRequestResult) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *HttpRequestResult) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *HttpRequestResult) GetHasBody() bool { + if x != nil { + return x.HasBody + } + return false +} + +func (x *HttpRequestResult) GetHeaderMutations() []*HeaderMutation { + if x != nil { + return x.HeaderMutations + } + return nil +} + +func (x *HttpRequestResult) GetFindings() []*Finding { + if x != nil { + return x.Findings + } + return nil +} + +func (x *HttpRequestResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *HttpRequestResult) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +var File_supervisor_middleware_proto protoreflect.FileDescriptor + +const file_supervisor_middleware_proto_rawDesc = "" + + "\n" + + "\x1bsupervisor_middleware.proto\x12\x17openshell.middleware.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0fextension.proto\"[\n" + + "\x19MiddlewareDescribeRequest\x12>\n" + + "\agateway\x18\x01 \x01(\v2$.openshell.extension.v1.PeerMetadataR\agateway\"\x8a\x02\n" + + "\x12MiddlewareManifest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12'\n" + + "\x0fservice_version\x18\x02 \x01(\tR\x0eserviceVersion\x12F\n" + + "\bbindings\x18\x03 \x03(\v2*.openshell.middleware.v1.MiddlewareBindingR\bbindings\x12+\n" + + "\x11expected_audience\x18\x04 \x01(\tR\x10expectedAudience\x12B\n" + + "\textension\x18\x05 \x01(\v2$.openshell.extension.v1.PeerMetadataR\textension\"\xb2\x02\n" + + "\x11MiddlewareBinding\x12T\n" + + "\toperation\x18\x01 \x01(\x0e26.openshell.middleware.v1.SupervisorMiddlewareOperationR\toperation\x12H\n" + + "\x05phase\x18\x02 \x01(\x0e22.openshell.middleware.v1.SupervisorMiddlewarePhaseR\x05phase\x12*\n" + + "\x11max_payload_bytes\x18\x03 \x01(\x04R\x0fmaxPayloadBytes\x12B\n" + + "\x0frequest_timeout\x18h \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeoutJ\x04\b\x04\x10\x05R\atimeout\"q\n" + + "\x15ValidateConfigRequest\x12/\n" + + "\x06config\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x06config\x12'\n" + + "\x0fmiddleware_name\x18\x02 \x01(\tR\x0emiddlewareName\"F\n" + + "\x16ValidateConfigResponse\x12\x14\n" + + "\x05valid\x18\x01 \x01(\bR\x05valid\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"\x95\x03\n" + + "\x15HttpRequestEvaluation\x12H\n" + + "\x05phase\x18\x01 \x01(\x0e22.openshell.middleware.v1.SupervisorMiddlewarePhaseR\x05phase\x12A\n" + + "\acontext\x18\x02 \x01(\v2'.openshell.middleware.v1.RequestContextR\acontext\x12/\n" + + "\x06config\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x06config\x12B\n" + + "\x06target\x18\x04 \x01(\v2*.openshell.middleware.v1.HttpRequestTargetR\x06target\x12=\n" + + "\aheaders\x18\x05 \x03(\v2#.openshell.middleware.v1.HttpHeaderR\aheaders\x12\x12\n" + + "\x04body\x18\x06 \x01(\fR\x04body\x12'\n" + + "\x0fmiddleware_name\x18\a \x01(\tR\x0emiddlewareName\"6\n" + + "\n" + + "HttpHeader\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"\xd0\x02\n" + + "\x11HttpResponseEvent\x12N\n" + + "\tpreflight\x18\x01 \x01(\v2..openshell.middleware.v1.HttpResponsePreflightH\x00R\tpreflight\x12C\n" + + "\x04body\x18\x02 \x01(\v2-.openshell.middleware.v1.HttpResponseBodyUnitH\x00R\x04body\x12K\n" + + "\btrailers\x18\x04 \x01(\v2-.openshell.middleware.v1.HttpResponseTrailersH\x00R\btrailers\x12P\n" + + "\vsession_end\x18\x03 \x01(\v2-.openshell.middleware.v1.MiddlewareSessionEndH\x00R\n" + + "sessionEndB\a\n" + + "\x05event\"\xba\x02\n" + + "\x17HttpResponseEventResult\x12a\n" + + "\x10preflight_result\x18\x01 \x01(\v24.openshell.middleware.v1.HttpResponsePreflightResultH\x00R\x0fpreflightResult\x12R\n" + + "\vbody_result\x18\x02 \x01(\v2/.openshell.middleware.v1.HttpResponseBodyResultH\x00R\n" + + "bodyResult\x12^\n" + + "\x0ftrailers_result\x18\x03 \x01(\v23.openshell.middleware.v1.HttpResponseTrailersResultH\x00R\x0etrailersResultB\b\n" + + "\x06result\"\xe5\x03\n" + + "\x15HttpResponsePreflight\x12A\n" + + "\acontext\x18\x01 \x01(\v2'.openshell.middleware.v1.RequestContextR\acontext\x12B\n" + + "\x06target\x18\x02 \x01(\v2*.openshell.middleware.v1.HttpRequestTargetR\x06target\x12\x1f\n" + + "\vstatus_code\x18\x03 \x01(\rR\n" + + "statusCode\x12=\n" + + "\aheaders\x18\x04 \x03(\v2#.openshell.middleware.v1.HttpHeaderR\aheaders\x12'\n" + + "\x0fmiddleware_name\x18\x05 \x01(\tR\x0emiddlewareName\x12/\n" + + "\x06config\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x06config\x12*\n" + + "\x11max_payload_bytes\x18\a \x01(\x04R\x0fmaxPayloadBytes\x12_\n" + + "\x14permitted_body_modes\x18\b \x03(\x0e2-.openshell.middleware.v1.HttpResponseBodyModeR\x12permittedBodyModes\"\xb5\x04\n" + + "\x1bHttpResponsePreflightResult\x12H\n" + + "\x04skip\x18\x01 \x01(\v22.openshell.middleware.v1.HttpResponsePreflightSkipH\x00R\x04skip\x12Q\n" + + "\ainspect\x18\x02 \x01(\v25.openshell.middleware.v1.HttpResponsePreflightInspectH\x00R\ainspect\x12[\n" + + "\x0eblock_delivery\x18\a \x01(\v22.openshell.middleware.v1.HttpResponseBlockDeliveryH\x00R\rblockDelivery\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1f\n" + + "\vreason_code\x18\x04 \x01(\tR\n" + + "reasonCode\x12<\n" + + "\bfindings\x18\x05 \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12^\n" + + "\bmetadata\x18\x06 \x03(\v2B.openshell.middleware.v1.HttpResponsePreflightResult.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\b\n" + + "\x06action\"\x1b\n" + + "\x19HttpResponsePreflightSkip\"\x1b\n" + + "\x19HttpResponseBlockDelivery\"\xbe\x01\n" + + "\x1cHttpResponsePreflightInspect\x12J\n" + + "\tbody_mode\x18\x01 \x01(\x0e2-.openshell.middleware.v1.HttpResponseBodyModeR\bbodyMode\x12R\n" + + "\x10header_mutations\x18\x02 \x03(\v2'.openshell.middleware.v1.HeaderMutationR\x0fheaderMutations\"w\n" + + "\x14HttpResponseBodyUnit\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x04R\bsequence\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04data\x12\"\n" + + "\rend_of_stream\x18\x03 \x01(\bR\vendOfStreamB\t\n" + + "\apayload\"\xba\x05\n" + + "\x16HttpResponseBodyResult\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x04R\bsequence\x12Y\n" + + "\fpass_through\x18\x02 \x01(\v24.openshell.middleware.v1.HttpResponseBodyPassThroughH\x00R\vpassThrough\x12R\n" + + "\ttransform\x18\x03 \x01(\v22.openshell.middleware.v1.HttpResponseBodyTransformH\x00R\ttransform\x12[\n" + + "\x0eblock_delivery\x18\b \x01(\v22.openshell.middleware.v1.HttpResponseBlockDeliveryH\x00R\rblockDelivery\x12_\n" + + "\x0eskip_remaining\x18\t \x01(\v26.openshell.middleware.v1.HttpResponseBodySkipRemainingH\x00R\rskipRemaining\x12\x16\n" + + "\x06reason\x18\x04 \x01(\tR\x06reason\x12\x1f\n" + + "\vreason_code\x18\x05 \x01(\tR\n" + + "reasonCode\x12<\n" + + "\bfindings\x18\x06 \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12Y\n" + + "\bmetadata\x18\a \x03(\v2=.openshell.middleware.v1.HttpResponseBodyResult.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\b\n" + + "\x06action\"\x1d\n" + + "\x1bHttpResponseBodyPassThrough\"\xd9\x01\n" + + "\x1dHttpResponseBodySkipRemaining\x12Y\n" + + "\fpass_through\x18\x01 \x01(\v24.openshell.middleware.v1.HttpResponseBodyPassThroughH\x00R\vpassThrough\x12R\n" + + "\ttransform\x18\x02 \x01(\v22.openshell.middleware.v1.HttpResponseBodyTransformH\x00R\ttransformB\t\n" + + "\acurrent\"@\n" + + "\x19HttpResponseBodyTransform\x12\x14\n" + + "\x04data\x18\x01 \x01(\fH\x00R\x04dataB\r\n" + + "\vreplacement\"U\n" + + "\x14HttpResponseTrailers\x12=\n" + + "\aheaders\x18\x01 \x03(\v2#.openshell.middleware.v1.HttpHeaderR\aheaders\"\x85\x03\n" + + "\x1aHttpResponseTrailersResult\x12T\n" + + "\x11trailer_mutations\x18\x01 \x03(\v2'.openshell.middleware.v1.HeaderMutationR\x10trailerMutations\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x1f\n" + + "\vreason_code\x18\x03 \x01(\tR\n" + + "reasonCode\x12<\n" + + "\bfindings\x18\x04 \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12]\n" + + "\bmetadata\x18\x05 \x03(\v2A.openshell.middleware.v1.HttpResponseTrailersResult.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc3\x01\n" + + "\x14MiddlewareSessionEnd\x12K\n" + + "\x06reason\x18\x01 \x01(\x0e23.openshell.middleware.v1.MiddlewareSessionEndReasonR\x06reason\x12^\n" + + "\x0eprotocol_error\x18\x02 \x01(\v27.openshell.middleware.v1.MiddlewareSessionProtocolErrorR\rprotocolError\"\xe9\x01\n" + + "\x1eMiddlewareSessionProtocolError\x12P\n" + + "\n" + + "web_socket\x18\x01 \x01(\v2/.openshell.middleware.v1.WebSocketProtocolErrorH\x00R\twebSocket\x12k\n" + + "\x13middleware_exchange\x18\x02 \x01(\v28.openshell.middleware.v1.MiddlewareExchangeProtocolErrorH\x00R\x12middlewareExchangeB\b\n" + + "\x06domain\"\x18\n" + + "\x16WebSocketProtocolError\"!\n" + + "\x1fMiddlewareExchangeProtocolError\"\xdd\x02\n" + + "\x15WebSocketSessionEvent\x12K\n" + + "\tpreflight\x18\x01 \x01(\v2+.openshell.middleware.v1.WebSocketPreflightH\x00R\tpreflight\x12U\n" + + "\rsession_start\x18\x02 \x01(\v2..openshell.middleware.v1.WebSocketSessionStartH\x00R\fsessionStart\x12E\n" + + "\amessage\x18\x03 \x01(\v2).openshell.middleware.v1.WebSocketMessageH\x00R\amessage\x12P\n" + + "\vsession_end\x18\x04 \x01(\v2-.openshell.middleware.v1.MiddlewareSessionEndH\x00R\n" + + "sessionEndB\a\n" + + "\x05event\"\x95\x03\n" + + "\x12WebSocketPreflight\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12H\n" + + "\x05phase\x18\x02 \x01(\x0e22.openshell.middleware.v1.SupervisorMiddlewarePhaseR\x05phase\x12A\n" + + "\acontext\x18\x03 \x01(\v2'.openshell.middleware.v1.RequestContextR\acontext\x12B\n" + + "\x06target\x18\x04 \x01(\v2*.openshell.middleware.v1.HttpRequestTargetR\x06target\x125\n" + + "\x16requested_subprotocols\x18\x05 \x03(\tR\x15requestedSubprotocols\x12'\n" + + "\x0fmiddleware_name\x18\x06 \x01(\tR\x0emiddlewareName\x12/\n" + + "\x06config\x18\a \x01(\v2\x17.google.protobuf.StructR\x06config\"J\n" + + "\x15WebSocketSessionStart\x121\n" + + "\x14selected_subprotocol\x18\x01 \x01(\tR\x13selectedSubprotocol\"i\n" + + "\x10WebSocketMessage\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x04R\bsequence\x12\x14\n" + + "\x04text\x18\x02 \x01(\tH\x00R\x04text\x12\x18\n" + + "\x06binary\x18\x03 \x01(\fH\x00R\x06binaryB\t\n" + + "\apayload\"\xfa\x02\n" + + "\x1aWebSocketPreflightDecision\x12I\n" + + "\x06action\x18\x01 \x01(\x0e21.openshell.middleware.v1.WebSocketPreflightActionR\x06action\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x1f\n" + + "\vreason_code\x18\x03 \x01(\tR\n" + + "reasonCode\x12<\n" + + "\bfindings\x18\x04 \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12]\n" + + "\bmetadata\x18\x05 \x03(\v2A.openshell.middleware.v1.WebSocketPreflightDecision.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc1\x03\n" + + "\x16WebSocketMessageResult\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x04R\bsequence\x12=\n" + + "\bdecision\x18\x02 \x01(\x0e2!.openshell.middleware.v1.DecisionR\bdecision\x12\x14\n" + + "\x04text\x18\x03 \x01(\tH\x00R\x04text\x12\x18\n" + + "\x06binary\x18\x04 \x01(\fH\x00R\x06binary\x12\x16\n" + + "\x06reason\x18\x05 \x01(\tR\x06reason\x12\x1f\n" + + "\vreason_code\x18\x06 \x01(\tR\n" + + "reasonCode\x12<\n" + + "\bfindings\x18\a \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12Y\n" + + "\bmetadata\x18\b \x03(\v2=.openshell.middleware.v1.WebSocketMessageResult.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\r\n" + + "\vreplacement\"\xe7\x01\n" + + "\x1bWebSocketSessionEventResult\x12d\n" + + "\x12preflight_decision\x18\x01 \x01(\v23.openshell.middleware.v1.WebSocketPreflightDecisionH\x00R\x11preflightDecision\x12X\n" + + "\x0emessage_result\x18\x02 \x01(\v2/.openshell.middleware.v1.WebSocketMessageResultH\x00R\rmessageResultB\b\n" + + "\x06result\"\xd9\x01\n" + + "\x0eRequestContext\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12Q\n" + + "\x13originating_process\x18\x03 \x01(\v2 .openshell.middleware.v1.ProcessR\x12originatingProcess\x12\x18\n" + + "\asandbox\x18\x04 \x01(\tR\asandbox\x12\x1c\n" + + "\tworkspace\x18\x05 \x01(\tR\tworkspace\"\x95\x01\n" + + "\x11HttpRequestTarget\x12\x16\n" + + "\x06scheme\x18\x01 \x01(\tR\x06scheme\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04port\x12\x16\n" + + "\x06method\x18\x04 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x05 \x01(\tR\x04path\x12\x14\n" + + "\x05query\x18\x06 \x01(\tR\x05query\"Q\n" + + "\aProcess\x12\x16\n" + + "\x06binary\x18\x01 \x01(\tR\x06binary\x12\x10\n" + + "\x03pid\x18\x02 \x01(\rR\x03pid\x12\x1c\n" + + "\tancestors\x18\x03 \x03(\tR\tancestors\"\x85\x01\n" + + "\aFinding\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12\x14\n" + + "\x05count\x18\x03 \x01(\rR\x05count\x12\x1e\n" + + "\n" + + "confidence\x18\x04 \x01(\tR\n" + + "confidence\x12\x1a\n" + + "\bseverity\x18\x05 \x01(\tR\bseverity\"\x87\x01\n" + + "\vWriteHeader\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\x12N\n" + + "\von_existing\x18\x03 \x01(\x0e2-.openshell.middleware.v1.ExistingHeaderActionR\n" + + "onExisting\"\"\n" + + "\fRemoveHeader\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x9c\x01\n" + + "\x0eHeaderMutation\x12<\n" + + "\x05write\x18\x01 \x01(\v2$.openshell.middleware.v1.WriteHeaderH\x00R\x05write\x12?\n" + + "\x06remove\x18\x02 \x01(\v2%.openshell.middleware.v1.RemoveHeaderH\x00R\x06removeB\v\n" + + "\toperation\"\xdf\x03\n" + + "\x11HttpRequestResult\x12=\n" + + "\bdecision\x18\x01 \x01(\x0e2!.openshell.middleware.v1.DecisionR\bdecision\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x12\n" + + "\x04body\x18\x03 \x01(\fR\x04body\x12\x19\n" + + "\bhas_body\x18\x04 \x01(\bR\ahasBody\x12R\n" + + "\x10header_mutations\x18\x05 \x03(\v2'.openshell.middleware.v1.HeaderMutationR\x0fheaderMutations\x12<\n" + + "\bfindings\x18\x06 \x03(\v2 .openshell.middleware.v1.FindingR\bfindings\x12T\n" + + "\bmetadata\x18\a \x03(\v28.openshell.middleware.v1.HttpRequestResult.MetadataEntryR\bmetadata\x12\x1f\n" + + "\vreason_code\x18\b \x01(\tR\n" + + "reasonCode\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\xc1\x01\n" + + "\x14HttpResponseBodyMode\x12'\n" + + "#HTTP_RESPONSE_BODY_MODE_UNSPECIFIED\x10\x00\x12(\n" + + "$HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY\x10\x01\x12,\n" + + "(HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES\x10\x02\x12(\n" + + "$HTTP_RESPONSE_BODY_MODE_STREAM_BYTES\x10\x03*\xf9\x04\n" + + "\x1aMiddlewareSessionEndReason\x12-\n" + + ")MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED\x10\x00\x12(\n" + + "$MIDDLEWARE_SESSION_END_REASON_NORMAL\x10\x01\x127\n" + + "3MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT\x10\x02\x12/\n" + + "+MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD\x10\x03\x123\n" + + "/MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL\x10\x04\x124\n" + + "0MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE\x10\x05\x120\n" + + ",MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR\x10\x06\x12.\n" + + "*MIDDLEWARE_SESSION_END_REASON_CANCELLATION\x10\a\x122\n" + + ".MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE\x10\b\x12/\n" + + "+MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL\x10\t\x12/\n" + + "+MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED\x10\n" + + "\x125\n" + + "1MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT\x10\v*\xec\x01\n" + + "\x1dSupervisorMiddlewareOperation\x12/\n" + + "+SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED\x10\x00\x120\n" + + ",SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST\x10\x01\x125\n" + + "1SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE\x10\x02\x121\n" + + "-SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE\x10\x03*\xa5\x01\n" + + "\x19SupervisorMiddlewarePhase\x12+\n" + + "'SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED\x10\x00\x12/\n" + + "+SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS\x10\x01\x12*\n" + + "&SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN\x10\x02*\xbc\x01\n" + + "\x18WebSocketPreflightAction\x12+\n" + + "'WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED\x10\x00\x12'\n" + + "#WEB_SOCKET_PREFLIGHT_ACTION_INSPECT\x10\x01\x12$\n" + + " WEB_SOCKET_PREFLIGHT_ACTION_SKIP\x10\x02\x12$\n" + + " WEB_SOCKET_PREFLIGHT_ACTION_DENY\x10\x03*K\n" + + "\bDecision\x12\x18\n" + + "\x14DECISION_UNSPECIFIED\x10\x00\x12\x12\n" + + "\x0eDECISION_ALLOW\x10\x01\x12\x11\n" + + "\rDECISION_DENY\x10\x02*\xa8\x01\n" + + "\x14ExistingHeaderAction\x12&\n" + + "\"EXISTING_HEADER_ACTION_UNSPECIFIED\x10\x00\x12!\n" + + "\x1dEXISTING_HEADER_ACTION_APPEND\x10\x01\x12$\n" + + " EXISTING_HEADER_ACTION_OVERWRITE\x10\x02\x12\x1f\n" + + "\x1bEXISTING_HEADER_ACTION_SKIP\x10\x032\xf0\x03\n" + + "\x14SupervisorMiddleware\x12k\n" + + "\bDescribe\x122.openshell.middleware.v1.MiddlewareDescribeRequest\x1a+.openshell.middleware.v1.MiddlewareManifest\x12q\n" + + "\x0eValidateConfig\x12..openshell.middleware.v1.ValidateConfigRequest\x1a/.openshell.middleware.v1.ValidateConfigResponse\x12q\n" + + "\x13EvaluateHttpRequest\x12..openshell.middleware.v1.HttpRequestEvaluation\x1a*.openshell.middleware.v1.HttpRequestResult\x12\x84\x01\n" + + "\x18EvaluateWebSocketSession\x12..openshell.middleware.v1.WebSocketSessionEvent\x1a4.openshell.middleware.v1.WebSocketSessionEventResult(\x010\x012\x85\x01\n" + + "\x15HttpResponsePreReturn\x12l\n" + + "\bEvaluate\x12*.openshell.middleware.v1.HttpResponseEvent\x1a0.openshell.middleware.v1.HttpResponseEventResult(\x010\x01B\x89\x02\n" + + "\x1bcom.openshell.middleware.v1B\x19SupervisorMiddlewareProtoP\x01ZQgithub.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1;middlewarev1\xa2\x02\x03OMX\xaa\x02\x17Openshell.Middleware.V1\xca\x02\x17Openshell\\Middleware\\V1\xe2\x02#Openshell\\Middleware\\V1\\GPBMetadata\xea\x02\x19Openshell::Middleware::V1b\x06proto3" + +var ( + file_supervisor_middleware_proto_rawDescOnce sync.Once + file_supervisor_middleware_proto_rawDescData []byte +) + +func file_supervisor_middleware_proto_rawDescGZIP() []byte { + file_supervisor_middleware_proto_rawDescOnce.Do(func() { + file_supervisor_middleware_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_supervisor_middleware_proto_rawDesc), len(file_supervisor_middleware_proto_rawDesc))) + }) + return file_supervisor_middleware_proto_rawDescData +} + +var file_supervisor_middleware_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_supervisor_middleware_proto_msgTypes = make([]protoimpl.MessageInfo, 46) +var file_supervisor_middleware_proto_goTypes = []any{ + (HttpResponseBodyMode)(0), // 0: openshell.middleware.v1.HttpResponseBodyMode + (MiddlewareSessionEndReason)(0), // 1: openshell.middleware.v1.MiddlewareSessionEndReason + (SupervisorMiddlewareOperation)(0), // 2: openshell.middleware.v1.SupervisorMiddlewareOperation + (SupervisorMiddlewarePhase)(0), // 3: openshell.middleware.v1.SupervisorMiddlewarePhase + (WebSocketPreflightAction)(0), // 4: openshell.middleware.v1.WebSocketPreflightAction + (Decision)(0), // 5: openshell.middleware.v1.Decision + (ExistingHeaderAction)(0), // 6: openshell.middleware.v1.ExistingHeaderAction + (*MiddlewareDescribeRequest)(nil), // 7: openshell.middleware.v1.MiddlewareDescribeRequest + (*MiddlewareManifest)(nil), // 8: openshell.middleware.v1.MiddlewareManifest + (*MiddlewareBinding)(nil), // 9: openshell.middleware.v1.MiddlewareBinding + (*ValidateConfigRequest)(nil), // 10: openshell.middleware.v1.ValidateConfigRequest + (*ValidateConfigResponse)(nil), // 11: openshell.middleware.v1.ValidateConfigResponse + (*HttpRequestEvaluation)(nil), // 12: openshell.middleware.v1.HttpRequestEvaluation + (*HttpHeader)(nil), // 13: openshell.middleware.v1.HttpHeader + (*HttpResponseEvent)(nil), // 14: openshell.middleware.v1.HttpResponseEvent + (*HttpResponseEventResult)(nil), // 15: openshell.middleware.v1.HttpResponseEventResult + (*HttpResponsePreflight)(nil), // 16: openshell.middleware.v1.HttpResponsePreflight + (*HttpResponsePreflightResult)(nil), // 17: openshell.middleware.v1.HttpResponsePreflightResult + (*HttpResponsePreflightSkip)(nil), // 18: openshell.middleware.v1.HttpResponsePreflightSkip + (*HttpResponseBlockDelivery)(nil), // 19: openshell.middleware.v1.HttpResponseBlockDelivery + (*HttpResponsePreflightInspect)(nil), // 20: openshell.middleware.v1.HttpResponsePreflightInspect + (*HttpResponseBodyUnit)(nil), // 21: openshell.middleware.v1.HttpResponseBodyUnit + (*HttpResponseBodyResult)(nil), // 22: openshell.middleware.v1.HttpResponseBodyResult + (*HttpResponseBodyPassThrough)(nil), // 23: openshell.middleware.v1.HttpResponseBodyPassThrough + (*HttpResponseBodySkipRemaining)(nil), // 24: openshell.middleware.v1.HttpResponseBodySkipRemaining + (*HttpResponseBodyTransform)(nil), // 25: openshell.middleware.v1.HttpResponseBodyTransform + (*HttpResponseTrailers)(nil), // 26: openshell.middleware.v1.HttpResponseTrailers + (*HttpResponseTrailersResult)(nil), // 27: openshell.middleware.v1.HttpResponseTrailersResult + (*MiddlewareSessionEnd)(nil), // 28: openshell.middleware.v1.MiddlewareSessionEnd + (*MiddlewareSessionProtocolError)(nil), // 29: openshell.middleware.v1.MiddlewareSessionProtocolError + (*WebSocketProtocolError)(nil), // 30: openshell.middleware.v1.WebSocketProtocolError + (*MiddlewareExchangeProtocolError)(nil), // 31: openshell.middleware.v1.MiddlewareExchangeProtocolError + (*WebSocketSessionEvent)(nil), // 32: openshell.middleware.v1.WebSocketSessionEvent + (*WebSocketPreflight)(nil), // 33: openshell.middleware.v1.WebSocketPreflight + (*WebSocketSessionStart)(nil), // 34: openshell.middleware.v1.WebSocketSessionStart + (*WebSocketMessage)(nil), // 35: openshell.middleware.v1.WebSocketMessage + (*WebSocketPreflightDecision)(nil), // 36: openshell.middleware.v1.WebSocketPreflightDecision + (*WebSocketMessageResult)(nil), // 37: openshell.middleware.v1.WebSocketMessageResult + (*WebSocketSessionEventResult)(nil), // 38: openshell.middleware.v1.WebSocketSessionEventResult + (*RequestContext)(nil), // 39: openshell.middleware.v1.RequestContext + (*HttpRequestTarget)(nil), // 40: openshell.middleware.v1.HttpRequestTarget + (*Process)(nil), // 41: openshell.middleware.v1.Process + (*Finding)(nil), // 42: openshell.middleware.v1.Finding + (*WriteHeader)(nil), // 43: openshell.middleware.v1.WriteHeader + (*RemoveHeader)(nil), // 44: openshell.middleware.v1.RemoveHeader + (*HeaderMutation)(nil), // 45: openshell.middleware.v1.HeaderMutation + (*HttpRequestResult)(nil), // 46: openshell.middleware.v1.HttpRequestResult + nil, // 47: openshell.middleware.v1.HttpResponsePreflightResult.MetadataEntry + nil, // 48: openshell.middleware.v1.HttpResponseBodyResult.MetadataEntry + nil, // 49: openshell.middleware.v1.HttpResponseTrailersResult.MetadataEntry + nil, // 50: openshell.middleware.v1.WebSocketPreflightDecision.MetadataEntry + nil, // 51: openshell.middleware.v1.WebSocketMessageResult.MetadataEntry + nil, // 52: openshell.middleware.v1.HttpRequestResult.MetadataEntry + (*extensionv1.PeerMetadata)(nil), // 53: openshell.extension.v1.PeerMetadata + (*durationpb.Duration)(nil), // 54: google.protobuf.Duration + (*structpb.Struct)(nil), // 55: google.protobuf.Struct +} +var file_supervisor_middleware_proto_depIdxs = []int32{ + 53, // 0: openshell.middleware.v1.MiddlewareDescribeRequest.gateway:type_name -> openshell.extension.v1.PeerMetadata + 9, // 1: openshell.middleware.v1.MiddlewareManifest.bindings:type_name -> openshell.middleware.v1.MiddlewareBinding + 53, // 2: openshell.middleware.v1.MiddlewareManifest.extension:type_name -> openshell.extension.v1.PeerMetadata + 2, // 3: openshell.middleware.v1.MiddlewareBinding.operation:type_name -> openshell.middleware.v1.SupervisorMiddlewareOperation + 3, // 4: openshell.middleware.v1.MiddlewareBinding.phase:type_name -> openshell.middleware.v1.SupervisorMiddlewarePhase + 54, // 5: openshell.middleware.v1.MiddlewareBinding.request_timeout:type_name -> google.protobuf.Duration + 55, // 6: openshell.middleware.v1.ValidateConfigRequest.config:type_name -> google.protobuf.Struct + 3, // 7: openshell.middleware.v1.HttpRequestEvaluation.phase:type_name -> openshell.middleware.v1.SupervisorMiddlewarePhase + 39, // 8: openshell.middleware.v1.HttpRequestEvaluation.context:type_name -> openshell.middleware.v1.RequestContext + 55, // 9: openshell.middleware.v1.HttpRequestEvaluation.config:type_name -> google.protobuf.Struct + 40, // 10: openshell.middleware.v1.HttpRequestEvaluation.target:type_name -> openshell.middleware.v1.HttpRequestTarget + 13, // 11: openshell.middleware.v1.HttpRequestEvaluation.headers:type_name -> openshell.middleware.v1.HttpHeader + 16, // 12: openshell.middleware.v1.HttpResponseEvent.preflight:type_name -> openshell.middleware.v1.HttpResponsePreflight + 21, // 13: openshell.middleware.v1.HttpResponseEvent.body:type_name -> openshell.middleware.v1.HttpResponseBodyUnit + 26, // 14: openshell.middleware.v1.HttpResponseEvent.trailers:type_name -> openshell.middleware.v1.HttpResponseTrailers + 28, // 15: openshell.middleware.v1.HttpResponseEvent.session_end:type_name -> openshell.middleware.v1.MiddlewareSessionEnd + 17, // 16: openshell.middleware.v1.HttpResponseEventResult.preflight_result:type_name -> openshell.middleware.v1.HttpResponsePreflightResult + 22, // 17: openshell.middleware.v1.HttpResponseEventResult.body_result:type_name -> openshell.middleware.v1.HttpResponseBodyResult + 27, // 18: openshell.middleware.v1.HttpResponseEventResult.trailers_result:type_name -> openshell.middleware.v1.HttpResponseTrailersResult + 39, // 19: openshell.middleware.v1.HttpResponsePreflight.context:type_name -> openshell.middleware.v1.RequestContext + 40, // 20: openshell.middleware.v1.HttpResponsePreflight.target:type_name -> openshell.middleware.v1.HttpRequestTarget + 13, // 21: openshell.middleware.v1.HttpResponsePreflight.headers:type_name -> openshell.middleware.v1.HttpHeader + 55, // 22: openshell.middleware.v1.HttpResponsePreflight.config:type_name -> google.protobuf.Struct + 0, // 23: openshell.middleware.v1.HttpResponsePreflight.permitted_body_modes:type_name -> openshell.middleware.v1.HttpResponseBodyMode + 18, // 24: openshell.middleware.v1.HttpResponsePreflightResult.skip:type_name -> openshell.middleware.v1.HttpResponsePreflightSkip + 20, // 25: openshell.middleware.v1.HttpResponsePreflightResult.inspect:type_name -> openshell.middleware.v1.HttpResponsePreflightInspect + 19, // 26: openshell.middleware.v1.HttpResponsePreflightResult.block_delivery:type_name -> openshell.middleware.v1.HttpResponseBlockDelivery + 42, // 27: openshell.middleware.v1.HttpResponsePreflightResult.findings:type_name -> openshell.middleware.v1.Finding + 47, // 28: openshell.middleware.v1.HttpResponsePreflightResult.metadata:type_name -> openshell.middleware.v1.HttpResponsePreflightResult.MetadataEntry + 0, // 29: openshell.middleware.v1.HttpResponsePreflightInspect.body_mode:type_name -> openshell.middleware.v1.HttpResponseBodyMode + 45, // 30: openshell.middleware.v1.HttpResponsePreflightInspect.header_mutations:type_name -> openshell.middleware.v1.HeaderMutation + 23, // 31: openshell.middleware.v1.HttpResponseBodyResult.pass_through:type_name -> openshell.middleware.v1.HttpResponseBodyPassThrough + 25, // 32: openshell.middleware.v1.HttpResponseBodyResult.transform:type_name -> openshell.middleware.v1.HttpResponseBodyTransform + 19, // 33: openshell.middleware.v1.HttpResponseBodyResult.block_delivery:type_name -> openshell.middleware.v1.HttpResponseBlockDelivery + 24, // 34: openshell.middleware.v1.HttpResponseBodyResult.skip_remaining:type_name -> openshell.middleware.v1.HttpResponseBodySkipRemaining + 42, // 35: openshell.middleware.v1.HttpResponseBodyResult.findings:type_name -> openshell.middleware.v1.Finding + 48, // 36: openshell.middleware.v1.HttpResponseBodyResult.metadata:type_name -> openshell.middleware.v1.HttpResponseBodyResult.MetadataEntry + 23, // 37: openshell.middleware.v1.HttpResponseBodySkipRemaining.pass_through:type_name -> openshell.middleware.v1.HttpResponseBodyPassThrough + 25, // 38: openshell.middleware.v1.HttpResponseBodySkipRemaining.transform:type_name -> openshell.middleware.v1.HttpResponseBodyTransform + 13, // 39: openshell.middleware.v1.HttpResponseTrailers.headers:type_name -> openshell.middleware.v1.HttpHeader + 45, // 40: openshell.middleware.v1.HttpResponseTrailersResult.trailer_mutations:type_name -> openshell.middleware.v1.HeaderMutation + 42, // 41: openshell.middleware.v1.HttpResponseTrailersResult.findings:type_name -> openshell.middleware.v1.Finding + 49, // 42: openshell.middleware.v1.HttpResponseTrailersResult.metadata:type_name -> openshell.middleware.v1.HttpResponseTrailersResult.MetadataEntry + 1, // 43: openshell.middleware.v1.MiddlewareSessionEnd.reason:type_name -> openshell.middleware.v1.MiddlewareSessionEndReason + 29, // 44: openshell.middleware.v1.MiddlewareSessionEnd.protocol_error:type_name -> openshell.middleware.v1.MiddlewareSessionProtocolError + 30, // 45: openshell.middleware.v1.MiddlewareSessionProtocolError.web_socket:type_name -> openshell.middleware.v1.WebSocketProtocolError + 31, // 46: openshell.middleware.v1.MiddlewareSessionProtocolError.middleware_exchange:type_name -> openshell.middleware.v1.MiddlewareExchangeProtocolError + 33, // 47: openshell.middleware.v1.WebSocketSessionEvent.preflight:type_name -> openshell.middleware.v1.WebSocketPreflight + 34, // 48: openshell.middleware.v1.WebSocketSessionEvent.session_start:type_name -> openshell.middleware.v1.WebSocketSessionStart + 35, // 49: openshell.middleware.v1.WebSocketSessionEvent.message:type_name -> openshell.middleware.v1.WebSocketMessage + 28, // 50: openshell.middleware.v1.WebSocketSessionEvent.session_end:type_name -> openshell.middleware.v1.MiddlewareSessionEnd + 3, // 51: openshell.middleware.v1.WebSocketPreflight.phase:type_name -> openshell.middleware.v1.SupervisorMiddlewarePhase + 39, // 52: openshell.middleware.v1.WebSocketPreflight.context:type_name -> openshell.middleware.v1.RequestContext + 40, // 53: openshell.middleware.v1.WebSocketPreflight.target:type_name -> openshell.middleware.v1.HttpRequestTarget + 55, // 54: openshell.middleware.v1.WebSocketPreflight.config:type_name -> google.protobuf.Struct + 4, // 55: openshell.middleware.v1.WebSocketPreflightDecision.action:type_name -> openshell.middleware.v1.WebSocketPreflightAction + 42, // 56: openshell.middleware.v1.WebSocketPreflightDecision.findings:type_name -> openshell.middleware.v1.Finding + 50, // 57: openshell.middleware.v1.WebSocketPreflightDecision.metadata:type_name -> openshell.middleware.v1.WebSocketPreflightDecision.MetadataEntry + 5, // 58: openshell.middleware.v1.WebSocketMessageResult.decision:type_name -> openshell.middleware.v1.Decision + 42, // 59: openshell.middleware.v1.WebSocketMessageResult.findings:type_name -> openshell.middleware.v1.Finding + 51, // 60: openshell.middleware.v1.WebSocketMessageResult.metadata:type_name -> openshell.middleware.v1.WebSocketMessageResult.MetadataEntry + 36, // 61: openshell.middleware.v1.WebSocketSessionEventResult.preflight_decision:type_name -> openshell.middleware.v1.WebSocketPreflightDecision + 37, // 62: openshell.middleware.v1.WebSocketSessionEventResult.message_result:type_name -> openshell.middleware.v1.WebSocketMessageResult + 41, // 63: openshell.middleware.v1.RequestContext.originating_process:type_name -> openshell.middleware.v1.Process + 6, // 64: openshell.middleware.v1.WriteHeader.on_existing:type_name -> openshell.middleware.v1.ExistingHeaderAction + 43, // 65: openshell.middleware.v1.HeaderMutation.write:type_name -> openshell.middleware.v1.WriteHeader + 44, // 66: openshell.middleware.v1.HeaderMutation.remove:type_name -> openshell.middleware.v1.RemoveHeader + 5, // 67: openshell.middleware.v1.HttpRequestResult.decision:type_name -> openshell.middleware.v1.Decision + 45, // 68: openshell.middleware.v1.HttpRequestResult.header_mutations:type_name -> openshell.middleware.v1.HeaderMutation + 42, // 69: openshell.middleware.v1.HttpRequestResult.findings:type_name -> openshell.middleware.v1.Finding + 52, // 70: openshell.middleware.v1.HttpRequestResult.metadata:type_name -> openshell.middleware.v1.HttpRequestResult.MetadataEntry + 7, // 71: openshell.middleware.v1.SupervisorMiddleware.Describe:input_type -> openshell.middleware.v1.MiddlewareDescribeRequest + 10, // 72: openshell.middleware.v1.SupervisorMiddleware.ValidateConfig:input_type -> openshell.middleware.v1.ValidateConfigRequest + 12, // 73: openshell.middleware.v1.SupervisorMiddleware.EvaluateHttpRequest:input_type -> openshell.middleware.v1.HttpRequestEvaluation + 32, // 74: openshell.middleware.v1.SupervisorMiddleware.EvaluateWebSocketSession:input_type -> openshell.middleware.v1.WebSocketSessionEvent + 14, // 75: openshell.middleware.v1.HttpResponsePreReturn.Evaluate:input_type -> openshell.middleware.v1.HttpResponseEvent + 8, // 76: openshell.middleware.v1.SupervisorMiddleware.Describe:output_type -> openshell.middleware.v1.MiddlewareManifest + 11, // 77: openshell.middleware.v1.SupervisorMiddleware.ValidateConfig:output_type -> openshell.middleware.v1.ValidateConfigResponse + 46, // 78: openshell.middleware.v1.SupervisorMiddleware.EvaluateHttpRequest:output_type -> openshell.middleware.v1.HttpRequestResult + 38, // 79: openshell.middleware.v1.SupervisorMiddleware.EvaluateWebSocketSession:output_type -> openshell.middleware.v1.WebSocketSessionEventResult + 15, // 80: openshell.middleware.v1.HttpResponsePreReturn.Evaluate:output_type -> openshell.middleware.v1.HttpResponseEventResult + 76, // [76:81] is the sub-list for method output_type + 71, // [71:76] is the sub-list for method input_type + 71, // [71:71] is the sub-list for extension type_name + 71, // [71:71] is the sub-list for extension extendee + 0, // [0:71] is the sub-list for field type_name +} + +func init() { file_supervisor_middleware_proto_init() } +func file_supervisor_middleware_proto_init() { + if File_supervisor_middleware_proto != nil { + return + } + file_supervisor_middleware_proto_msgTypes[7].OneofWrappers = []any{ + (*HttpResponseEvent_Preflight)(nil), + (*HttpResponseEvent_Body)(nil), + (*HttpResponseEvent_Trailers)(nil), + (*HttpResponseEvent_SessionEnd)(nil), + } + file_supervisor_middleware_proto_msgTypes[8].OneofWrappers = []any{ + (*HttpResponseEventResult_PreflightResult)(nil), + (*HttpResponseEventResult_BodyResult)(nil), + (*HttpResponseEventResult_TrailersResult)(nil), + } + file_supervisor_middleware_proto_msgTypes[10].OneofWrappers = []any{ + (*HttpResponsePreflightResult_Skip)(nil), + (*HttpResponsePreflightResult_Inspect)(nil), + (*HttpResponsePreflightResult_BlockDelivery)(nil), + } + file_supervisor_middleware_proto_msgTypes[14].OneofWrappers = []any{ + (*HttpResponseBodyUnit_Data)(nil), + } + file_supervisor_middleware_proto_msgTypes[15].OneofWrappers = []any{ + (*HttpResponseBodyResult_PassThrough)(nil), + (*HttpResponseBodyResult_Transform)(nil), + (*HttpResponseBodyResult_BlockDelivery)(nil), + (*HttpResponseBodyResult_SkipRemaining)(nil), + } + file_supervisor_middleware_proto_msgTypes[17].OneofWrappers = []any{ + (*HttpResponseBodySkipRemaining_PassThrough)(nil), + (*HttpResponseBodySkipRemaining_Transform)(nil), + } + file_supervisor_middleware_proto_msgTypes[18].OneofWrappers = []any{ + (*HttpResponseBodyTransform_Data)(nil), + } + file_supervisor_middleware_proto_msgTypes[22].OneofWrappers = []any{ + (*MiddlewareSessionProtocolError_WebSocket)(nil), + (*MiddlewareSessionProtocolError_MiddlewareExchange)(nil), + } + file_supervisor_middleware_proto_msgTypes[25].OneofWrappers = []any{ + (*WebSocketSessionEvent_Preflight)(nil), + (*WebSocketSessionEvent_SessionStart)(nil), + (*WebSocketSessionEvent_Message)(nil), + (*WebSocketSessionEvent_SessionEnd)(nil), + } + file_supervisor_middleware_proto_msgTypes[28].OneofWrappers = []any{ + (*WebSocketMessage_Text)(nil), + (*WebSocketMessage_Binary)(nil), + } + file_supervisor_middleware_proto_msgTypes[30].OneofWrappers = []any{ + (*WebSocketMessageResult_Text)(nil), + (*WebSocketMessageResult_Binary)(nil), + } + file_supervisor_middleware_proto_msgTypes[31].OneofWrappers = []any{ + (*WebSocketSessionEventResult_PreflightDecision)(nil), + (*WebSocketSessionEventResult_MessageResult)(nil), + } + file_supervisor_middleware_proto_msgTypes[38].OneofWrappers = []any{ + (*HeaderMutation_Write)(nil), + (*HeaderMutation_Remove)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_supervisor_middleware_proto_rawDesc), len(file_supervisor_middleware_proto_rawDesc)), + NumEnums: 7, + NumMessages: 46, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_supervisor_middleware_proto_goTypes, + DependencyIndexes: file_supervisor_middleware_proto_depIdxs, + EnumInfos: file_supervisor_middleware_proto_enumTypes, + MessageInfos: file_supervisor_middleware_proto_msgTypes, + }.Build() + File_supervisor_middleware_proto = out.File + file_supervisor_middleware_proto_goTypes = nil + file_supervisor_middleware_proto_depIdxs = nil +} diff --git a/gen/openshell/middlewarev1/supervisor_middleware_grpc.pb.go b/gen/openshell/middlewarev1/supervisor_middleware_grpc.pb.go new file mode 100644 index 0000000..d5bec61 --- /dev/null +++ b/gen/openshell/middlewarev1/supervisor_middleware_grpc.pb.go @@ -0,0 +1,371 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: supervisor_middleware.proto + +package middlewarev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SupervisorMiddleware_Describe_FullMethodName = "/openshell.middleware.v1.SupervisorMiddleware/Describe" + SupervisorMiddleware_ValidateConfig_FullMethodName = "/openshell.middleware.v1.SupervisorMiddleware/ValidateConfig" + SupervisorMiddleware_EvaluateHttpRequest_FullMethodName = "/openshell.middleware.v1.SupervisorMiddleware/EvaluateHttpRequest" + SupervisorMiddleware_EvaluateWebSocketSession_FullMethodName = "/openshell.middleware.v1.SupervisorMiddleware/EvaluateWebSocketSession" +) + +// SupervisorMiddlewareClient is the client API for SupervisorMiddleware service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// SupervisorMiddleware discovers and configures one operator-run middleware. +// It evaluates HTTP requests and WebSocket messages before credentials. +// Phase-specific services share the same registration. +type SupervisorMiddlewareClient interface { + // Describe returns the service manifest and declared bindings. + Describe(ctx context.Context, in *MiddlewareDescribeRequest, opts ...grpc.CallOption) (*MiddlewareManifest, error) + // ValidateConfig checks service-specific configuration for one binding. + ValidateConfig(ctx context.Context, in *ValidateConfigRequest, opts ...grpc.CallOption) (*ValidateConfigResponse, error) + // EvaluateHttpRequest returns an allow, deny, or mutation decision for one + // buffered HTTP request. + EvaluateHttpRequest(ctx context.Context, in *HttpRequestEvaluation, opts ...grpc.CallOption) (*HttpRequestResult, error) + // EvaluateWebSocketSession opens one ordered, phase-specific stream for a + // single middleware stage and WebSocket upgrade attempt. The current + // implementation supports client-to-upstream text messages at + // PRE_CREDENTIALS; PRE_RETURN is reserved for upstream-to-client messages. + // A request may go unanswered when the session terminates. For every opened + // stage stream, OpenShell attempts at most one session_end before closing the + // stream when its transport is still writable. + EvaluateWebSocketSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[WebSocketSessionEvent, WebSocketSessionEventResult], error) +} + +type supervisorMiddlewareClient struct { + cc grpc.ClientConnInterface +} + +func NewSupervisorMiddlewareClient(cc grpc.ClientConnInterface) SupervisorMiddlewareClient { + return &supervisorMiddlewareClient{cc} +} + +func (c *supervisorMiddlewareClient) Describe(ctx context.Context, in *MiddlewareDescribeRequest, opts ...grpc.CallOption) (*MiddlewareManifest, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MiddlewareManifest) + err := c.cc.Invoke(ctx, SupervisorMiddleware_Describe_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *supervisorMiddlewareClient) ValidateConfig(ctx context.Context, in *ValidateConfigRequest, opts ...grpc.CallOption) (*ValidateConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateConfigResponse) + err := c.cc.Invoke(ctx, SupervisorMiddleware_ValidateConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *supervisorMiddlewareClient) EvaluateHttpRequest(ctx context.Context, in *HttpRequestEvaluation, opts ...grpc.CallOption) (*HttpRequestResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HttpRequestResult) + err := c.cc.Invoke(ctx, SupervisorMiddleware_EvaluateHttpRequest_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *supervisorMiddlewareClient) EvaluateWebSocketSession(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[WebSocketSessionEvent, WebSocketSessionEventResult], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &SupervisorMiddleware_ServiceDesc.Streams[0], SupervisorMiddleware_EvaluateWebSocketSession_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WebSocketSessionEvent, WebSocketSessionEventResult]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SupervisorMiddleware_EvaluateWebSocketSessionClient = grpc.BidiStreamingClient[WebSocketSessionEvent, WebSocketSessionEventResult] + +// SupervisorMiddlewareServer is the server API for SupervisorMiddleware service. +// All implementations must embed UnimplementedSupervisorMiddlewareServer +// for forward compatibility. +// +// SupervisorMiddleware discovers and configures one operator-run middleware. +// It evaluates HTTP requests and WebSocket messages before credentials. +// Phase-specific services share the same registration. +type SupervisorMiddlewareServer interface { + // Describe returns the service manifest and declared bindings. + Describe(context.Context, *MiddlewareDescribeRequest) (*MiddlewareManifest, error) + // ValidateConfig checks service-specific configuration for one binding. + ValidateConfig(context.Context, *ValidateConfigRequest) (*ValidateConfigResponse, error) + // EvaluateHttpRequest returns an allow, deny, or mutation decision for one + // buffered HTTP request. + EvaluateHttpRequest(context.Context, *HttpRequestEvaluation) (*HttpRequestResult, error) + // EvaluateWebSocketSession opens one ordered, phase-specific stream for a + // single middleware stage and WebSocket upgrade attempt. The current + // implementation supports client-to-upstream text messages at + // PRE_CREDENTIALS; PRE_RETURN is reserved for upstream-to-client messages. + // A request may go unanswered when the session terminates. For every opened + // stage stream, OpenShell attempts at most one session_end before closing the + // stream when its transport is still writable. + EvaluateWebSocketSession(grpc.BidiStreamingServer[WebSocketSessionEvent, WebSocketSessionEventResult]) error + mustEmbedUnimplementedSupervisorMiddlewareServer() +} + +// UnimplementedSupervisorMiddlewareServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSupervisorMiddlewareServer struct{} + +func (UnimplementedSupervisorMiddlewareServer) Describe(context.Context, *MiddlewareDescribeRequest) (*MiddlewareManifest, error) { + return nil, status.Error(codes.Unimplemented, "method Describe not implemented") +} +func (UnimplementedSupervisorMiddlewareServer) ValidateConfig(context.Context, *ValidateConfigRequest) (*ValidateConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ValidateConfig not implemented") +} +func (UnimplementedSupervisorMiddlewareServer) EvaluateHttpRequest(context.Context, *HttpRequestEvaluation) (*HttpRequestResult, error) { + return nil, status.Error(codes.Unimplemented, "method EvaluateHttpRequest not implemented") +} +func (UnimplementedSupervisorMiddlewareServer) EvaluateWebSocketSession(grpc.BidiStreamingServer[WebSocketSessionEvent, WebSocketSessionEventResult]) error { + return status.Error(codes.Unimplemented, "method EvaluateWebSocketSession not implemented") +} +func (UnimplementedSupervisorMiddlewareServer) mustEmbedUnimplementedSupervisorMiddlewareServer() {} +func (UnimplementedSupervisorMiddlewareServer) testEmbeddedByValue() {} + +// UnsafeSupervisorMiddlewareServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SupervisorMiddlewareServer will +// result in compilation errors. +type UnsafeSupervisorMiddlewareServer interface { + mustEmbedUnimplementedSupervisorMiddlewareServer() +} + +func RegisterSupervisorMiddlewareServer(s grpc.ServiceRegistrar, srv SupervisorMiddlewareServer) { + // If the following call panics, it indicates UnimplementedSupervisorMiddlewareServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SupervisorMiddleware_ServiceDesc, srv) +} + +func _SupervisorMiddleware_Describe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MiddlewareDescribeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SupervisorMiddlewareServer).Describe(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SupervisorMiddleware_Describe_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SupervisorMiddlewareServer).Describe(ctx, req.(*MiddlewareDescribeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SupervisorMiddleware_ValidateConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SupervisorMiddlewareServer).ValidateConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SupervisorMiddleware_ValidateConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SupervisorMiddlewareServer).ValidateConfig(ctx, req.(*ValidateConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SupervisorMiddleware_EvaluateHttpRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HttpRequestEvaluation) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SupervisorMiddlewareServer).EvaluateHttpRequest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SupervisorMiddleware_EvaluateHttpRequest_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SupervisorMiddlewareServer).EvaluateHttpRequest(ctx, req.(*HttpRequestEvaluation)) + } + return interceptor(ctx, in, info, handler) +} + +func _SupervisorMiddleware_EvaluateWebSocketSession_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(SupervisorMiddlewareServer).EvaluateWebSocketSession(&grpc.GenericServerStream[WebSocketSessionEvent, WebSocketSessionEventResult]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type SupervisorMiddleware_EvaluateWebSocketSessionServer = grpc.BidiStreamingServer[WebSocketSessionEvent, WebSocketSessionEventResult] + +// SupervisorMiddleware_ServiceDesc is the grpc.ServiceDesc for SupervisorMiddleware service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SupervisorMiddleware_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "openshell.middleware.v1.SupervisorMiddleware", + HandlerType: (*SupervisorMiddlewareServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Describe", + Handler: _SupervisorMiddleware_Describe_Handler, + }, + { + MethodName: "ValidateConfig", + Handler: _SupervisorMiddleware_ValidateConfig_Handler, + }, + { + MethodName: "EvaluateHttpRequest", + Handler: _SupervisorMiddleware_EvaluateHttpRequest_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "EvaluateWebSocketSession", + Handler: _SupervisorMiddleware_EvaluateWebSocketSession_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "supervisor_middleware.proto", +} + +const ( + HttpResponsePreReturn_Evaluate_FullMethodName = "/openshell.middleware.v1.HttpResponsePreReturn/Evaluate" +) + +// HttpResponsePreReturnClient is the client API for HttpResponsePreReturn service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// HttpResponsePreReturn evaluates one response for one middleware stage before +// OpenShell returns it to the sandbox. +type HttpResponsePreReturnClient interface { + // Evaluate starts with preflight and may continue with selected body units + // and trailers. A body unit marked end_of_stream ends body inspection, not + // the event stream. Trailers and one best-effort session_end may follow. + Evaluate(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HttpResponseEvent, HttpResponseEventResult], error) +} + +type httpResponsePreReturnClient struct { + cc grpc.ClientConnInterface +} + +func NewHttpResponsePreReturnClient(cc grpc.ClientConnInterface) HttpResponsePreReturnClient { + return &httpResponsePreReturnClient{cc} +} + +func (c *httpResponsePreReturnClient) Evaluate(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[HttpResponseEvent, HttpResponseEventResult], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &HttpResponsePreReturn_ServiceDesc.Streams[0], HttpResponsePreReturn_Evaluate_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[HttpResponseEvent, HttpResponseEventResult]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type HttpResponsePreReturn_EvaluateClient = grpc.BidiStreamingClient[HttpResponseEvent, HttpResponseEventResult] + +// HttpResponsePreReturnServer is the server API for HttpResponsePreReturn service. +// All implementations must embed UnimplementedHttpResponsePreReturnServer +// for forward compatibility. +// +// HttpResponsePreReturn evaluates one response for one middleware stage before +// OpenShell returns it to the sandbox. +type HttpResponsePreReturnServer interface { + // Evaluate starts with preflight and may continue with selected body units + // and trailers. A body unit marked end_of_stream ends body inspection, not + // the event stream. Trailers and one best-effort session_end may follow. + Evaluate(grpc.BidiStreamingServer[HttpResponseEvent, HttpResponseEventResult]) error + mustEmbedUnimplementedHttpResponsePreReturnServer() +} + +// UnimplementedHttpResponsePreReturnServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHttpResponsePreReturnServer struct{} + +func (UnimplementedHttpResponsePreReturnServer) Evaluate(grpc.BidiStreamingServer[HttpResponseEvent, HttpResponseEventResult]) error { + return status.Error(codes.Unimplemented, "method Evaluate not implemented") +} +func (UnimplementedHttpResponsePreReturnServer) mustEmbedUnimplementedHttpResponsePreReturnServer() {} +func (UnimplementedHttpResponsePreReturnServer) testEmbeddedByValue() {} + +// UnsafeHttpResponsePreReturnServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HttpResponsePreReturnServer will +// result in compilation errors. +type UnsafeHttpResponsePreReturnServer interface { + mustEmbedUnimplementedHttpResponsePreReturnServer() +} + +func RegisterHttpResponsePreReturnServer(s grpc.ServiceRegistrar, srv HttpResponsePreReturnServer) { + // If the following call panics, it indicates UnimplementedHttpResponsePreReturnServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HttpResponsePreReturn_ServiceDesc, srv) +} + +func _HttpResponsePreReturn_Evaluate_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(HttpResponsePreReturnServer).Evaluate(&grpc.GenericServerStream[HttpResponseEvent, HttpResponseEventResult]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type HttpResponsePreReturn_EvaluateServer = grpc.BidiStreamingServer[HttpResponseEvent, HttpResponseEventResult] + +// HttpResponsePreReturn_ServiceDesc is the grpc.ServiceDesc for HttpResponsePreReturn service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HttpResponsePreReturn_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "openshell.middleware.v1.HttpResponsePreReturn", + HandlerType: (*HttpResponsePreReturnServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Evaluate", + Handler: _HttpResponsePreReturn_Evaluate_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "supervisor_middleware.proto", +} diff --git a/gen/openshell/openshellv1/openshell.pb.go b/gen/openshell/openshellv1/openshell.pb.go new file mode 100644 index 0000000..c11f63e --- /dev/null +++ b/gen/openshell/openshellv1/openshell.pb.go @@ -0,0 +1,20839 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: openshell.proto + +package openshellv1 + +import ( + datamodelv1 "github.com/agentnameservice/ans-openshell/gen/openshell/datamodelv1" + _ "github.com/agentnameservice/ans-openshell/gen/openshell/optionsv1" + sandboxv1 "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ExtensionKind int32 + +const ( + ExtensionKind_EXTENSION_KIND_UNSPECIFIED ExtensionKind = 0 + ExtensionKind_EXTENSION_KIND_COMPUTE_DRIVER ExtensionKind = 1 + ExtensionKind_EXTENSION_KIND_CREDENTIAL_DRIVER ExtensionKind = 2 + ExtensionKind_EXTENSION_KIND_GATEWAY_INTERCEPTOR ExtensionKind = 3 + ExtensionKind_EXTENSION_KIND_SUPERVISOR_MIDDLEWARE ExtensionKind = 4 +) + +// Enum value maps for ExtensionKind. +var ( + ExtensionKind_name = map[int32]string{ + 0: "EXTENSION_KIND_UNSPECIFIED", + 1: "EXTENSION_KIND_COMPUTE_DRIVER", + 2: "EXTENSION_KIND_CREDENTIAL_DRIVER", + 3: "EXTENSION_KIND_GATEWAY_INTERCEPTOR", + 4: "EXTENSION_KIND_SUPERVISOR_MIDDLEWARE", + } + ExtensionKind_value = map[string]int32{ + "EXTENSION_KIND_UNSPECIFIED": 0, + "EXTENSION_KIND_COMPUTE_DRIVER": 1, + "EXTENSION_KIND_CREDENTIAL_DRIVER": 2, + "EXTENSION_KIND_GATEWAY_INTERCEPTOR": 3, + "EXTENSION_KIND_SUPERVISOR_MIDDLEWARE": 4, + } +) + +func (x ExtensionKind) Enum() *ExtensionKind { + p := new(ExtensionKind) + *p = x + return p +} + +func (x ExtensionKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExtensionKind) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[0].Descriptor() +} + +func (ExtensionKind) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[0] +} + +func (x ExtensionKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExtensionKind.Descriptor instead. +func (ExtensionKind) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{0} +} + +// High-level sandbox lifecycle phase derived by the gateway. +// +// Clients should rely on this normalized lifecycle summary for readiness and +// deletion decisions instead of interpreting raw conditions. +type SandboxPhase int32 + +const ( + SandboxPhase_SANDBOX_PHASE_UNSPECIFIED SandboxPhase = 0 + SandboxPhase_SANDBOX_PHASE_PROVISIONING SandboxPhase = 1 + SandboxPhase_SANDBOX_PHASE_READY SandboxPhase = 2 + SandboxPhase_SANDBOX_PHASE_ERROR SandboxPhase = 3 + SandboxPhase_SANDBOX_PHASE_DELETING SandboxPhase = 4 + SandboxPhase_SANDBOX_PHASE_UNKNOWN SandboxPhase = 5 + SandboxPhase_SANDBOX_PHASE_STOPPING SandboxPhase = 6 + SandboxPhase_SANDBOX_PHASE_STOPPED SandboxPhase = 7 + SandboxPhase_SANDBOX_PHASE_STARTING SandboxPhase = 8 + // The canonical main process exited successfully and its result is final. + SandboxPhase_SANDBOX_PHASE_COMPLETED SandboxPhase = 9 +) + +// Enum value maps for SandboxPhase. +var ( + SandboxPhase_name = map[int32]string{ + 0: "SANDBOX_PHASE_UNSPECIFIED", + 1: "SANDBOX_PHASE_PROVISIONING", + 2: "SANDBOX_PHASE_READY", + 3: "SANDBOX_PHASE_ERROR", + 4: "SANDBOX_PHASE_DELETING", + 5: "SANDBOX_PHASE_UNKNOWN", + 6: "SANDBOX_PHASE_STOPPING", + 7: "SANDBOX_PHASE_STOPPED", + 8: "SANDBOX_PHASE_STARTING", + 9: "SANDBOX_PHASE_COMPLETED", + } + SandboxPhase_value = map[string]int32{ + "SANDBOX_PHASE_UNSPECIFIED": 0, + "SANDBOX_PHASE_PROVISIONING": 1, + "SANDBOX_PHASE_READY": 2, + "SANDBOX_PHASE_ERROR": 3, + "SANDBOX_PHASE_DELETING": 4, + "SANDBOX_PHASE_UNKNOWN": 5, + "SANDBOX_PHASE_STOPPING": 6, + "SANDBOX_PHASE_STOPPED": 7, + "SANDBOX_PHASE_STARTING": 8, + "SANDBOX_PHASE_COMPLETED": 9, + } +) + +func (x SandboxPhase) Enum() *SandboxPhase { + p := new(SandboxPhase) + *p = x + return p +} + +func (x SandboxPhase) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SandboxPhase) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[1].Descriptor() +} + +func (SandboxPhase) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[1] +} + +func (x SandboxPhase) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SandboxPhase.Descriptor instead. +func (SandboxPhase) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{1} +} + +// Operation whose installed authority is tracked by a receipt. +type ProviderMutationKind int32 + +const ( + ProviderMutationKind_PROVIDER_MUTATION_KIND_UNSPECIFIED ProviderMutationKind = 0 + ProviderMutationKind_PROVIDER_MUTATION_KIND_ATTACH ProviderMutationKind = 1 + ProviderMutationKind_PROVIDER_MUTATION_KIND_DETACH ProviderMutationKind = 2 + ProviderMutationKind_PROVIDER_MUTATION_KIND_UPDATE ProviderMutationKind = 3 + // Reconstructed status for existing desired state without a mutation receipt. + ProviderMutationKind_PROVIDER_MUTATION_KIND_OBSERVE ProviderMutationKind = 4 +) + +// Enum value maps for ProviderMutationKind. +var ( + ProviderMutationKind_name = map[int32]string{ + 0: "PROVIDER_MUTATION_KIND_UNSPECIFIED", + 1: "PROVIDER_MUTATION_KIND_ATTACH", + 2: "PROVIDER_MUTATION_KIND_DETACH", + 3: "PROVIDER_MUTATION_KIND_UPDATE", + 4: "PROVIDER_MUTATION_KIND_OBSERVE", + } + ProviderMutationKind_value = map[string]int32{ + "PROVIDER_MUTATION_KIND_UNSPECIFIED": 0, + "PROVIDER_MUTATION_KIND_ATTACH": 1, + "PROVIDER_MUTATION_KIND_DETACH": 2, + "PROVIDER_MUTATION_KIND_UPDATE": 3, + "PROVIDER_MUTATION_KIND_OBSERVE": 4, + } +) + +func (x ProviderMutationKind) Enum() *ProviderMutationKind { + p := new(ProviderMutationKind) + *p = x + return p +} + +func (x ProviderMutationKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderMutationKind) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[2].Descriptor() +} + +func (ProviderMutationKind) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[2] +} + +func (x ProviderMutationKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderMutationKind.Descriptor instead. +func (ProviderMutationKind) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{2} +} + +// Readiness states describe persisted intent separately from installed state. +type ProviderReadinessState int32 + +const ( + ProviderReadinessState_PROVIDER_READINESS_STATE_UNSPECIFIED ProviderReadinessState = 0 + ProviderReadinessState_PROVIDER_READINESS_STATE_PERSISTED ProviderReadinessState = 1 + ProviderReadinessState_PROVIDER_READINESS_STATE_PENDING ProviderReadinessState = 2 + ProviderReadinessState_PROVIDER_READINESS_STATE_READY ProviderReadinessState = 3 + ProviderReadinessState_PROVIDER_READINESS_STATE_WITHHELD ProviderReadinessState = 4 + ProviderReadinessState_PROVIDER_READINESS_STATE_REVOKED ProviderReadinessState = 5 + ProviderReadinessState_PROVIDER_READINESS_STATE_FAILED ProviderReadinessState = 6 + ProviderReadinessState_PROVIDER_READINESS_STATE_SUPERSEDED ProviderReadinessState = 7 +) + +// Enum value maps for ProviderReadinessState. +var ( + ProviderReadinessState_name = map[int32]string{ + 0: "PROVIDER_READINESS_STATE_UNSPECIFIED", + 1: "PROVIDER_READINESS_STATE_PERSISTED", + 2: "PROVIDER_READINESS_STATE_PENDING", + 3: "PROVIDER_READINESS_STATE_READY", + 4: "PROVIDER_READINESS_STATE_WITHHELD", + 5: "PROVIDER_READINESS_STATE_REVOKED", + 6: "PROVIDER_READINESS_STATE_FAILED", + 7: "PROVIDER_READINESS_STATE_SUPERSEDED", + } + ProviderReadinessState_value = map[string]int32{ + "PROVIDER_READINESS_STATE_UNSPECIFIED": 0, + "PROVIDER_READINESS_STATE_PERSISTED": 1, + "PROVIDER_READINESS_STATE_PENDING": 2, + "PROVIDER_READINESS_STATE_READY": 3, + "PROVIDER_READINESS_STATE_WITHHELD": 4, + "PROVIDER_READINESS_STATE_REVOKED": 5, + "PROVIDER_READINESS_STATE_FAILED": 6, + "PROVIDER_READINESS_STATE_SUPERSEDED": 7, + } +) + +func (x ProviderReadinessState) Enum() *ProviderReadinessState { + p := new(ProviderReadinessState) + *p = x + return p +} + +func (x ProviderReadinessState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderReadinessState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[3].Descriptor() +} + +func (ProviderReadinessState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[3] +} + +func (x ProviderReadinessState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderReadinessState.Descriptor instead. +func (ProviderReadinessState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{3} +} + +// Closed reason categories are safe to display. Raw installation errors are +// never part of the readiness protocol. +type ProviderReadinessReason int32 + +const ( + ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED ProviderReadinessReason = 0 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR ProviderReadinessReason = 1 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS ProviderReadinessReason = 2 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_POLICY ProviderReadinessReason = 3 + ProviderReadinessReason_PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS ProviderReadinessReason = 4 + ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR ProviderReadinessReason = 5 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD ProviderReadinessReason = 6 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED ProviderReadinessReason = 7 + ProviderReadinessReason_PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED ProviderReadinessReason = 8 + ProviderReadinessReason_PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED ProviderReadinessReason = 9 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED ProviderReadinessReason = 10 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED ProviderReadinessReason = 11 + ProviderReadinessReason_PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED ProviderReadinessReason = 12 + ProviderReadinessReason_PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED ProviderReadinessReason = 13 + ProviderReadinessReason_PROVIDER_READINESS_REASON_LOCAL_POLICY ProviderReadinessReason = 14 + ProviderReadinessReason_PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH ProviderReadinessReason = 15 +) + +// Enum value maps for ProviderReadinessReason. +var ( + ProviderReadinessReason_name = map[int32]string{ + 0: "PROVIDER_READINESS_REASON_UNSPECIFIED", + 1: "PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR", + 2: "PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS", + 3: "PROVIDER_READINESS_REASON_WAITING_FOR_POLICY", + 4: "PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS", + 5: "PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR", + 6: "PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD", + 7: "PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED", + 8: "PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED", + 9: "PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED", + 10: "PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED", + 11: "PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED", + 12: "PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED", + 13: "PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED", + 14: "PROVIDER_READINESS_REASON_LOCAL_POLICY", + 15: "PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH", + } + ProviderReadinessReason_value = map[string]int32{ + "PROVIDER_READINESS_REASON_UNSPECIFIED": 0, + "PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR": 1, + "PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS": 2, + "PROVIDER_READINESS_REASON_WAITING_FOR_POLICY": 3, + "PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS": 4, + "PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR": 5, + "PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD": 6, + "PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED": 7, + "PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED": 8, + "PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED": 9, + "PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED": 10, + "PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED": 11, + "PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED": 12, + "PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED": 13, + "PROVIDER_READINESS_REASON_LOCAL_POLICY": 14, + "PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH": 15, + } +) + +func (x ProviderReadinessReason) Enum() *ProviderReadinessReason { + p := new(ProviderReadinessReason) + *p = x + return p +} + +func (x ProviderReadinessReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderReadinessReason) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[4].Descriptor() +} + +func (ProviderReadinessReason) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[4] +} + +func (x ProviderReadinessReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderReadinessReason.Descriptor instead. +func (ProviderReadinessReason) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{4} +} + +// Component whose desired state is tracked by a durable update operation. +type ConfigComponent int32 + +const ( + ConfigComponent_CONFIG_COMPONENT_UNSPECIFIED ConfigComponent = 0 + ConfigComponent_CONFIG_COMPONENT_SANDBOX_CONFIG ConfigComponent = 1 + ConfigComponent_CONFIG_COMPONENT_PROVIDER_ENVIRONMENT ConfigComponent = 2 +) + +// Enum value maps for ConfigComponent. +var ( + ConfigComponent_name = map[int32]string{ + 0: "CONFIG_COMPONENT_UNSPECIFIED", + 1: "CONFIG_COMPONENT_SANDBOX_CONFIG", + 2: "CONFIG_COMPONENT_PROVIDER_ENVIRONMENT", + } + ConfigComponent_value = map[string]int32{ + "CONFIG_COMPONENT_UNSPECIFIED": 0, + "CONFIG_COMPONENT_SANDBOX_CONFIG": 1, + "CONFIG_COMPONENT_PROVIDER_ENVIRONMENT": 2, + } +) + +func (x ConfigComponent) Enum() *ConfigComponent { + p := new(ConfigComponent) + *p = x + return p +} + +func (x ConfigComponent) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigComponent) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[5].Descriptor() +} + +func (ConfigComponent) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[5] +} + +func (x ConfigComponent) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigComponent.Descriptor instead. +func (ConfigComponent) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{5} +} + +// Result of applying a component revision at its owning runtime boundary. +type ConfigApplyOutcome int32 + +const ( + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSPECIFIED ConfigApplyOutcome = 0 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_APPLIED ConfigApplyOutcome = 1 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE ConfigApplyOutcome = 2 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_IGNORED_STALE ConfigApplyOutcome = 3 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE ConfigApplyOutcome = 4 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_DEGRADED ConfigApplyOutcome = 5 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD ConfigApplyOutcome = 6 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_FAILED_CLOSED ConfigApplyOutcome = 7 + ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSUPPORTED ConfigApplyOutcome = 8 +) + +// Enum value maps for ConfigApplyOutcome. +var ( + ConfigApplyOutcome_name = map[int32]string{ + 0: "CONFIG_APPLY_OUTCOME_UNSPECIFIED", + 1: "CONFIG_APPLY_OUTCOME_APPLIED", + 2: "CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE", + 3: "CONFIG_APPLY_OUTCOME_IGNORED_STALE", + 4: "CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE", + 5: "CONFIG_APPLY_OUTCOME_DEGRADED", + 6: "CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD", + 7: "CONFIG_APPLY_OUTCOME_FAILED_CLOSED", + 8: "CONFIG_APPLY_OUTCOME_UNSUPPORTED", + } + ConfigApplyOutcome_value = map[string]int32{ + "CONFIG_APPLY_OUTCOME_UNSPECIFIED": 0, + "CONFIG_APPLY_OUTCOME_APPLIED": 1, + "CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE": 2, + "CONFIG_APPLY_OUTCOME_IGNORED_STALE": 3, + "CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE": 4, + "CONFIG_APPLY_OUTCOME_DEGRADED": 5, + "CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD": 6, + "CONFIG_APPLY_OUTCOME_FAILED_CLOSED": 7, + "CONFIG_APPLY_OUTCOME_UNSUPPORTED": 8, + } +) + +func (x ConfigApplyOutcome) Enum() *ConfigApplyOutcome { + p := new(ConfigApplyOutcome) + *p = x + return p +} + +func (x ConfigApplyOutcome) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigApplyOutcome) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[6].Descriptor() +} + +func (ConfigApplyOutcome) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[6] +} + +func (x ConfigApplyOutcome) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigApplyOutcome.Descriptor instead. +func (ConfigApplyOutcome) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{6} +} + +// Durable lifecycle of one desired-state update operation. +type ConfigUpdateOperationState int32 + +const ( + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED ConfigUpdateOperationState = 0 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_PENDING ConfigUpdateOperationState = 1 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_APPLIED ConfigUpdateOperationState = 2 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_INACTIVE ConfigUpdateOperationState = 3 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_FAILED ConfigUpdateOperationState = 4 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED ConfigUpdateOperationState = 5 + ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_CANCELLED ConfigUpdateOperationState = 6 +) + +// Enum value maps for ConfigUpdateOperationState. +var ( + ConfigUpdateOperationState_name = map[int32]string{ + 0: "CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED", + 1: "CONFIG_UPDATE_OPERATION_STATE_PENDING", + 2: "CONFIG_UPDATE_OPERATION_STATE_APPLIED", + 3: "CONFIG_UPDATE_OPERATION_STATE_INACTIVE", + 4: "CONFIG_UPDATE_OPERATION_STATE_FAILED", + 5: "CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED", + 6: "CONFIG_UPDATE_OPERATION_STATE_CANCELLED", + } + ConfigUpdateOperationState_value = map[string]int32{ + "CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED": 0, + "CONFIG_UPDATE_OPERATION_STATE_PENDING": 1, + "CONFIG_UPDATE_OPERATION_STATE_APPLIED": 2, + "CONFIG_UPDATE_OPERATION_STATE_INACTIVE": 3, + "CONFIG_UPDATE_OPERATION_STATE_FAILED": 4, + "CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED": 5, + "CONFIG_UPDATE_OPERATION_STATE_CANCELLED": 6, + } +) + +func (x ConfigUpdateOperationState) Enum() *ConfigUpdateOperationState { + p := new(ConfigUpdateOperationState) + *p = x + return p +} + +func (x ConfigUpdateOperationState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigUpdateOperationState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[7].Descriptor() +} + +func (ConfigUpdateOperationState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[7] +} + +func (x ConfigUpdateOperationState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigUpdateOperationState.Descriptor instead. +func (ConfigUpdateOperationState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{7} +} + +// Provider credential token grant configuration. +// When present, the credential is obtained dynamically via OAuth2 grant when needed. +type ProviderCredentialTokenGrantType int32 + +const ( + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED ProviderCredentialTokenGrantType = 0 + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS ProviderCredentialTokenGrantType = 1 + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE ProviderCredentialTokenGrantType = 2 +) + +// Enum value maps for ProviderCredentialTokenGrantType. +var ( + ProviderCredentialTokenGrantType_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS", + 2: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE", + } + ProviderCredentialTokenGrantType_value = map[string]int32{ + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS": 1, + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE": 2, + } +) + +func (x ProviderCredentialTokenGrantType) Enum() *ProviderCredentialTokenGrantType { + p := new(ProviderCredentialTokenGrantType) + *p = x + return p +} + +func (x ProviderCredentialTokenGrantType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialTokenGrantType) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[8].Descriptor() +} + +func (ProviderCredentialTokenGrantType) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[8] +} + +func (x ProviderCredentialTokenGrantType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialTokenGrantType.Descriptor instead. +func (ProviderCredentialTokenGrantType) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{8} +} + +type ProviderCredentialRefreshStrategy int32 + +const ( + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED ProviderCredentialRefreshStrategy = 0 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC ProviderCredentialRefreshStrategy = 1 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_EXTERNAL ProviderCredentialRefreshStrategy = 2 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN ProviderCredentialRefreshStrategy = 3 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_CLIENT_CREDENTIALS ProviderCredentialRefreshStrategy = 4 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_GOOGLE_SERVICE_ACCOUNT_JWT ProviderCredentialRefreshStrategy = 5 + ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_AWS_STS_ASSUME_ROLE ProviderCredentialRefreshStrategy = 6 +) + +// Enum value maps for ProviderCredentialRefreshStrategy. +var ( + ProviderCredentialRefreshStrategy_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC", + 2: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_EXTERNAL", + 3: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN", + 4: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_CLIENT_CREDENTIALS", + 5: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_GOOGLE_SERVICE_ACCOUNT_JWT", + 6: "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_AWS_STS_ASSUME_ROLE", + } + ProviderCredentialRefreshStrategy_value = map[string]int32{ + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC": 1, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_EXTERNAL": 2, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN": 3, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_CLIENT_CREDENTIALS": 4, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_GOOGLE_SERVICE_ACCOUNT_JWT": 5, + "PROVIDER_CREDENTIAL_REFRESH_STRATEGY_AWS_STS_ASSUME_ROLE": 6, + } +) + +func (x ProviderCredentialRefreshStrategy) Enum() *ProviderCredentialRefreshStrategy { + p := new(ProviderCredentialRefreshStrategy) + *p = x + return p +} + +func (x ProviderCredentialRefreshStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialRefreshStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[9].Descriptor() +} + +func (ProviderCredentialRefreshStrategy) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[9] +} + +func (x ProviderCredentialRefreshStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialRefreshStrategy.Descriptor instead. +func (ProviderCredentialRefreshStrategy) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{9} +} + +// Stable provider profile categories used by clients for grouping and filtering. +type ProviderProfileCategory int32 + +const ( + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_UNSPECIFIED ProviderProfileCategory = 0 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_OTHER ProviderProfileCategory = 1 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_INFERENCE ProviderProfileCategory = 2 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_AGENT ProviderProfileCategory = 3 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL ProviderProfileCategory = 4 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_MESSAGING ProviderProfileCategory = 5 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_DATA ProviderProfileCategory = 6 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_KNOWLEDGE ProviderProfileCategory = 7 +) + +// Enum value maps for ProviderProfileCategory. +var ( + ProviderProfileCategory_name = map[int32]string{ + 0: "PROVIDER_PROFILE_CATEGORY_UNSPECIFIED", + 1: "PROVIDER_PROFILE_CATEGORY_OTHER", + 2: "PROVIDER_PROFILE_CATEGORY_INFERENCE", + 3: "PROVIDER_PROFILE_CATEGORY_AGENT", + 4: "PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL", + 5: "PROVIDER_PROFILE_CATEGORY_MESSAGING", + 6: "PROVIDER_PROFILE_CATEGORY_DATA", + 7: "PROVIDER_PROFILE_CATEGORY_KNOWLEDGE", + } + ProviderProfileCategory_value = map[string]int32{ + "PROVIDER_PROFILE_CATEGORY_UNSPECIFIED": 0, + "PROVIDER_PROFILE_CATEGORY_OTHER": 1, + "PROVIDER_PROFILE_CATEGORY_INFERENCE": 2, + "PROVIDER_PROFILE_CATEGORY_AGENT": 3, + "PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL": 4, + "PROVIDER_PROFILE_CATEGORY_MESSAGING": 5, + "PROVIDER_PROFILE_CATEGORY_DATA": 6, + "PROVIDER_PROFILE_CATEGORY_KNOWLEDGE": 7, + } +) + +func (x ProviderProfileCategory) Enum() *ProviderProfileCategory { + p := new(ProviderProfileCategory) + *p = x + return p +} + +func (x ProviderProfileCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderProfileCategory) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[10].Descriptor() +} + +func (ProviderProfileCategory) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[10] +} + +func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderProfileCategory.Descriptor instead. +func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{10} +} + +type ConfigurationAdmissionState int32 + +const ( + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_UNSPECIFIED ConfigurationAdmissionState = 0 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_PENDING ConfigurationAdmissionState = 1 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_ACCEPTED ConfigurationAdmissionState = 2 + ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_REJECTED ConfigurationAdmissionState = 3 +) + +// Enum value maps for ConfigurationAdmissionState. +var ( + ConfigurationAdmissionState_name = map[int32]string{ + 0: "CONFIGURATION_ADMISSION_STATE_UNSPECIFIED", + 1: "CONFIGURATION_ADMISSION_STATE_PENDING", + 2: "CONFIGURATION_ADMISSION_STATE_ACCEPTED", + 3: "CONFIGURATION_ADMISSION_STATE_REJECTED", + } + ConfigurationAdmissionState_value = map[string]int32{ + "CONFIGURATION_ADMISSION_STATE_UNSPECIFIED": 0, + "CONFIGURATION_ADMISSION_STATE_PENDING": 1, + "CONFIGURATION_ADMISSION_STATE_ACCEPTED": 2, + "CONFIGURATION_ADMISSION_STATE_REJECTED": 3, + } +) + +func (x ConfigurationAdmissionState) Enum() *ConfigurationAdmissionState { + p := new(ConfigurationAdmissionState) + *p = x + return p +} + +func (x ConfigurationAdmissionState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConfigurationAdmissionState) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[11].Descriptor() +} + +func (ConfigurationAdmissionState) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[11] +} + +func (x ConfigurationAdmissionState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConfigurationAdmissionState.Descriptor instead. +func (ConfigurationAdmissionState) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{11} +} + +// Policy load status. +type PolicyStatus int32 + +const ( + PolicyStatus_POLICY_STATUS_UNSPECIFIED PolicyStatus = 0 + // Server received the update; sandbox has not yet loaded it. + PolicyStatus_POLICY_STATUS_PENDING PolicyStatus = 1 + // Sandbox successfully applied this policy version. + PolicyStatus_POLICY_STATUS_LOADED PolicyStatus = 2 + // Sandbox attempted to apply but failed; LKG policy remains active. + // ListSandboxPolicies also uses FAILED for historical payloads that are + // invalid under the current schema; load_error contains the diagnostic. + PolicyStatus_POLICY_STATUS_FAILED PolicyStatus = 3 + // A newer version was persisted before the sandbox loaded this one. + PolicyStatus_POLICY_STATUS_SUPERSEDED PolicyStatus = 4 +) + +// Enum value maps for PolicyStatus. +var ( + PolicyStatus_name = map[int32]string{ + 0: "POLICY_STATUS_UNSPECIFIED", + 1: "POLICY_STATUS_PENDING", + 2: "POLICY_STATUS_LOADED", + 3: "POLICY_STATUS_FAILED", + 4: "POLICY_STATUS_SUPERSEDED", + } + PolicyStatus_value = map[string]int32{ + "POLICY_STATUS_UNSPECIFIED": 0, + "POLICY_STATUS_PENDING": 1, + "POLICY_STATUS_LOADED": 2, + "POLICY_STATUS_FAILED": 3, + "POLICY_STATUS_SUPERSEDED": 4, + } +) + +func (x PolicyStatus) Enum() *PolicyStatus { + p := new(PolicyStatus) + *p = x + return p +} + +func (x PolicyStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[12].Descriptor() +} + +func (PolicyStatus) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[12] +} + +func (x PolicyStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PolicyStatus.Descriptor instead. +func (PolicyStatus) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{12} +} + +// Service status enum. +type ServiceStatus int32 + +const ( + ServiceStatus_SERVICE_STATUS_UNSPECIFIED ServiceStatus = 0 + ServiceStatus_SERVICE_STATUS_HEALTHY ServiceStatus = 1 + ServiceStatus_SERVICE_STATUS_DEGRADED ServiceStatus = 2 + ServiceStatus_SERVICE_STATUS_UNHEALTHY ServiceStatus = 3 +) + +// Enum value maps for ServiceStatus. +var ( + ServiceStatus_name = map[int32]string{ + 0: "SERVICE_STATUS_UNSPECIFIED", + 1: "SERVICE_STATUS_HEALTHY", + 2: "SERVICE_STATUS_DEGRADED", + 3: "SERVICE_STATUS_UNHEALTHY", + } + ServiceStatus_value = map[string]int32{ + "SERVICE_STATUS_UNSPECIFIED": 0, + "SERVICE_STATUS_HEALTHY": 1, + "SERVICE_STATUS_DEGRADED": 2, + "SERVICE_STATUS_UNHEALTHY": 3, + } +) + +func (x ServiceStatus) Enum() *ServiceStatus { + p := new(ServiceStatus) + *p = x + return p +} + +func (x ServiceStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[13].Descriptor() +} + +func (ServiceStatus) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[13] +} + +func (x ServiceStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ServiceStatus.Descriptor instead. +func (ServiceStatus) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{13} +} + +// Workspace-scoped role for members. +type WorkspaceRole int32 + +const ( + WorkspaceRole_WORKSPACE_ROLE_UNSPECIFIED WorkspaceRole = 0 + WorkspaceRole_WORKSPACE_ROLE_USER WorkspaceRole = 1 + WorkspaceRole_WORKSPACE_ROLE_ADMIN WorkspaceRole = 2 +) + +// Enum value maps for WorkspaceRole. +var ( + WorkspaceRole_name = map[int32]string{ + 0: "WORKSPACE_ROLE_UNSPECIFIED", + 1: "WORKSPACE_ROLE_USER", + 2: "WORKSPACE_ROLE_ADMIN", + } + WorkspaceRole_value = map[string]int32{ + "WORKSPACE_ROLE_UNSPECIFIED": 0, + "WORKSPACE_ROLE_USER": 1, + "WORKSPACE_ROLE_ADMIN": 2, + } +) + +func (x WorkspaceRole) Enum() *WorkspaceRole { + p := new(WorkspaceRole) + *p = x + return p +} + +func (x WorkspaceRole) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkspaceRole) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[14].Descriptor() +} + +func (WorkspaceRole) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[14] +} + +func (x WorkspaceRole) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkspaceRole.Descriptor instead. +func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{14} +} + +// Stable recovery action for the most recent provider credential refresh +// failure. Kept after the pre-existing enums so adding it does not renumber +// their generated descriptors. Clients should use this field instead of +// parsing last_error. +type ProviderCredentialRefreshRecoveryAction int32 + +const ( + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED ProviderCredentialRefreshRecoveryAction = 0 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY ProviderCredentialRefreshRecoveryAction = 1 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE ProviderCredentialRefreshRecoveryAction = 2 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION ProviderCredentialRefreshRecoveryAction = 3 + ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE ProviderCredentialRefreshRecoveryAction = 4 +) + +// Enum value maps for ProviderCredentialRefreshRecoveryAction. +var ( + ProviderCredentialRefreshRecoveryAction_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY", + 2: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE", + 3: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION", + 4: "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE", + } + ProviderCredentialRefreshRecoveryAction_value = map[string]int32{ + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY": 1, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE": 2, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION": 3, + "PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE": 4, + } +) + +func (x ProviderCredentialRefreshRecoveryAction) Enum() *ProviderCredentialRefreshRecoveryAction { + p := new(ProviderCredentialRefreshRecoveryAction) + *p = x + return p +} + +func (x ProviderCredentialRefreshRecoveryAction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialRefreshRecoveryAction) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[15].Descriptor() +} + +func (ProviderCredentialRefreshRecoveryAction) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[15] +} + +func (x ProviderCredentialRefreshRecoveryAction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialRefreshRecoveryAction.Descriptor instead. +func (ProviderCredentialRefreshRecoveryAction) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{15} +} + +// Policy applied when the canonical main process exits outside an intentional +// stop or delete operation. Kept after existing enums so generated enum +// descriptors remain stable. +type SandboxRestartPolicy int32 + +const ( + SandboxRestartPolicy_SANDBOX_RESTART_POLICY_UNSPECIFIED SandboxRestartPolicy = 0 + SandboxRestartPolicy_SANDBOX_RESTART_POLICY_NEVER SandboxRestartPolicy = 1 + SandboxRestartPolicy_SANDBOX_RESTART_POLICY_ON_FAILURE SandboxRestartPolicy = 2 + SandboxRestartPolicy_SANDBOX_RESTART_POLICY_ALWAYS SandboxRestartPolicy = 3 +) + +// Enum value maps for SandboxRestartPolicy. +var ( + SandboxRestartPolicy_name = map[int32]string{ + 0: "SANDBOX_RESTART_POLICY_UNSPECIFIED", + 1: "SANDBOX_RESTART_POLICY_NEVER", + 2: "SANDBOX_RESTART_POLICY_ON_FAILURE", + 3: "SANDBOX_RESTART_POLICY_ALWAYS", + } + SandboxRestartPolicy_value = map[string]int32{ + "SANDBOX_RESTART_POLICY_UNSPECIFIED": 0, + "SANDBOX_RESTART_POLICY_NEVER": 1, + "SANDBOX_RESTART_POLICY_ON_FAILURE": 2, + "SANDBOX_RESTART_POLICY_ALWAYS": 3, + } +) + +func (x SandboxRestartPolicy) Enum() *SandboxRestartPolicy { + p := new(SandboxRestartPolicy) + *p = x + return p +} + +func (x SandboxRestartPolicy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SandboxRestartPolicy) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[16].Descriptor() +} + +func (SandboxRestartPolicy) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[16] +} + +func (x SandboxRestartPolicy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SandboxRestartPolicy.Descriptor instead. +func (SandboxRestartPolicy) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{16} +} + +// Result of a public delete, membership removal, or session revocation. +// Default requests return NOT_FOUND for a missing target. With allow_missing, +// only a missing target becomes ALREADY_ABSENT; parent lookup, authorization, +// validation, precondition, and backend errors retain their normal status. +// These results describe the targeted resource, not a same-name replacement. +type DeletionOutcome int32 + +const ( + // No outcome was supplied. Never infer completion from this value. + DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED DeletionOutcome = 0 + // The targeted gateway resource is removed (or the SSH session is revoked). + // Downstream platform garbage collection may still be finishing. + DeletionOutcome_DELETION_OUTCOME_COMPLETED DeletionOutcome = 1 + // Sandbox deletion is accepted but its gateway record still exists. + // Observe the targeted sandbox ID until it disappears for completion. + DeletionOutcome_DELETION_OUTCOME_ACCEPTED DeletionOutcome = 2 + // The target did not exist and allow_missing was true. + DeletionOutcome_DELETION_OUTCOME_ALREADY_ABSENT DeletionOutcome = 3 +) + +// Enum value maps for DeletionOutcome. +var ( + DeletionOutcome_name = map[int32]string{ + 0: "DELETION_OUTCOME_UNSPECIFIED", + 1: "DELETION_OUTCOME_COMPLETED", + 2: "DELETION_OUTCOME_ACCEPTED", + 3: "DELETION_OUTCOME_ALREADY_ABSENT", + } + DeletionOutcome_value = map[string]int32{ + "DELETION_OUTCOME_UNSPECIFIED": 0, + "DELETION_OUTCOME_COMPLETED": 1, + "DELETION_OUTCOME_ACCEPTED": 2, + "DELETION_OUTCOME_ALREADY_ABSENT": 3, + } +) + +func (x DeletionOutcome) Enum() *DeletionOutcome { + p := new(DeletionOutcome) + *p = x + return p +} + +func (x DeletionOutcome) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DeletionOutcome) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[17].Descriptor() +} + +func (DeletionOutcome) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[17] +} + +func (x DeletionOutcome) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DeletionOutcome.Descriptor instead. +func (DeletionOutcome) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{17} +} + +// Last observed network result for a configured external tool endpoint. +// Results describe accepted traffic observations, not present availability. +type EndpointResult int32 + +const ( + EndpointResult_ENDPOINT_RESULT_UNSPECIFIED EndpointResult = 0 + // No exchange has been observed under the current configuration and session. + EndpointResult_ENDPOINT_RESULT_NO_OBSERVED_EXCHANGE EndpointResult = 1 + // An upstream HTTP status below 400 was received. Its body can still contain + // an MCP error; this result does not establish tool-call success. + EndpointResult_ENDPOINT_RESULT_HTTP_RESPONSE_RECEIVED EndpointResult = 2 + // OpenShell policy denied the request locally. + EndpointResult_ENDPOINT_RESULT_POLICY_DENIED EndpointResult = 3 + // An applicable OpenShell-managed credential was unavailable. + EndpointResult_ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE EndpointResult = 4 + // TLS setup for the upstream connection failed. + EndpointResult_ENDPOINT_RESULT_TLS_FAILED EndpointResult = 5 + // The upstream transport failed before an HTTP response arrived. + EndpointResult_ENDPOINT_RESULT_TRANSPORT_FAILED EndpointResult = 6 + // The upstream service returned an HTTP rejection. + EndpointResult_ENDPOINT_RESULT_UPSTREAM_REJECTED EndpointResult = 7 +) + +// Enum value maps for EndpointResult. +var ( + EndpointResult_name = map[int32]string{ + 0: "ENDPOINT_RESULT_UNSPECIFIED", + 1: "ENDPOINT_RESULT_NO_OBSERVED_EXCHANGE", + 2: "ENDPOINT_RESULT_HTTP_RESPONSE_RECEIVED", + 3: "ENDPOINT_RESULT_POLICY_DENIED", + 4: "ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE", + 5: "ENDPOINT_RESULT_TLS_FAILED", + 6: "ENDPOINT_RESULT_TRANSPORT_FAILED", + 7: "ENDPOINT_RESULT_UPSTREAM_REJECTED", + } + EndpointResult_value = map[string]int32{ + "ENDPOINT_RESULT_UNSPECIFIED": 0, + "ENDPOINT_RESULT_NO_OBSERVED_EXCHANGE": 1, + "ENDPOINT_RESULT_HTTP_RESPONSE_RECEIVED": 2, + "ENDPOINT_RESULT_POLICY_DENIED": 3, + "ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE": 4, + "ENDPOINT_RESULT_TLS_FAILED": 5, + "ENDPOINT_RESULT_TRANSPORT_FAILED": 6, + "ENDPOINT_RESULT_UPSTREAM_REJECTED": 7, + } +) + +func (x EndpointResult) Enum() *EndpointResult { + p := new(EndpointResult) + *p = x + return p +} + +func (x EndpointResult) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EndpointResult) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[18].Descriptor() +} + +func (EndpointResult) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[18] +} + +func (x EndpointResult) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EndpointResult.Descriptor instead. +func (EndpointResult) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{18} +} + +// Controls whether an exposed service receives the request's application +// Authorization header. +type ServiceAuthorizationMode int32 + +const ( + // Omission preserves the secure legacy behavior and resolves to STRIP. + ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_UNSPECIFIED ServiceAuthorizationMode = 0 + // Remove Authorization before forwarding to the sandbox service. + ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_STRIP ServiceAuthorizationMode = 1 + // Forward one syntactically valid bearer Authorization header unchanged. + ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH ServiceAuthorizationMode = 2 +) + +// Enum value maps for ServiceAuthorizationMode. +var ( + ServiceAuthorizationMode_name = map[int32]string{ + 0: "SERVICE_AUTHORIZATION_MODE_UNSPECIFIED", + 1: "SERVICE_AUTHORIZATION_MODE_STRIP", + 2: "SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH", + } + ServiceAuthorizationMode_value = map[string]int32{ + "SERVICE_AUTHORIZATION_MODE_UNSPECIFIED": 0, + "SERVICE_AUTHORIZATION_MODE_STRIP": 1, + "SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH": 2, + } +) + +func (x ServiceAuthorizationMode) Enum() *ServiceAuthorizationMode { + p := new(ServiceAuthorizationMode) + *p = x + return p +} + +func (x ServiceAuthorizationMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ServiceAuthorizationMode) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[19].Descriptor() +} + +func (ServiceAuthorizationMode) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[19] +} + +func (x ServiceAuthorizationMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ServiceAuthorizationMode.Descriptor instead. +func (ServiceAuthorizationMode) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{19} +} + +// IssueSandboxToken request. Empty body; identity is established by the +// authentication credentials carried in the request headers (a projected +// Kubernetes ServiceAccount JWT in the K8s driver path). +type IssueSandboxTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IssueSandboxTokenRequest) Reset() { + *x = IssueSandboxTokenRequest{} + mi := &file_openshell_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IssueSandboxTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IssueSandboxTokenRequest) ProtoMessage() {} + +func (x *IssueSandboxTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[0] + 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 IssueSandboxTokenRequest.ProtoReflect.Descriptor instead. +func (*IssueSandboxTokenRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{0} +} + +// IssueSandboxToken response. The supervisor caches the returned token in +// memory and presents it as `Authorization: Bearer` on every subsequent +// gateway RPC. +type IssueSandboxTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-minted session JWT bound to the calling sandbox's UUID, active + // runtime generation, authorization epoch, and durable token lineage. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + // Absolute expiry of the issued token. Absence means the token is non-expiring. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IssueSandboxTokenResponse) Reset() { + *x = IssueSandboxTokenResponse{} + mi := &file_openshell_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IssueSandboxTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IssueSandboxTokenResponse) ProtoMessage() {} + +func (x *IssueSandboxTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[1] + 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 IssueSandboxTokenResponse.ProtoReflect.Descriptor instead. +func (*IssueSandboxTokenResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{1} +} + +func (x *IssueSandboxTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *IssueSandboxTokenResponse) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +// RefreshSandboxToken request. The calling principal must already be a +// sandbox principal (i.e. the request carries a still-valid gateway-minted +// JWT in its Authorization header). Extension service names are resolved +// against server-owned registrations and the sandbox's effective policy; +// callers never choose token audiences directly. +type RefreshSandboxTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Operator registration names for extension services selected by the + // sandbox's effective policy. + ExtensionServiceNames []string `protobuf:"bytes,1,rep,name=extension_service_names,json=extensionServiceNames,proto3" json:"extension_service_names,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshSandboxTokenRequest) Reset() { + *x = RefreshSandboxTokenRequest{} + mi := &file_openshell_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshSandboxTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshSandboxTokenRequest) ProtoMessage() {} + +func (x *RefreshSandboxTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[2] + 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 RefreshSandboxTokenRequest.ProtoReflect.Descriptor instead. +func (*RefreshSandboxTokenRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{2} +} + +func (x *RefreshSandboxTokenRequest) GetExtensionServiceNames() []string { + if x != nil { + return x.ExtensionServiceNames + } + return nil +} + +// RefreshSandboxToken response. The new token replaces the supervisor's +// in-memory bearer credential. +type RefreshSandboxTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Fresh gateway-minted JWT bound to the same sandbox UUID. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + // Absolute expiry of the new token. Absence means the token is non-expiring. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // Fresh credentials for the requested, policy-authorized extension + // services. These remain in supervisor memory and are never persisted. + ExtensionCredentials []*ExtensionServiceCredential `protobuf:"bytes,3,rep,name=extension_credentials,json=extensionCredentials,proto3" json:"extension_credentials,omitempty"` + // Fresh Sandbox Protocol bearer token from the same atomic refresh. + SandboxToken string `protobuf:"bytes,4,opt,name=sandbox_token,json=sandboxToken,proto3" json:"sandbox_token,omitempty"` + // Absolute Sandbox Protocol token expiry. Absence means the token is non-expiring. + SandboxExpirationTime *timestamppb.Timestamp `protobuf:"bytes,105,opt,name=sandbox_expiration_time,json=sandboxExpirationTime,proto3" json:"sandbox_expiration_time,omitempty"` + // Launch generation to which both refreshed credentials are bound. + SessionId string `protobuf:"bytes,6,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Durable authorization epoch shared by the gateway and Sandbox Runtime. + CredentialEpoch uint64 `protobuf:"varint,7,opt,name=credential_epoch,json=credentialEpoch,proto3" json:"credential_epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshSandboxTokenResponse) Reset() { + *x = RefreshSandboxTokenResponse{} + mi := &file_openshell_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshSandboxTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshSandboxTokenResponse) ProtoMessage() {} + +func (x *RefreshSandboxTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[3] + 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 RefreshSandboxTokenResponse.ProtoReflect.Descriptor instead. +func (*RefreshSandboxTokenResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{3} +} + +func (x *RefreshSandboxTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *RefreshSandboxTokenResponse) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +func (x *RefreshSandboxTokenResponse) GetExtensionCredentials() []*ExtensionServiceCredential { + if x != nil { + return x.ExtensionCredentials + } + return nil +} + +func (x *RefreshSandboxTokenResponse) GetSandboxToken() string { + if x != nil { + return x.SandboxToken + } + return "" +} + +func (x *RefreshSandboxTokenResponse) GetSandboxExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.SandboxExpirationTime + } + return nil +} + +func (x *RefreshSandboxTokenResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *RefreshSandboxTokenResponse) GetCredentialEpoch() uint64 { + if x != nil { + return x.CredentialEpoch + } + return 0 +} + +// Health check request. +type HealthRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthRequest) Reset() { + *x = HealthRequest{} + mi := &file_openshell_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthRequest) ProtoMessage() {} + +func (x *HealthRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[4] + 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 HealthRequest.ProtoReflect.Descriptor instead. +func (*HealthRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{4} +} + +// Health check response. +type HealthResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Service status. + Status ServiceStatus `protobuf:"varint,1,opt,name=status,proto3,enum=openshell.v1.ServiceStatus" json:"status,omitempty"` + // Service version. + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthResponse) Reset() { + *x = HealthResponse{} + mi := &file_openshell_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthResponse) ProtoMessage() {} + +func (x *HealthResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[5] + 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 HealthResponse.ProtoReflect.Descriptor instead. +func (*HealthResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{5} +} + +func (x *HealthResponse) GetStatus() ServiceStatus { + if x != nil { + return x.Status + } + return ServiceStatus_SERVICE_STATUS_UNSPECIFIED +} + +func (x *HealthResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +// Current-user request. The identity comes from the authenticated request. +type GetCurrentUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentUserRequest) Reset() { + *x = GetCurrentUserRequest{} + mi := &file_openshell_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentUserRequest) ProtoMessage() {} + +func (x *GetCurrentUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[6] + 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 GetCurrentUserRequest.ProtoReflect.Descriptor instead. +func (*GetCurrentUserRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{6} +} + +// Authenticated user identity as validated by the gateway. +type GetCurrentUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable identity subject (for example, the OIDC `sub` claim). + Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + // Human-readable identity name when supplied by the authentication provider. + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Roles granted to the authenticated identity. + Roles []string `protobuf:"bytes,3,rep,name=roles,proto3" json:"roles,omitempty"` + // OAuth2 scopes granted to the authenticated identity. + Scopes []string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty"` + // Authentication provider that established the identity. + IdentityProvider string `protobuf:"bytes,5,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentUserResponse) Reset() { + *x = GetCurrentUserResponse{} + mi := &file_openshell_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentUserResponse) ProtoMessage() {} + +func (x *GetCurrentUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[7] + 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 GetCurrentUserResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentUserResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{7} +} + +func (x *GetCurrentUserResponse) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *GetCurrentUserResponse) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *GetCurrentUserResponse) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *GetCurrentUserResponse) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *GetCurrentUserResponse) GetIdentityProvider() string { + if x != nil { + return x.IdentityProvider + } + return "" +} + +// Gateway info request. +type GetGatewayInfoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayInfoRequest) Reset() { + *x = GetGatewayInfoRequest{} + mi := &file_openshell_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayInfoRequest) ProtoMessage() {} + +func (x *GetGatewayInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[8] + 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 GetGatewayInfoRequest.ProtoReflect.Descriptor instead. +func (*GetGatewayInfoRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{8} +} + +// Gateway info response. +type GetGatewayInfoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Service status. + Status ServiceStatus `protobuf:"varint,1,opt,name=status,proto3,enum=openshell.v1.ServiceStatus" json:"status,omitempty"` + // OpenShell gateway binary version. + GatewayVersion string `protobuf:"bytes,2,opt,name=gateway_version,json=gatewayVersion,proto3" json:"gateway_version,omitempty"` + // Compute driver runtimes initialized by this gateway. Current gateways + // return exactly one entry. + ComputeDrivers []*ComputeDriverInfo `protobuf:"bytes,3,rep,name=compute_drivers,json=computeDrivers,proto3" json:"compute_drivers,omitempty"` + // Negotiated non-secret metadata for every initialized extension. + Extensions []*NegotiatedExtensionInfo `protobuf:"bytes,4,rep,name=extensions,proto3" json:"extensions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayInfoResponse) Reset() { + *x = GetGatewayInfoResponse{} + mi := &file_openshell_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayInfoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayInfoResponse) ProtoMessage() {} + +func (x *GetGatewayInfoResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[9] + 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 GetGatewayInfoResponse.ProtoReflect.Descriptor instead. +func (*GetGatewayInfoResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{9} +} + +func (x *GetGatewayInfoResponse) GetStatus() ServiceStatus { + if x != nil { + return x.Status + } + return ServiceStatus_SERVICE_STATUS_UNSPECIFIED +} + +func (x *GetGatewayInfoResponse) GetGatewayVersion() string { + if x != nil { + return x.GatewayVersion + } + return "" +} + +func (x *GetGatewayInfoResponse) GetComputeDrivers() []*ComputeDriverInfo { + if x != nil { + return x.ComputeDrivers + } + return nil +} + +func (x *GetGatewayInfoResponse) GetExtensions() []*NegotiatedExtensionInfo { + if x != nil { + return x.Extensions + } + return nil +} + +// Public, non-secret snapshot of one successful startup negotiation. +type NegotiatedExtensionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind ExtensionKind `protobuf:"varint,1,opt,name=kind,proto3,enum=openshell.v1.ExtensionKind" json:"kind,omitempty"` + // Gateway/operator-selected registration name. + ConfiguredName string `protobuf:"bytes,2,opt,name=configured_name,json=configuredName,proto3" json:"configured_name,omitempty"` + // Extension-reported implementation identity. + ImplementationName string `protobuf:"bytes,3,opt,name=implementation_name,json=implementationName,proto3" json:"implementation_name,omitempty"` + // Extension build version, distinct from the protocol version. + ImplementationVersion string `protobuf:"bytes,4,opt,name=implementation_version,json=implementationVersion,proto3" json:"implementation_version,omitempty"` + ProtocolMajor uint32 `protobuf:"varint,5,opt,name=protocol_major,json=protocolMajor,proto3" json:"protocol_major,omitempty"` + ProtocolMinor uint32 `protobuf:"varint,6,opt,name=protocol_minor,json=protocolMinor,proto3" json:"protocol_minor,omitempty"` + // Extension-supported optional capabilities, sorted for stable output. + SupportedCapabilities []string `protobuf:"bytes,7,rep,name=supported_capabilities,json=supportedCapabilities,proto3" json:"supported_capabilities,omitempty"` + // Capabilities the extension requires from the gateway. + RequiredCapabilities []string `protobuf:"bytes,8,rep,name=required_capabilities,json=requiredCapabilities,proto3" json:"required_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegotiatedExtensionInfo) Reset() { + *x = NegotiatedExtensionInfo{} + mi := &file_openshell_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegotiatedExtensionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegotiatedExtensionInfo) ProtoMessage() {} + +func (x *NegotiatedExtensionInfo) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[10] + 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 NegotiatedExtensionInfo.ProtoReflect.Descriptor instead. +func (*NegotiatedExtensionInfo) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{10} +} + +func (x *NegotiatedExtensionInfo) GetKind() ExtensionKind { + if x != nil { + return x.Kind + } + return ExtensionKind_EXTENSION_KIND_UNSPECIFIED +} + +func (x *NegotiatedExtensionInfo) GetConfiguredName() string { + if x != nil { + return x.ConfiguredName + } + return "" +} + +func (x *NegotiatedExtensionInfo) GetImplementationName() string { + if x != nil { + return x.ImplementationName + } + return "" +} + +func (x *NegotiatedExtensionInfo) GetImplementationVersion() string { + if x != nil { + return x.ImplementationVersion + } + return "" +} + +func (x *NegotiatedExtensionInfo) GetProtocolMajor() uint32 { + if x != nil { + return x.ProtocolMajor + } + return 0 +} + +func (x *NegotiatedExtensionInfo) GetProtocolMinor() uint32 { + if x != nil { + return x.ProtocolMinor + } + return 0 +} + +func (x *NegotiatedExtensionInfo) GetSupportedCapabilities() []string { + if x != nil { + return x.SupportedCapabilities + } + return nil +} + +func (x *NegotiatedExtensionInfo) GetRequiredCapabilities() []string { + if x != nil { + return x.RequiredCapabilities + } + return nil +} + +// Info for one initialized compute driver runtime. +type ComputeDriverInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-selected driver name used for routing and driver_config keys. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Capabilities reported by the driver during gateway runtime initialization. + Capabilities *ComputeDriverCapabilities `protobuf:"bytes,2,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComputeDriverInfo) Reset() { + *x = ComputeDriverInfo{} + mi := &file_openshell_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComputeDriverInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComputeDriverInfo) ProtoMessage() {} + +func (x *ComputeDriverInfo) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[11] + 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 ComputeDriverInfo.ProtoReflect.Descriptor instead. +func (*ComputeDriverInfo) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{11} +} + +func (x *ComputeDriverInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ComputeDriverInfo) GetCapabilities() *ComputeDriverCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +// Public compute driver capability snapshot. +type ComputeDriverCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Driver-reported human-readable name from the startup capability snapshot. + DriverName string `protobuf:"bytes,1,opt,name=driver_name,json=driverName,proto3" json:"driver_name,omitempty"` + // Driver-reported implementation version from the startup capability snapshot. + DriverVersion string `protobuf:"bytes,2,opt,name=driver_version,json=driverVersion,proto3" json:"driver_version,omitempty"` + // Static portable resource request forms reported by the driver. + ResourceCapabilities *ResourceCapabilities `protobuf:"bytes,3,opt,name=resource_capabilities,json=resourceCapabilities,proto3" json:"resource_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComputeDriverCapabilities) Reset() { + *x = ComputeDriverCapabilities{} + mi := &file_openshell_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComputeDriverCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComputeDriverCapabilities) ProtoMessage() {} + +func (x *ComputeDriverCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[12] + 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 ComputeDriverCapabilities.ProtoReflect.Descriptor instead. +func (*ComputeDriverCapabilities) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{12} +} + +func (x *ComputeDriverCapabilities) GetDriverName() string { + if x != nil { + return x.DriverName + } + return "" +} + +func (x *ComputeDriverCapabilities) GetDriverVersion() string { + if x != nil { + return x.DriverVersion + } + return "" +} + +func (x *ComputeDriverCapabilities) GetResourceCapabilities() *ResourceCapabilities { + if x != nil { + return x.ResourceCapabilities + } + return nil +} + +// Static portable resource request forms reported by a compute driver. +// An omitted domain means the driver does not report that domain. +type ResourceCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cpu *CpuResourceCapabilities `protobuf:"bytes,1,opt,name=cpu,proto3" json:"cpu,omitempty"` + Memory *MemoryResourceCapabilities `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + Gpu *GpuResourceCapabilities `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceCapabilities) Reset() { + *x = ResourceCapabilities{} + mi := &file_openshell_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceCapabilities) ProtoMessage() {} + +func (x *ResourceCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[13] + 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 ResourceCapabilities.ProtoReflect.Descriptor instead. +func (*ResourceCapabilities) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{13} +} + +func (x *ResourceCapabilities) GetCpu() *CpuResourceCapabilities { + if x != nil { + return x.Cpu + } + return nil +} + +func (x *ResourceCapabilities) GetMemory() *MemoryResourceCapabilities { + if x != nil { + return x.Memory + } + return nil +} + +func (x *ResourceCapabilities) GetGpu() *GpuResourceCapabilities { + if x != nil { + return x.Gpu + } + return nil +} + +type CpuResourceCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The driver accepts and enforces a portable CPU limit. + LimitSupported bool `protobuf:"varint,1,opt,name=limit_supported,json=limitSupported,proto3" json:"limit_supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CpuResourceCapabilities) Reset() { + *x = CpuResourceCapabilities{} + mi := &file_openshell_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CpuResourceCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CpuResourceCapabilities) ProtoMessage() {} + +func (x *CpuResourceCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[14] + 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 CpuResourceCapabilities.ProtoReflect.Descriptor instead. +func (*CpuResourceCapabilities) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{14} +} + +func (x *CpuResourceCapabilities) GetLimitSupported() bool { + if x != nil { + return x.LimitSupported + } + return false +} + +type MemoryResourceCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The driver accepts and enforces a portable memory limit. + LimitSupported bool `protobuf:"varint,1,opt,name=limit_supported,json=limitSupported,proto3" json:"limit_supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoryResourceCapabilities) Reset() { + *x = MemoryResourceCapabilities{} + mi := &file_openshell_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoryResourceCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoryResourceCapabilities) ProtoMessage() {} + +func (x *MemoryResourceCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[15] + 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 MemoryResourceCapabilities.ProtoReflect.Descriptor instead. +func (*MemoryResourceCapabilities) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{15} +} + +func (x *MemoryResourceCapabilities) GetLimitSupported() bool { + if x != nil { + return x.LimitSupported + } + return false +} + +type GpuResourceCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The driver accepts a GPU request with no explicit count. + DefaultSelectionSupported bool `protobuf:"varint,1,opt,name=default_selection_supported,json=defaultSelectionSupported,proto3" json:"default_selection_supported,omitempty"` + // The driver accepts an explicit `gpu.count` request. + CountSelectionSupported bool `protobuf:"varint,2,opt,name=count_selection_supported,json=countSelectionSupported,proto3" json:"count_selection_supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GpuResourceCapabilities) Reset() { + *x = GpuResourceCapabilities{} + mi := &file_openshell_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GpuResourceCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GpuResourceCapabilities) ProtoMessage() {} + +func (x *GpuResourceCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[16] + 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 GpuResourceCapabilities.ProtoReflect.Descriptor instead. +func (*GpuResourceCapabilities) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{16} +} + +func (x *GpuResourceCapabilities) GetDefaultSelectionSupported() bool { + if x != nil { + return x.DefaultSelectionSupported + } + return false +} + +func (x *GpuResourceCapabilities) GetCountSelectionSupported() bool { + if x != nil { + return x.CountSelectionSupported + } + return false +} + +// Public sandbox resource exposed by the OpenShell API. +// +// This is the canonical gateway-owned view of a sandbox. It merges user intent +// (`spec`) with gateway-managed metadata and status derived from internal +// compute-driver observations. +// +// Note: The `namespace` field has been removed from the public API. It remains +// in the internal `DriverSandbox` message as a compute-driver implementation detail. +type Sandbox struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Desired sandbox configuration submitted through the API. + Spec *SandboxSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` + // Latest user-facing observed status derived by the gateway. + Status *SandboxStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + // Read-only SSH host identity in OpenSSH SHA256: form. Stable for + // this sandbox's lifetime, including runtime restarts. Empty on legacy + // runtimes that do not provide a gateway-managed SSH identity. + HostKeyFingerprint string `protobuf:"bytes,6,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` + // Read-only provenance for sandboxes created from a reusable workload template. + CreatedFromWorkloadTemplate *SandboxWorkloadTemplateProvenance `protobuf:"bytes,20,opt,name=created_from_workload_template,json=createdFromWorkloadTemplate,proto3" json:"created_from_workload_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Sandbox) Reset() { + *x = Sandbox{} + mi := &file_openshell_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Sandbox) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Sandbox) ProtoMessage() {} + +func (x *Sandbox) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[17] + 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 Sandbox.ProtoReflect.Descriptor instead. +func (*Sandbox) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{17} +} + +func (x *Sandbox) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Sandbox) GetSpec() *SandboxSpec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *Sandbox) GetStatus() *SandboxStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *Sandbox) GetHostKeyFingerprint() string { + if x != nil { + return x.HostKeyFingerprint + } + return "" +} + +func (x *Sandbox) GetCreatedFromWorkloadTemplate() *SandboxWorkloadTemplateProvenance { + if x != nil { + return x.CreatedFromWorkloadTemplate + } + return nil +} + +// Desired sandbox configuration provided through the public API. +type SandboxSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Log level exposed to processes running inside the sandbox. + LogLevel string `protobuf:"bytes,1,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"` + // Environment variables injected into the sandbox runtime. + Environment map[string]string `protobuf:"bytes,5,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Container or VM template used to provision the sandbox. + Template *SandboxTemplate `protobuf:"bytes,6,opt,name=template,proto3" json:"template,omitempty"` + // Required sandbox policy configuration. + Policy *sandboxv1.SandboxPolicy `protobuf:"bytes,7,opt,name=policy,proto3" json:"policy,omitempty"` + // Provider names to attach to this sandbox. + Providers []string `protobuf:"bytes,8,rep,name=providers,proto3" json:"providers,omitempty"` + // Portable resource requirements used by the gateway for driver selection + // and by drivers for provisioning. + ResourceRequirements *ResourceRequirements `protobuf:"bytes,9,opt,name=resource_requirements,json=resourceRequirements,proto3" json:"resource_requirements,omitempty"` + // Canonical command launched once by the sandbox supervisor. No shell + // parsing is performed. The gateway normalizes an omitted command to the + // portable scratch login shell before persistence. + Command []string `protobuf:"bytes,12,rep,name=command,proto3" json:"command,omitempty"` + // Allocate a retained pseudo-terminal for the main process. + Tty bool `protobuf:"varint,13,opt,name=tty,proto3" json:"tty,omitempty"` + // Gateway-owned attachment identity, changed atomically with the provider set. + // Equality only: detach and reattach must not revive an older receipt. + ProviderAttachmentEpoch string `protobuf:"bytes,14,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + // Gateway-owned policy for replacing the sandbox runtime after the canonical + // main process exits. Unspecified is normalized to Never before persistence. + RestartPolicy SandboxRestartPolicy `protobuf:"varint,15,opt,name=restart_policy,json=restartPolicy,proto3,enum=openshell.v1.SandboxRestartPolicy" json:"restart_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxSpec) Reset() { + *x = SandboxSpec{} + mi := &file_openshell_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxSpec) ProtoMessage() {} + +func (x *SandboxSpec) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[18] + 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 SandboxSpec.ProtoReflect.Descriptor instead. +func (*SandboxSpec) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{18} +} + +func (x *SandboxSpec) GetLogLevel() string { + if x != nil { + return x.LogLevel + } + return "" +} + +func (x *SandboxSpec) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *SandboxSpec) GetTemplate() *SandboxTemplate { + if x != nil { + return x.Template + } + return nil +} + +func (x *SandboxSpec) GetPolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *SandboxSpec) GetProviders() []string { + if x != nil { + return x.Providers + } + return nil +} + +func (x *SandboxSpec) GetResourceRequirements() *ResourceRequirements { + if x != nil { + return x.ResourceRequirements + } + return nil +} + +func (x *SandboxSpec) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *SandboxSpec) GetTty() bool { + if x != nil { + return x.Tty + } + return false +} + +func (x *SandboxSpec) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + +func (x *SandboxSpec) GetRestartPolicy() SandboxRestartPolicy { + if x != nil { + return x.RestartPolicy + } + return SandboxRestartPolicy_SANDBOX_RESTART_POLICY_UNSPECIFIED +} + +type ResourceRequirements struct { + state protoimpl.MessageState `protogen:"open.v1"` + // GPU requirements for the sandbox. Presence indicates a GPU request. + Gpu *GpuResourceRequirements `protobuf:"bytes,1,opt,name=gpu,proto3" json:"gpu,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceRequirements) Reset() { + *x = ResourceRequirements{} + mi := &file_openshell_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceRequirements) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceRequirements) ProtoMessage() {} + +func (x *ResourceRequirements) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[19] + 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 ResourceRequirements.ProtoReflect.Descriptor instead. +func (*ResourceRequirements) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{19} +} + +func (x *ResourceRequirements) GetGpu() *GpuResourceRequirements { + if x != nil { + return x.Gpu + } + return nil +} + +// Public GPU resource requirements. +type GpuResourceRequirements struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional number of GPUs requested. When omitted, the request is for one + // GPU using the selected driver's default assignment behavior. + Count *uint32 `protobuf:"varint,1,opt,name=count,proto3,oneof" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GpuResourceRequirements) Reset() { + *x = GpuResourceRequirements{} + mi := &file_openshell_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GpuResourceRequirements) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GpuResourceRequirements) ProtoMessage() {} + +func (x *GpuResourceRequirements) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[20] + 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 GpuResourceRequirements.ProtoReflect.Descriptor instead. +func (*GpuResourceRequirements) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{20} +} + +func (x *GpuResourceRequirements) GetCount() uint32 { + if x != nil && x.Count != nil { + return *x.Count + } + return 0 +} + +// Historical inline compute template mapped onto compute-driver template inputs. +// +// Despite its name, this is not a reusable named sandbox template resource. It +// is an inline part of `SandboxSpec` kept for v1 compatibility. A future +// breaking API cleanup may rename this message to free `SandboxTemplate` for +// the reusable template resource now represented by `SandboxWorkloadTemplate`. +type SandboxTemplate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Fully-qualified OCI image reference used to boot the sandbox. + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + // Optional runtime class name requested from the compute platform. + RuntimeClassName string `protobuf:"bytes,2,opt,name=runtime_class_name,json=runtimeClassName,proto3" json:"runtime_class_name,omitempty"` + // Optional agent socket path exposed to the workload. + AgentSocket string `protobuf:"bytes,3,opt,name=agent_socket,json=agentSocket,proto3" json:"agent_socket,omitempty"` + // Labels applied to compute-platform resources for this sandbox. + Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Annotations applied to compute-platform resources for this sandbox. + Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Additional environment variables injected by the template. + Environment map[string]string `protobuf:"bytes,6,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Platform-specific compute resource requirements and limits. + Resources *structpb.Struct `protobuf:"bytes,7,opt,name=resources,proto3" json:"resources,omitempty"` + // Enable Kubernetes user namespace isolation (hostUsers: false). + // When true, container UID 0 maps to a non-root host UID and capabilities + // become namespaced. Requires Kubernetes 1.33+ with user namespace support + // available (beta through 1.35, GA in 1.36+) and a supporting runtime. + // When unset, the cluster-wide default is used. + UserNamespaces *bool `protobuf:"varint,10,opt,name=user_namespaces,json=userNamespaces,proto3,oneof" json:"user_namespaces,omitempty"` + // Driver-keyed opaque config envelope supplied by the caller. + // The gateway selects the block matching the active compute driver and + // forwards only that inner Struct to DriverSandboxTemplate.driver_config. + // The selected driver owns nested schema validation. + DriverConfig *structpb.Struct `protobuf:"bytes,11,opt,name=driver_config,json=driverConfig,proto3" json:"driver_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxTemplate) Reset() { + *x = SandboxTemplate{} + mi := &file_openshell_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxTemplate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxTemplate) ProtoMessage() {} + +func (x *SandboxTemplate) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[21] + 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 SandboxTemplate.ProtoReflect.Descriptor instead. +func (*SandboxTemplate) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{21} +} + +func (x *SandboxTemplate) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *SandboxTemplate) GetRuntimeClassName() string { + if x != nil { + return x.RuntimeClassName + } + return "" +} + +func (x *SandboxTemplate) GetAgentSocket() string { + if x != nil { + return x.AgentSocket + } + return "" +} + +func (x *SandboxTemplate) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *SandboxTemplate) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *SandboxTemplate) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *SandboxTemplate) GetResources() *structpb.Struct { + if x != nil { + return x.Resources + } + return nil +} + +func (x *SandboxTemplate) GetUserNamespaces() bool { + if x != nil && x.UserNamespaces != nil { + return *x.UserNamespaces + } + return false +} + +func (x *SandboxTemplate) GetDriverConfig() *structpb.Struct { + if x != nil { + return x.DriverConfig + } + return nil +} + +// Reusable named sandbox workload template resource. +// +// This is the actual workspace-scoped template resource used to create +// sandboxes by reference. It uses the longer name in v1 to avoid colliding with +// the historical inline `SandboxTemplate` message. A future breaking API +// cleanup may rename this resource to `SandboxTemplate`. +type SandboxWorkloadTemplate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Desired reusable workload shape and template-owned driver config. + Spec *SandboxWorkloadTemplateSpec `protobuf:"bytes,2,opt,name=spec,proto3" json:"spec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadTemplate) Reset() { + *x = SandboxWorkloadTemplate{} + mi := &file_openshell_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxWorkloadTemplate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxWorkloadTemplate) ProtoMessage() {} + +func (x *SandboxWorkloadTemplate) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxWorkloadTemplate.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplate) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{22} +} + +func (x *SandboxWorkloadTemplate) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SandboxWorkloadTemplate) GetSpec() *SandboxWorkloadTemplateSpec { + if x != nil { + return x.Spec + } + return nil +} + +type SandboxWorkloadTemplateSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Portable workload shape. + Workload *SandboxWorkloadConfig `protobuf:"bytes,1,opt,name=workload,proto3" json:"workload,omitempty"` + // Driver-keyed opaque config envelope supplied by the template owner. + DriverConfig *structpb.Struct `protobuf:"bytes,2,opt,name=driver_config,json=driverConfig,proto3" json:"driver_config,omitempty"` + // Desired service level associated with this template. + DesiredServiceLevel *SandboxServiceLevel `protobuf:"bytes,3,opt,name=desired_service_level,json=desiredServiceLevel,proto3" json:"desired_service_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadTemplateSpec) Reset() { + *x = SandboxWorkloadTemplateSpec{} + mi := &file_openshell_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxWorkloadTemplateSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxWorkloadTemplateSpec) ProtoMessage() {} + +func (x *SandboxWorkloadTemplateSpec) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxWorkloadTemplateSpec.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplateSpec) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{23} +} + +func (x *SandboxWorkloadTemplateSpec) GetWorkload() *SandboxWorkloadConfig { + if x != nil { + return x.Workload + } + return nil +} + +func (x *SandboxWorkloadTemplateSpec) GetDriverConfig() *structpb.Struct { + if x != nil { + return x.DriverConfig + } + return nil +} + +func (x *SandboxWorkloadTemplateSpec) GetDesiredServiceLevel() *SandboxServiceLevel { + if x != nil { + return x.DesiredServiceLevel + } + return nil +} + +type SandboxWorkloadConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Fully-qualified OCI image reference used to boot the sandbox. + Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` + // Environment variables injected into the sandbox runtime. + Environment map[string]string `protobuf:"bytes,2,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Portable resource requirements for sandboxes created from this workload. + Resources *SandboxResources `protobuf:"bytes,3,opt,name=resources,proto3" json:"resources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadConfig) Reset() { + *x = SandboxWorkloadConfig{} + mi := &file_openshell_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxWorkloadConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxWorkloadConfig) ProtoMessage() {} + +func (x *SandboxWorkloadConfig) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxWorkloadConfig.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadConfig) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{24} +} + +func (x *SandboxWorkloadConfig) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *SandboxWorkloadConfig) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *SandboxWorkloadConfig) GetResources() *SandboxResources { + if x != nil { + return x.Resources + } + return nil +} + +type SandboxResources struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Portable CPU quantity, for example "500m" or "2". + Cpu string `protobuf:"bytes,1,opt,name=cpu,proto3" json:"cpu,omitempty"` + // Portable memory quantity, for example "512Mi" or "2Gi". + Memory string `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + // GPU requirements for the sandbox workload. Presence indicates a GPU + // request. When count is omitted, the request uses the selected driver's + // default GPU assignment behavior. + Gpu *GpuResourceRequirements `protobuf:"bytes,3,opt,name=gpu,proto3" json:"gpu,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxResources) Reset() { + *x = SandboxResources{} + mi := &file_openshell_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxResources) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxResources) ProtoMessage() {} + +func (x *SandboxResources) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxResources.ProtoReflect.Descriptor instead. +func (*SandboxResources) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{25} +} + +func (x *SandboxResources) GetCpu() string { + if x != nil { + return x.Cpu + } + return "" +} + +func (x *SandboxResources) GetMemory() string { + if x != nil { + return x.Memory + } + return "" +} + +func (x *SandboxResources) GetGpu() *GpuResourceRequirements { + if x != nil { + return x.Gpu + } + return nil +} + +type SandboxServiceLevel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Startup *SandboxStartup `protobuf:"bytes,1,opt,name=startup,proto3" json:"startup,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxServiceLevel) Reset() { + *x = SandboxServiceLevel{} + mi := &file_openshell_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxServiceLevel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxServiceLevel) ProtoMessage() {} + +func (x *SandboxServiceLevel) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxServiceLevel.ProtoReflect.Descriptor instead. +func (*SandboxServiceLevel) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{26} +} + +func (x *SandboxServiceLevel) GetStartup() *SandboxStartup { + if x != nil { + return x.Startup + } + return nil +} + +type SandboxStartup struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReadyWithin *durationpb.Duration `protobuf:"bytes,1,opt,name=ready_within,json=readyWithin,proto3" json:"ready_within,omitempty"` + MaxBurst uint32 `protobuf:"varint,2,opt,name=max_burst,json=maxBurst,proto3" json:"max_burst,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStartup) Reset() { + *x = SandboxStartup{} + mi := &file_openshell_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStartup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStartup) ProtoMessage() {} + +func (x *SandboxStartup) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxStartup.ProtoReflect.Descriptor instead. +func (*SandboxStartup) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{27} +} + +func (x *SandboxStartup) GetReadyWithin() *durationpb.Duration { + if x != nil { + return x.ReadyWithin + } + return nil +} + +func (x *SandboxStartup) GetMaxBurst() uint32 { + if x != nil { + return x.MaxBurst + } + return 0 +} + +type SandboxWorkloadTemplateProvenance struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ResourceVersion string `protobuf:"bytes,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxWorkloadTemplateProvenance) Reset() { + *x = SandboxWorkloadTemplateProvenance{} + mi := &file_openshell_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxWorkloadTemplateProvenance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxWorkloadTemplateProvenance) ProtoMessage() {} + +func (x *SandboxWorkloadTemplateProvenance) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxWorkloadTemplateProvenance.ProtoReflect.Descriptor instead. +func (*SandboxWorkloadTemplateProvenance) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{28} +} + +func (x *SandboxWorkloadTemplateProvenance) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SandboxWorkloadTemplateProvenance) GetResourceVersion() string { + if x != nil { + return x.ResourceVersion + } + return "" +} + +// User-facing sandbox status derived by the gateway from compute-driver observations. +// +// Public status does not embed driver-only flags such as `deleting`. +type SandboxStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the agent pod or equivalent runtime instance. + AgentPod string `protobuf:"bytes,2,opt,name=agent_pod,json=agentPod,proto3" json:"agent_pod,omitempty"` + // File descriptor or endpoint for reaching the agent service, when available. + AgentFd string `protobuf:"bytes,3,opt,name=agent_fd,json=agentFd,proto3" json:"agent_fd,omitempty"` + // File descriptor or endpoint for reaching the sandbox service, when available. + SandboxFd string `protobuf:"bytes,4,opt,name=sandbox_fd,json=sandboxFd,proto3" json:"sandbox_fd,omitempty"` + // Latest user-facing readiness and lifecycle conditions. + Conditions []*SandboxCondition `protobuf:"bytes,5,rep,name=conditions,proto3" json:"conditions,omitempty"` + // Gateway-derived lifecycle summary. + Phase SandboxPhase `protobuf:"varint,6,opt,name=phase,proto3,enum=openshell.v1.SandboxPhase" json:"phase,omitempty"` + // Currently active policy version (updated when sandbox reports loaded). + CurrentPolicyVersion uint32 `protobuf:"varint,7,opt,name=current_policy_version,json=currentPolicyVersion,proto3" json:"current_policy_version,omitempty"` + // Supervisor instance currently associated with the canonical main process. + // The gateway uses this to reject stale exit reports after a restart. + MainProcessInstanceId string `protobuf:"bytes,8,opt,name=main_process_instance_id,json=mainProcessInstanceId,proto3" json:"main_process_instance_id,omitempty"` + // Most recent normalized main process result. Signal exits use 128 + signal number. + // Cleared when a replacement main process becomes ready. + ExitCode *int32 `protobuf:"varint,9,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"` + // Last accepted network result for each configured tool server endpoint. + // Currently populated for MCP-over-HTTP endpoints. These passive results + // remain separate from sandbox lifecycle conditions and readiness. + EndpointStatuses []*EndpointStatus `protobuf:"bytes,10,rep,name=endpoint_statuses,json=endpointStatuses,proto3" json:"endpoint_statuses,omitempty"` + // Independent of infrastructure phase; retained across driver observations. + ConfigurationAdmission *SandboxConfigurationAdmission `protobuf:"bytes,11,opt,name=configuration_admission,json=configurationAdmission,proto3" json:"configuration_admission,omitempty"` + // Durable first-acceptance marker. Absent on legacy records; never reset by restart. + ConfigurationActivated *bool `protobuf:"varint,12,opt,name=configuration_activated,json=configurationActivated,proto3,oneof" json:"configuration_activated,omitempty"` + // Gateway-owned provisioning deadlines, retained after timeout for inspection and retry. + Provisioning *SandboxProvisioning `protobuf:"bytes,13,opt,name=provisioning,proto3" json:"provisioning,omitempty"` + // Consecutive policy-driven restart number in the current crash loop. + RestartCount uint32 `protobuf:"varint,14,opt,name=restart_count,json=restartCount,proto3" json:"restart_count,omitempty"` + // Deadline for the next restart attempt or recovery check. + NextRestartTime *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=next_restart_time,json=nextRestartTime,proto3" json:"next_restart_time,omitempty"` + // Time when the current main process became ready. + MainProcessStartedTime *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=main_process_started_time,json=mainProcessStartedTime,proto3" json:"main_process_started_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStatus) Reset() { + *x = SandboxStatus{} + mi := &file_openshell_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStatus) ProtoMessage() {} + +func (x *SandboxStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxStatus.ProtoReflect.Descriptor instead. +func (*SandboxStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{29} +} + +func (x *SandboxStatus) GetAgentPod() string { + if x != nil { + return x.AgentPod + } + return "" +} + +func (x *SandboxStatus) GetAgentFd() string { + if x != nil { + return x.AgentFd + } + return "" +} + +func (x *SandboxStatus) GetSandboxFd() string { + if x != nil { + return x.SandboxFd + } + return "" +} + +func (x *SandboxStatus) GetConditions() []*SandboxCondition { + if x != nil { + return x.Conditions + } + return nil +} + +func (x *SandboxStatus) GetPhase() SandboxPhase { + if x != nil { + return x.Phase + } + return SandboxPhase_SANDBOX_PHASE_UNSPECIFIED +} + +func (x *SandboxStatus) GetCurrentPolicyVersion() uint32 { + if x != nil { + return x.CurrentPolicyVersion + } + return 0 +} + +func (x *SandboxStatus) GetMainProcessInstanceId() string { + if x != nil { + return x.MainProcessInstanceId + } + return "" +} + +func (x *SandboxStatus) GetExitCode() int32 { + if x != nil && x.ExitCode != nil { + return *x.ExitCode + } + return 0 +} + +func (x *SandboxStatus) GetEndpointStatuses() []*EndpointStatus { + if x != nil { + return x.EndpointStatuses + } + return nil +} + +func (x *SandboxStatus) GetConfigurationAdmission() *SandboxConfigurationAdmission { + if x != nil { + return x.ConfigurationAdmission + } + return nil +} + +func (x *SandboxStatus) GetConfigurationActivated() bool { + if x != nil && x.ConfigurationActivated != nil { + return *x.ConfigurationActivated + } + return false +} + +func (x *SandboxStatus) GetProvisioning() *SandboxProvisioning { + if x != nil { + return x.Provisioning + } + return nil +} + +func (x *SandboxStatus) GetRestartCount() uint32 { + if x != nil { + return x.RestartCount + } + return 0 +} + +func (x *SandboxStatus) GetNextRestartTime() *timestamppb.Timestamp { + if x != nil { + return x.NextRestartTime + } + return nil +} + +func (x *SandboxStatus) GetMainProcessStartedTime() *timestamppb.Timestamp { + if x != nil { + return x.MainProcessStartedTime + } + return nil +} + +// User-facing sandbox condition derived from platform or gateway observations. +type SandboxCondition struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Condition class, typically mirroring the underlying platform condition type. + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Condition status value such as `True`, `False`, or `Unknown`. + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + // Short machine-readable reason associated with the condition. + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + // Human-readable condition message. + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + // Timestamp reported by the condition owner for the last transition. + TransitionTime *timestamppb.Timestamp `protobuf:"bytes,105,opt,name=transition_time,json=transitionTime,proto3" json:"transition_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxCondition) Reset() { + *x = SandboxCondition{} + mi := &file_openshell_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxCondition) ProtoMessage() {} + +func (x *SandboxCondition) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxCondition.ProtoReflect.Descriptor instead. +func (*SandboxCondition) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{30} +} + +func (x *SandboxCondition) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SandboxCondition) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SandboxCondition) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *SandboxCondition) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SandboxCondition) GetTransitionTime() *timestamppb.Timestamp { + if x != nil { + return x.TransitionTime + } + return nil +} + +// Public platform event exposed on the sandbox watch stream. +type PlatformEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Time when the event occurred. + EventTime *timestamppb.Timestamp `protobuf:"bytes,101,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` + // Event source (e.g. "kubernetes", "docker", "process"). + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + // Event type/severity (e.g. "Normal", "Warning"). + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + // Short reason code (e.g. "Started", "Pulled", "Failed"). + Reason string `protobuf:"bytes,4,opt,name=reason,proto3" json:"reason,omitempty"` + // Human-readable event message. + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + // Optional metadata as key-value pairs. + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlatformEvent) Reset() { + *x = PlatformEvent{} + mi := &file_openshell_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlatformEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlatformEvent) ProtoMessage() {} + +func (x *PlatformEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlatformEvent.ProtoReflect.Descriptor instead. +func (*PlatformEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{31} +} + +func (x *PlatformEvent) GetEventTime() *timestamppb.Timestamp { + if x != nil { + return x.EventTime + } + return nil +} + +func (x *PlatformEvent) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *PlatformEvent) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *PlatformEvent) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *PlatformEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *PlatformEvent) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// Create sandbox request. +type CreateSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,7,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Spec *SandboxSpec `protobuf:"bytes,1,opt,name=spec,proto3" json:"spec,omitempty"` + // Optional user-supplied sandbox name. When empty the server generates one. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Optional labels for the sandbox (key-value metadata). + Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional annotations for the sandbox (non-selector metadata). + Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // One-shot launch hint indicating that the creating client will attach to + // the canonical main process. The supervisor keeps the terminal transport + // alive until that attachment connects and closes naturally. + AwaitMainProcessAttachment bool `protobuf:"varint,5,opt,name=await_main_process_attachment,json=awaitMainProcessAttachment,proto3" json:"await_main_process_attachment,omitempty"` + // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. + WorkloadTemplate string `protobuf:"bytes,6,opt,name=workload_template,json=workloadTemplate,proto3" json:"workload_template,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,8,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // HTTP services to expose when the sandbox is created. Endpoints are + // registered after the sandbox has been persisted and route only while the + // sandbox is ready. + ServiceExposures []*SandboxServiceExposure `protobuf:"bytes,9,rep,name=service_exposures,json=serviceExposures,proto3" json:"service_exposures,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSandboxRequest) Reset() { + *x = CreateSandboxRequest{} + mi := &file_openshell_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSandboxRequest) ProtoMessage() {} + +func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead. +func (*CreateSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{32} +} + +func (x *CreateSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *CreateSandboxRequest) GetSpec() *SandboxSpec { + if x != nil { + return x.Spec + } + return nil +} + +func (x *CreateSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateSandboxRequest) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *CreateSandboxRequest) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *CreateSandboxRequest) GetAwaitMainProcessAttachment() bool { + if x != nil { + return x.AwaitMainProcessAttachment + } + return false +} + +func (x *CreateSandboxRequest) GetWorkloadTemplate() string { + if x != nil { + return x.WorkloadTemplate + } + return "" +} + +func (x *CreateSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *CreateSandboxRequest) GetServiceExposures() []*SandboxServiceExposure { + if x != nil { + return x.ServiceExposures + } + return nil +} + +type CreateSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Template *SandboxWorkloadTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSandboxTemplateRequest) Reset() { + *x = CreateSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSandboxTemplateRequest) ProtoMessage() {} + +func (x *CreateSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*CreateSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{33} +} + +func (x *CreateSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *CreateSandboxTemplateRequest) GetTemplate() *SandboxWorkloadTemplate { + if x != nil { + return x.Template + } + return nil +} + +func (x *CreateSandboxTemplateRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type GetSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxTemplateRequest) Reset() { + *x = GetSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxTemplateRequest) ProtoMessage() {} + +func (x *GetSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{34} +} + +func (x *GetSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxTemplateRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type ListSandboxTemplatesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Named and all-workspaces selections are accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of templates to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListSandboxTemplates response. All other request + // parameters except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional label selector in key=value comma-separated form. + LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxTemplatesRequest) Reset() { + *x = ListSandboxTemplatesRequest{} + mi := &file_openshell_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxTemplatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxTemplatesRequest) ProtoMessage() {} + +func (x *ListSandboxTemplatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSandboxTemplatesRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxTemplatesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{35} +} + +func (x *ListSandboxTemplatesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListSandboxTemplatesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSandboxTemplatesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListSandboxTemplatesRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +type DeleteSandboxTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Succeed with ALREADY_ABSENT if the target is missing. Authorization and + // parent-workspace checks still apply. + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxTemplateRequest) Reset() { + *x = DeleteSandboxTemplateRequest{} + mi := &file_openshell_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxTemplateRequest) ProtoMessage() {} + +func (x *DeleteSandboxTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteSandboxTemplateRequest.ProtoReflect.Descriptor instead. +func (*DeleteSandboxTemplateRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{36} +} + +func (x *DeleteSandboxTemplateRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteSandboxTemplateRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteSandboxTemplateRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteSandboxTemplateRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type SandboxTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Template *SandboxWorkloadTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxTemplateResponse) Reset() { + *x = SandboxTemplateResponse{} + mi := &file_openshell_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxTemplateResponse) ProtoMessage() {} + +func (x *SandboxTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SandboxTemplateResponse.ProtoReflect.Descriptor instead. +func (*SandboxTemplateResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{37} +} + +func (x *SandboxTemplateResponse) GetTemplate() *SandboxWorkloadTemplate { + if x != nil { + return x.Template + } + return nil +} + +type ListSandboxTemplatesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Templates []*SandboxWorkloadTemplate `protobuf:"bytes,1,rep,name=templates,proto3" json:"templates,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxTemplatesResponse) Reset() { + *x = ListSandboxTemplatesResponse{} + mi := &file_openshell_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxTemplatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxTemplatesResponse) ProtoMessage() {} + +func (x *ListSandboxTemplatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSandboxTemplatesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxTemplatesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{38} +} + +func (x *ListSandboxTemplatesResponse) GetTemplates() []*SandboxWorkloadTemplate { + if x != nil { + return x.Templates + } + return nil +} + +func (x *ListSandboxTemplatesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +type DeleteSandboxTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxTemplateResponse) Reset() { + *x = DeleteSandboxTemplateResponse{} + mi := &file_openshell_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxTemplateResponse) ProtoMessage() {} + +func (x *DeleteSandboxTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[39] + 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 DeleteSandboxTemplateResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxTemplateResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{39} +} + +func (x *DeleteSandboxTemplateResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Request a gateway-owned staging slot for a local rootfs tar archive. +type BeginRootfsTarStagingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Base file name of the local archive. The gateway uses it only to name the + // staged file; path separators and traversal components are rejected. + FileName string `protobuf:"bytes,1,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + // Size of the local archive in bytes, checked against the driver limit + // before the gateway allocates a slot. + SizeBytes uint64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginRootfsTarStagingRequest) Reset() { + *x = BeginRootfsTarStagingRequest{} + mi := &file_openshell_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginRootfsTarStagingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginRootfsTarStagingRequest) ProtoMessage() {} + +func (x *BeginRootfsTarStagingRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[40] + 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 BeginRootfsTarStagingRequest.ProtoReflect.Descriptor instead. +func (*BeginRootfsTarStagingRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{40} +} + +func (x *BeginRootfsTarStagingRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *BeginRootfsTarStagingRequest) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +func (x *BeginRootfsTarStagingRequest) GetSizeBytes() uint64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +// Gateway-issued staging slot. +type BeginRootfsTarStagingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Opaque single-use token. Pass it as + // `template.driver_config..rootfs_tar_staging_token` on + // CreateSandbox. The first CreateSandbox presenting it consumes it. + StagingToken string `protobuf:"bytes,1,opt,name=staging_token,json=stagingToken,proto3" json:"staging_token,omitempty"` + // Absolute path on the gateway host the client must write the archive to. + UploadPath string `protobuf:"bytes,2,opt,name=upload_path,json=uploadPath,proto3" json:"upload_path,omitempty"` + // Maximum accepted archive size in bytes, enforced again by the driver. + MaxBytes uint64 `protobuf:"varint,3,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` + // Wall-clock deadline after which the gateway reclaims the slot. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,104,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BeginRootfsTarStagingResponse) Reset() { + *x = BeginRootfsTarStagingResponse{} + mi := &file_openshell_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BeginRootfsTarStagingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BeginRootfsTarStagingResponse) ProtoMessage() {} + +func (x *BeginRootfsTarStagingResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[41] + 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 BeginRootfsTarStagingResponse.ProtoReflect.Descriptor instead. +func (*BeginRootfsTarStagingResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{41} +} + +func (x *BeginRootfsTarStagingResponse) GetStagingToken() string { + if x != nil { + return x.StagingToken + } + return "" +} + +func (x *BeginRootfsTarStagingResponse) GetUploadPath() string { + if x != nil { + return x.UploadPath + } + return "" +} + +func (x *BeginRootfsTarStagingResponse) GetMaxBytes() uint64 { + if x != nil { + return x.MaxBytes + } + return 0 +} + +func (x *BeginRootfsTarStagingResponse) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +// Get sandbox request. +type GetSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxRequest) Reset() { + *x = GetSandboxRequest{} + mi := &file_openshell_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxRequest) ProtoMessage() {} + +func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[42] + 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 GetSandboxRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{42} +} + +func (x *GetSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// List sandboxes request. +type ListSandboxesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Named and all-workspaces selections are accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of sandboxes to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListSandboxes response. All other request parameters + // except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxesRequest) Reset() { + *x = ListSandboxesRequest{} + mi := &file_openshell_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxesRequest) ProtoMessage() {} + +func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[43] + 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 ListSandboxesRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{43} +} + +func (x *ListSandboxesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListSandboxesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSandboxesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListSandboxesRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +// List providers attached to a sandbox request. +type ListSandboxProvidersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // The maximum number of providers to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListSandboxProviders response. All other request + // parameters except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxProvidersRequest) Reset() { + *x = ListSandboxProvidersRequest{} + mi := &file_openshell_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxProvidersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxProvidersRequest) ProtoMessage() {} + +func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[44] + 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 ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{44} +} + +func (x *ListSandboxProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListSandboxProvidersRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ListSandboxProvidersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSandboxProvidersRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// Attach provider to sandbox request. +type AttachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Provider name to attach. + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachSandboxProviderRequest) Reset() { + *x = AttachSandboxProviderRequest{} + mi := &file_openshell_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachSandboxProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachSandboxProviderRequest) ProtoMessage() {} + +func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[45] + 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 AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{45} +} + +func (x *AttachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *AttachSandboxProviderRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *AttachSandboxProviderRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *AttachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *AttachSandboxProviderRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Detach provider from sandbox request. +type DetachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Provider name to detach. + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachSandboxProviderRequest) Reset() { + *x = DetachSandboxProviderRequest{} + mi := &file_openshell_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachSandboxProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachSandboxProviderRequest) ProtoMessage() {} + +func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[46] + 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 DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{46} +} + +func (x *DetachSandboxProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DetachSandboxProviderRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *DetachSandboxProviderRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *DetachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *DetachSandboxProviderRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Delete sandbox request. +type DeleteSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Canonical sandbox name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Succeed with ALREADY_ABSENT if the target is missing. Does not wait for + // asynchronous cleanup and does not suppress authorization or parent errors. + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxRequest) Reset() { + *x = DeleteSandboxRequest{} + mi := &file_openshell_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxRequest) ProtoMessage() {} + +func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[47] + 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 DeleteSandboxRequest.ProtoReflect.Descriptor instead. +func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{47} +} + +func (x *DeleteSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteSandboxRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Stop sandbox request. +type StopSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StopSandboxRequest) Reset() { + *x = StopSandboxRequest{} + mi := &file_openshell_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StopSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopSandboxRequest) ProtoMessage() {} + +func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[48] + 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 StopSandboxRequest.ProtoReflect.Descriptor instead. +func (*StopSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{48} +} + +func (x *StopSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *StopSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StopSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Start sandbox request. +type StartSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartSandboxRequest) Reset() { + *x = StartSandboxRequest{} + mi := &file_openshell_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartSandboxRequest) ProtoMessage() {} + +func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[49] + 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 StartSandboxRequest.ProtoReflect.Descriptor instead. +func (*StartSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{49} +} + +func (x *StartSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *StartSandboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StartSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Sandbox response. +type SandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service URLs created by CreateSandbox, keyed by service name. The empty + // key identifies the unnamed service. Other sandbox RPCs return an empty map. + ServiceUrls map[string]string `protobuf:"bytes,2,rep,name=service_urls,json=serviceUrls,proto3" json:"service_urls,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxResponse) Reset() { + *x = SandboxResponse{} + mi := &file_openshell_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxResponse) ProtoMessage() {} + +func (x *SandboxResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[50] + 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 SandboxResponse.ProtoReflect.Descriptor instead. +func (*SandboxResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{50} +} + +func (x *SandboxResponse) GetSandbox() *Sandbox { + if x != nil { + return x.Sandbox + } + return nil +} + +func (x *SandboxResponse) GetServiceUrls() map[string]string { + if x != nil { + return x.ServiceUrls + } + return nil +} + +// List sandboxes response. +type ListSandboxesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandboxes []*Sandbox `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxesResponse) Reset() { + *x = ListSandboxesResponse{} + mi := &file_openshell_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxesResponse) ProtoMessage() {} + +func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[51] + 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 ListSandboxesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{51} +} + +func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { + if x != nil { + return x.Sandboxes + } + return nil +} + +func (x *ListSandboxesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// List providers attached to a sandbox response. +type ListSandboxProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxProvidersResponse) Reset() { + *x = ListSandboxProvidersResponse{} + mi := &file_openshell_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxProvidersResponse) ProtoMessage() {} + +func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[52] + 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 ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{52} +} + +func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { + if x != nil { + return x.Providers + } + return nil +} + +func (x *ListSandboxProvidersResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Attach provider to sandbox response. +type AttachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was newly attached. False means it was already attached. + Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` + // Persisted intent; readiness requires current supervisor observations. + Receipt *ProviderMutationReceipt `protobuf:"bytes,3,opt,name=receipt,proto3" json:"receipt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachSandboxProviderResponse) Reset() { + *x = AttachSandboxProviderResponse{} + mi := &file_openshell_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachSandboxProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachSandboxProviderResponse) ProtoMessage() {} + +func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[53] + 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 AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{53} +} + +func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { + if x != nil { + return x.Sandbox + } + return nil +} + +func (x *AttachSandboxProviderResponse) GetAttached() bool { + if x != nil { + return x.Attached + } + return false +} + +func (x *AttachSandboxProviderResponse) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + +// Detach provider from sandbox response. +type DetachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was removed. False means it was not attached. + Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` + // Revocation is complete only when this receipt reports REVOKED. + Receipt *ProviderMutationReceipt `protobuf:"bytes,3,opt,name=receipt,proto3" json:"receipt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachSandboxProviderResponse) Reset() { + *x = DetachSandboxProviderResponse{} + mi := &file_openshell_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachSandboxProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachSandboxProviderResponse) ProtoMessage() {} + +func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[54] + 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 DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{54} +} + +func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { + if x != nil { + return x.Sandbox + } + return nil +} + +func (x *DetachSandboxProviderResponse) GetDetached() bool { + if x != nil { + return x.Detached + } + return false +} + +func (x *DetachSandboxProviderResponse) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + +// Exact desired authority. Revisions are opaque identities, never ordered. +type ProviderDesiredIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + AttachmentEpoch string `protobuf:"bytes,3,opt,name=attachment_epoch,json=attachmentEpoch,proto3" json:"attachment_epoch,omitempty"` + // Empty for a detached provider. + ProviderId string `protobuf:"bytes,4,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + ProviderResourceVersion uint64 `protobuf:"varint,5,opt,name=provider_resource_version,json=providerResourceVersion,proto3" json:"provider_resource_version,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,6,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + ConfigRevision uint64 `protobuf:"varint,7,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyHash string `protobuf:"bytes,8,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderDesiredIdentity) Reset() { + *x = ProviderDesiredIdentity{} + mi := &file_openshell_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderDesiredIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderDesiredIdentity) ProtoMessage() {} + +func (x *ProviderDesiredIdentity) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[55] + 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 ProviderDesiredIdentity.ProtoReflect.Descriptor instead. +func (*ProviderDesiredIdentity) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{55} +} + +func (x *ProviderDesiredIdentity) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ProviderDesiredIdentity) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ProviderDesiredIdentity) GetAttachmentEpoch() string { + if x != nil { + return x.AttachmentEpoch + } + return "" +} + +func (x *ProviderDesiredIdentity) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + +func (x *ProviderDesiredIdentity) GetProviderResourceVersion() uint64 { + if x != nil { + return x.ProviderResourceVersion + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *ProviderDesiredIdentity) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +// Identifies one component snapshot revision. Revisions are equality tokens, +// not members of one shared ordering domain. +type ConfigSnapshotRevision struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Component: + // + // *ConfigSnapshotRevision_SandboxConfig + // *ConfigSnapshotRevision_ProviderEnvironment + // *ConfigSnapshotRevision_ProviderTarget + Component isConfigSnapshotRevision_Component `protobuf_oneof:"component"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigSnapshotRevision) Reset() { + *x = ConfigSnapshotRevision{} + mi := &file_openshell_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigSnapshotRevision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigSnapshotRevision) ProtoMessage() {} + +func (x *ConfigSnapshotRevision) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[56] + 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 ConfigSnapshotRevision.ProtoReflect.Descriptor instead. +func (*ConfigSnapshotRevision) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{56} +} + +func (x *ConfigSnapshotRevision) GetComponent() isConfigSnapshotRevision_Component { + if x != nil { + return x.Component + } + return nil +} + +func (x *ConfigSnapshotRevision) GetSandboxConfig() *SandboxConfigRevision { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_SandboxConfig); ok { + return x.SandboxConfig + } + } + return nil +} + +func (x *ConfigSnapshotRevision) GetProviderEnvironment() uint64 { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_ProviderEnvironment); ok { + return x.ProviderEnvironment + } + } + return 0 +} + +func (x *ConfigSnapshotRevision) GetProviderTarget() *ProviderDesiredIdentity { + if x != nil { + if x, ok := x.Component.(*ConfigSnapshotRevision_ProviderTarget); ok { + return x.ProviderTarget + } + } + return nil +} + +type isConfigSnapshotRevision_Component interface { + isConfigSnapshotRevision_Component() +} + +type ConfigSnapshotRevision_SandboxConfig struct { + SandboxConfig *SandboxConfigRevision `protobuf:"bytes,1,opt,name=sandbox_config,json=sandboxConfig,proto3,oneof"` +} + +type ConfigSnapshotRevision_ProviderEnvironment struct { + ProviderEnvironment uint64 `protobuf:"varint,2,opt,name=provider_environment,json=providerEnvironment,proto3,oneof"` +} + +type ConfigSnapshotRevision_ProviderTarget struct { + // Complete desired authority for one sandbox-scoped provider mutation. + ProviderTarget *ProviderDesiredIdentity `protobuf:"bytes,3,opt,name=provider_target,json=providerTarget,proto3,oneof"` +} + +func (*ConfigSnapshotRevision_SandboxConfig) isConfigSnapshotRevision_Component() {} + +func (*ConfigSnapshotRevision_ProviderEnvironment) isConfigSnapshotRevision_Component() {} + +func (*ConfigSnapshotRevision_ProviderTarget) isConfigSnapshotRevision_Component() {} + +// Identity needed to correlate effective sandbox configuration with the +// policy-history row whose apply status the gateway records. +type SandboxConfigRevision struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigRevision uint64 `protobuf:"varint,1,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyVersion uint32 `protobuf:"varint,2,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + PolicySource sandboxv1.PolicySource `protobuf:"varint,3,opt,name=policy_source,json=policySource,proto3,enum=openshell.sandbox.v1.PolicySource" json:"policy_source,omitempty"` + GlobalPolicyVersion uint32 `protobuf:"varint,4,opt,name=global_policy_version,json=globalPolicyVersion,proto3" json:"global_policy_version,omitempty"` + // Monotonic revision of the sandbox-scoped settings row. This disambiguates + // setting operations whose effective config fingerprint is equality-only. + SettingsRevision uint64 `protobuf:"varint,5,opt,name=settings_revision,json=settingsRevision,proto3" json:"settings_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxConfigRevision) Reset() { + *x = SandboxConfigRevision{} + mi := &file_openshell_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxConfigRevision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxConfigRevision) ProtoMessage() {} + +func (x *SandboxConfigRevision) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[57] + 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 SandboxConfigRevision.ProtoReflect.Descriptor instead. +func (*SandboxConfigRevision) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{57} +} + +func (x *SandboxConfigRevision) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *SandboxConfigRevision) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *SandboxConfigRevision) GetPolicySource() sandboxv1.PolicySource { + if x != nil { + return x.PolicySource + } + return sandboxv1.PolicySource(0) +} + +func (x *SandboxConfigRevision) GetGlobalPolicyVersion() uint32 { + if x != nil { + return x.GlobalPolicyVersion + } + return 0 +} + +func (x *SandboxConfigRevision) GetSettingsRevision() uint64 { + if x != nil { + return x.SettingsRevision + } + return 0 +} + +// Durable progress for one sandbox-scoped desired-state mutation. Snapshot +// contents and credentials are never stored in this resource. +type ConfigUpdateOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + OperationId string `protobuf:"bytes,1,opt,name=operation_id,json=operationId,proto3" json:"operation_id,omitempty"` + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Component ConfigComponent `protobuf:"varint,3,opt,name=component,proto3,enum=openshell.v1.ConfigComponent" json:"component,omitempty"` + TargetRevision *ConfigSnapshotRevision `protobuf:"bytes,4,opt,name=target_revision,json=targetRevision,proto3" json:"target_revision,omitempty"` + State ConfigUpdateOperationState `protobuf:"varint,5,opt,name=state,proto3,enum=openshell.v1.ConfigUpdateOperationState" json:"state,omitempty"` + Outcome ConfigApplyOutcome `protobuf:"varint,6,opt,name=outcome,proto3,enum=openshell.v1.ConfigApplyOutcome" json:"outcome,omitempty"` + SanitizedError string `protobuf:"bytes,7,opt,name=sanitized_error,json=sanitizedError,proto3" json:"sanitized_error,omitempty"` + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + UpdatedTime *timestamppb.Timestamp `protobuf:"bytes,109,opt,name=updated_time,json=updatedTime,proto3" json:"updated_time,omitempty"` + // Absent until the operation reaches a terminal state. + CompletedTime *timestamppb.Timestamp `protobuf:"bytes,110,opt,name=completed_time,json=completedTime,proto3" json:"completed_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigUpdateOperation) Reset() { + *x = ConfigUpdateOperation{} + mi := &file_openshell_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigUpdateOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigUpdateOperation) ProtoMessage() {} + +func (x *ConfigUpdateOperation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[58] + 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 ConfigUpdateOperation.ProtoReflect.Descriptor instead. +func (*ConfigUpdateOperation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{58} +} + +func (x *ConfigUpdateOperation) GetOperationId() string { + if x != nil { + return x.OperationId + } + return "" +} + +func (x *ConfigUpdateOperation) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ConfigUpdateOperation) GetComponent() ConfigComponent { + if x != nil { + return x.Component + } + return ConfigComponent_CONFIG_COMPONENT_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetTargetRevision() *ConfigSnapshotRevision { + if x != nil { + return x.TargetRevision + } + return nil +} + +func (x *ConfigUpdateOperation) GetState() ConfigUpdateOperationState { + if x != nil { + return x.State + } + return ConfigUpdateOperationState_CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetOutcome() ConfigApplyOutcome { + if x != nil { + return x.Outcome + } + return ConfigApplyOutcome_CONFIG_APPLY_OUTCOME_UNSPECIFIED +} + +func (x *ConfigUpdateOperation) GetSanitizedError() string { + if x != nil { + return x.SanitizedError + } + return "" +} + +func (x *ConfigUpdateOperation) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *ConfigUpdateOperation) GetUpdatedTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedTime + } + return nil +} + +func (x *ConfigUpdateOperation) GetCompletedTime() *timestamppb.Timestamp { + if x != nil { + return x.CompletedTime + } + return nil +} + +// Immutable, secret-free record of one sandbox's intended provider mutation. +type ProviderMutationReceipt struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReceiptId string `protobuf:"bytes,1,opt,name=receipt_id,json=receiptId,proto3" json:"receipt_id,omitempty"` + // Shared by all sandbox receipts from one provider update. + MutationId string `protobuf:"bytes,2,opt,name=mutation_id,json=mutationId,proto3" json:"mutation_id,omitempty"` + Provider string `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"` + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` + Kind ProviderMutationKind `protobuf:"varint,5,opt,name=kind,proto3,enum=openshell.v1.ProviderMutationKind" json:"kind,omitempty"` + Desired *ProviderDesiredIdentity `protobuf:"bytes,6,opt,name=desired,proto3" json:"desired,omitempty"` + PersistedTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=persisted_time,json=persistedTime,proto3" json:"persisted_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderMutationReceipt) Reset() { + *x = ProviderMutationReceipt{} + mi := &file_openshell_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderMutationReceipt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderMutationReceipt) ProtoMessage() {} + +func (x *ProviderMutationReceipt) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[59] + 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 ProviderMutationReceipt.ProtoReflect.Descriptor instead. +func (*ProviderMutationReceipt) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{59} +} + +func (x *ProviderMutationReceipt) GetReceiptId() string { + if x != nil { + return x.ReceiptId + } + return "" +} + +func (x *ProviderMutationReceipt) GetMutationId() string { + if x != nil { + return x.MutationId + } + return "" +} + +func (x *ProviderMutationReceipt) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ProviderMutationReceipt) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ProviderMutationReceipt) GetKind() ProviderMutationKind { + if x != nil { + return x.Kind + } + return ProviderMutationKind_PROVIDER_MUTATION_KIND_UNSPECIFIED +} + +func (x *ProviderMutationReceipt) GetDesired() *ProviderDesiredIdentity { + if x != nil { + return x.Desired + } + return nil +} + +func (x *ProviderMutationReceipt) GetPersistedTime() *timestamppb.Timestamp { + if x != nil { + return x.PersistedTime + } + return nil +} + +// Installed state reported by the current supervisor. Process installation is +// acknowledged by the authenticated sandbox boundary after replacing its +// environment for future process launches. +type ProviderReadinessObservation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-issued identifier from the current ConnectSupervisor response. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Monotonic only within this connection; unrelated to revision fingerprints. + Sequence uint64 `protobuf:"varint,2,opt,name=sequence,proto3" json:"sequence,omitempty"` + AttachmentEpoch string `protobuf:"bytes,3,opt,name=attachment_epoch,json=attachmentEpoch,proto3" json:"attachment_epoch,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,4,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + ConfigRevision uint64 `protobuf:"varint,5,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + PolicyHash string `protobuf:"bytes,6,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + CredentialsInstalled bool `protobuf:"varint,7,opt,name=credentials_installed,json=credentialsInstalled,proto3" json:"credentials_installed,omitempty"` + PolicyActive bool `protobuf:"varint,8,opt,name=policy_active,json=policyActive,proto3" json:"policy_active,omitempty"` + LaunchEnvironmentInstalled bool `protobuf:"varint,9,opt,name=launch_environment_installed,json=launchEnvironmentInstalled,proto3" json:"launch_environment_installed,omitempty"` + ProcessInstanceId string `protobuf:"bytes,10,opt,name=process_instance_id,json=processInstanceId,proto3" json:"process_instance_id,omitempty"` + Reason ProviderReadinessReason `protobuf:"varint,11,opt,name=reason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderReadinessObservation) Reset() { + *x = ProviderReadinessObservation{} + mi := &file_openshell_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderReadinessObservation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderReadinessObservation) ProtoMessage() {} + +func (x *ProviderReadinessObservation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[60] + 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 ProviderReadinessObservation.ProtoReflect.Descriptor instead. +func (*ProviderReadinessObservation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{60} +} + +func (x *ProviderReadinessObservation) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *ProviderReadinessObservation) GetSequence() uint64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *ProviderReadinessObservation) GetAttachmentEpoch() string { + if x != nil { + return x.AttachmentEpoch + } + return "" +} + +func (x *ProviderReadinessObservation) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *ProviderReadinessObservation) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *ProviderReadinessObservation) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *ProviderReadinessObservation) GetCredentialsInstalled() bool { + if x != nil { + return x.CredentialsInstalled + } + return false +} + +func (x *ProviderReadinessObservation) GetPolicyActive() bool { + if x != nil { + return x.PolicyActive + } + return false +} + +func (x *ProviderReadinessObservation) GetLaunchEnvironmentInstalled() bool { + if x != nil { + return x.LaunchEnvironmentInstalled + } + return false +} + +func (x *ProviderReadinessObservation) GetProcessInstanceId() string { + if x != nil { + return x.ProcessInstanceId + } + return "" +} + +func (x *ProviderReadinessObservation) GetReason() ProviderReadinessReason { + if x != nil { + return x.Reason + } + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED +} + +// Operator view of desired and observed state; contains no credential material. +type ProviderReadinessStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Receipt *ProviderMutationReceipt `protobuf:"bytes,1,opt,name=receipt,proto3" json:"receipt,omitempty"` + State ProviderReadinessState `protobuf:"varint,2,opt,name=state,proto3,enum=openshell.v1.ProviderReadinessState" json:"state,omitempty"` + Reason ProviderReadinessReason `protobuf:"varint,3,opt,name=reason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"reason,omitempty"` + Observed *ProviderReadinessObservation `protobuf:"bytes,4,opt,name=observed,proto3" json:"observed,omitempty"` + NetworkInstanceId string `protobuf:"bytes,5,opt,name=network_instance_id,json=networkInstanceId,proto3" json:"network_instance_id,omitempty"` + // Absent until the current supervisor session supplies accepted evidence. + ObservedTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=observed_time,json=observedTime,proto3" json:"observed_time,omitempty"` + EvaluatedTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=evaluated_time,json=evaluatedTime,proto3" json:"evaluated_time,omitempty"` + // Durable operation for the receipt, including its terminal apply outcome. + Operation *ConfigUpdateOperation `protobuf:"bytes,8,opt,name=operation,proto3" json:"operation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderReadinessStatus) Reset() { + *x = ProviderReadinessStatus{} + mi := &file_openshell_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderReadinessStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderReadinessStatus) ProtoMessage() {} + +func (x *ProviderReadinessStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[61] + 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 ProviderReadinessStatus.ProtoReflect.Descriptor instead. +func (*ProviderReadinessStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{61} +} + +func (x *ProviderReadinessStatus) GetReceipt() *ProviderMutationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + +func (x *ProviderReadinessStatus) GetState() ProviderReadinessState { + if x != nil { + return x.State + } + return ProviderReadinessState_PROVIDER_READINESS_STATE_UNSPECIFIED +} + +func (x *ProviderReadinessStatus) GetReason() ProviderReadinessReason { + if x != nil { + return x.Reason + } + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED +} + +func (x *ProviderReadinessStatus) GetObserved() *ProviderReadinessObservation { + if x != nil { + return x.Observed + } + return nil +} + +func (x *ProviderReadinessStatus) GetNetworkInstanceId() string { + if x != nil { + return x.NetworkInstanceId + } + return "" +} + +func (x *ProviderReadinessStatus) GetObservedTime() *timestamppb.Timestamp { + if x != nil { + return x.ObservedTime + } + return nil +} + +func (x *ProviderReadinessStatus) GetEvaluatedTime() *timestamppb.Timestamp { + if x != nil { + return x.EvaluatedTime + } + return nil +} + +func (x *ProviderReadinessStatus) GetOperation() *ConfigUpdateOperation { + if x != nil { + return x.Operation + } + return nil +} + +// Query an immutable receipt, or reconstruct the current desired state when +// receipt_id is empty. The sandbox identity must match the receipt. +type GetSandboxProviderStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + ReceiptId string `protobuf:"bytes,3,opt,name=receipt_id,json=receiptId,proto3" json:"receipt_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderStatusRequest) Reset() { + *x = GetSandboxProviderStatusRequest{} + mi := &file_openshell_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderStatusRequest) ProtoMessage() {} + +func (x *GetSandboxProviderStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[62] + 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 GetSandboxProviderStatusRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{62} +} + +func (x *GetSandboxProviderStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxProviderStatusRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *GetSandboxProviderStatusRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *GetSandboxProviderStatusRequest) GetReceiptId() string { + if x != nil { + return x.ReceiptId + } + return "" +} + +type GetSandboxProviderStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *ProviderReadinessStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderStatusResponse) Reset() { + *x = GetSandboxProviderStatusResponse{} + mi := &file_openshell_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderStatusResponse) ProtoMessage() {} + +func (x *GetSandboxProviderStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[63] + 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 GetSandboxProviderStatusResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{63} +} + +func (x *GetSandboxProviderStatusResponse) GetStatus() *ProviderReadinessStatus { + if x != nil { + return x.Status + } + return nil +} + +// Installation evidence from the authenticated supervisor for this sandbox. +// A caller-provided instance identifier alone never establishes authority. +type ReportProviderReadinessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Observation *ProviderReadinessObservation `protobuf:"bytes,2,opt,name=observation,proto3" json:"observation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportProviderReadinessRequest) Reset() { + *x = ReportProviderReadinessRequest{} + mi := &file_openshell_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportProviderReadinessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportProviderReadinessRequest) ProtoMessage() {} + +func (x *ReportProviderReadinessRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[64] + 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 ReportProviderReadinessRequest.ProtoReflect.Descriptor instead. +func (*ReportProviderReadinessRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{64} +} + +func (x *ReportProviderReadinessRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportProviderReadinessRequest) GetObservation() *ProviderReadinessObservation { + if x != nil { + return x.Observation + } + return nil +} + +// Acknowledges accepted evidence without granting a separate session authority. +// Identical retries do not extend the evidence's original acceptance time. +type ReportProviderReadinessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AcceptedSequence uint64 `protobuf:"varint,1,opt,name=accepted_sequence,json=acceptedSequence,proto3" json:"accepted_sequence,omitempty"` + ReportInterval *durationpb.Duration `protobuf:"bytes,102,opt,name=report_interval,json=reportInterval,proto3" json:"report_interval,omitempty"` + ObservationTtl *durationpb.Duration `protobuf:"bytes,103,opt,name=observation_ttl,json=observationTtl,proto3" json:"observation_ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportProviderReadinessResponse) Reset() { + *x = ReportProviderReadinessResponse{} + mi := &file_openshell_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportProviderReadinessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportProviderReadinessResponse) ProtoMessage() {} + +func (x *ReportProviderReadinessResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[65] + 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 ReportProviderReadinessResponse.ProtoReflect.Descriptor instead. +func (*ReportProviderReadinessResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{65} +} + +func (x *ReportProviderReadinessResponse) GetAcceptedSequence() uint64 { + if x != nil { + return x.AcceptedSequence + } + return 0 +} + +func (x *ReportProviderReadinessResponse) GetReportInterval() *durationpb.Duration { + if x != nil { + return x.ReportInterval + } + return nil +} + +func (x *ReportProviderReadinessResponse) GetObservationTtl() *durationpb.Duration { + if x != nil { + return x.ObservationTtl + } + return nil +} + +// Delete sandbox response. +type DeleteSandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + // Immutable identity of the targeted sandbox, empty for ALREADY_ABSENT. + // A same-name replacement is not part of this deletion. + SandboxId string `protobuf:"bytes,3,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxResponse) Reset() { + *x = DeleteSandboxResponse{} + mi := &file_openshell_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSandboxResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSandboxResponse) ProtoMessage() {} + +func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[66] + 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 DeleteSandboxResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{66} +} + +func (x *DeleteSandboxResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +func (x *DeleteSandboxResponse) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +// Create SSH session request. +type CreateSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSshSessionRequest) Reset() { + *x = CreateSshSessionRequest{} + mi := &file_openshell_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSshSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSshSessionRequest) ProtoMessage() {} + +func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[67] + 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 CreateSshSessionRequest.ProtoReflect.Descriptor instead. +func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{67} +} + +func (x *CreateSshSessionRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *CreateSshSessionRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +// Create SSH session response. +// +// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH +// executes through `/bin/sh -c` on the caller's workstation. Servers MUST +// uphold the charset contract below; clients MUST reject responses that +// violate it. The client's own escaping provides defense-in-depth, but +// narrow charsets close injection vectors at the trust boundary. +type CreateSshSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. [A-Za-z0-9._-]{1,128}. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token for the gateway tunnel. URL-safe ASCII + // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or + // whitespace. + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 + // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus + // `.-:[]` only, up to 253 bytes. + GatewayHost string `protobuf:"bytes,3,opt,name=gateway_host,json=gatewayHost,proto3" json:"gateway_host,omitempty"` + // Gateway port for SSH proxy connection. Must be in range 1..=65535. + GatewayPort uint32 `protobuf:"varint,4,opt,name=gateway_port,json=gatewayPort,proto3" json:"gateway_port,omitempty"` + // Gateway scheme. Must be exactly "http" or "https". + GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` + // Expected sandbox SSH host identity in OpenSSH SHA256: form. + // Stable for the sandbox's lifetime. Empty only for legacy runtimes. + HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` + // Absolute expiry. Absence means no expiry. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSshSessionResponse) Reset() { + *x = CreateSshSessionResponse{} + mi := &file_openshell_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSshSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSshSessionResponse) ProtoMessage() {} + +func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[68] + 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 CreateSshSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{68} +} + +func (x *CreateSshSessionResponse) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *CreateSshSessionResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *CreateSshSessionResponse) GetGatewayHost() string { + if x != nil { + return x.GatewayHost + } + return "" +} + +func (x *CreateSshSessionResponse) GetGatewayPort() uint32 { + if x != nil { + return x.GatewayPort + } + return 0 +} + +func (x *CreateSshSessionResponse) GetGatewayScheme() string { + if x != nil { + return x.GatewayScheme + } + return "" +} + +func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { + if x != nil { + return x.HostKeyFingerprint + } + return "" +} + +func (x *CreateSshSessionResponse) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +// Request to expose an HTTP service running inside a sandbox. +type ExposeServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Service name within the sandbox. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,3,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether to print/use the browser-facing service URL. + Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,6,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Application authorization behavior. Omission resolves to STRIP. + AuthorizationMode ServiceAuthorizationMode `protobuf:"varint,7,opt,name=authorization_mode,json=authorizationMode,proto3,enum=openshell.v1.ServiceAuthorizationMode" json:"authorization_mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExposeServiceRequest) Reset() { + *x = ExposeServiceRequest{} + mi := &file_openshell_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExposeServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceRequest) ProtoMessage() {} + +func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[69] + 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 ExposeServiceRequest.ProtoReflect.Descriptor instead. +func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{69} +} + +func (x *ExposeServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ExposeServiceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ExposeServiceRequest) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + +func (x *ExposeServiceRequest) GetDomain() bool { + if x != nil { + return x.Domain + } + return false +} + +func (x *ExposeServiceRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ExposeServiceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ExposeServiceRequest) GetAuthorizationMode() ServiceAuthorizationMode { + if x != nil { + return x.AuthorizationMode + } + return ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_UNSPECIFIED +} + +// Request to fetch an exposed sandbox service endpoint. +type GetServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServiceRequest) Reset() { + *x = GetServiceRequest{} + mi := &file_openshell_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceRequest) ProtoMessage() {} + +func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[70] + 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 GetServiceRequest.ProtoReflect.Descriptor instead. +func (*GetServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{70} +} + +func (x *GetServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetServiceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetServiceRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +// Request to list exposed sandbox service endpoints. +type ListServicesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Named and all-workspaces selections are accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of services to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListServices response. All other request parameters + // except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional sandbox name. Empty lists endpoints for all sandboxes. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListServicesRequest) Reset() { + *x = ListServicesRequest{} + mi := &file_openshell_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListServicesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServicesRequest) ProtoMessage() {} + +func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[71] + 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 ListServicesRequest.ProtoReflect.Descriptor instead. +func (*ListServicesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{71} +} + +func (x *ListServicesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListServicesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListServicesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListServicesRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +// Response containing exposed sandbox service endpoints. +type ListServicesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Services []*ServiceEndpointResponse `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListServicesResponse) Reset() { + *x = ListServicesResponse{} + mi := &file_openshell_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListServicesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServicesResponse) ProtoMessage() {} + +func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[72] + 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 ListServicesResponse.ProtoReflect.Descriptor instead. +func (*ListServicesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{72} +} + +func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { + if x != nil { + return x.Services + } + return nil +} + +func (x *ListServicesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Request to delete an exposed sandbox service endpoint. +type DeleteServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + AllowMissing bool `protobuf:"varint,4,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteServiceRequest) Reset() { + *x = DeleteServiceRequest{} + mi := &file_openshell_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteServiceRequest) ProtoMessage() {} + +func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[73] + 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 DeleteServiceRequest.ProtoReflect.Descriptor instead. +func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{73} +} + +func (x *DeleteServiceRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteServiceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteServiceRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *DeleteServiceRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteServiceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Response for deleting an exposed sandbox service endpoint. +type DeleteServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteServiceResponse) Reset() { + *x = DeleteServiceResponse{} + mi := &file_openshell_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteServiceResponse) ProtoMessage() {} + +func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[74] + 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 DeleteServiceResponse.ProtoReflect.Descriptor instead. +func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{74} +} + +func (x *DeleteServiceResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Persisted sandbox service endpoint. +type ServiceEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata. + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox object ID. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Sandbox name. + Sandbox string `protobuf:"bytes,3,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,5,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether browser-facing service routing is enabled for this endpoint. + Domain bool `protobuf:"varint,6,opt,name=domain,proto3" json:"domain,omitempty"` + // Effective application authorization behavior for ingress requests. + AuthorizationMode ServiceAuthorizationMode `protobuf:"varint,7,opt,name=authorization_mode,json=authorizationMode,proto3,enum=openshell.v1.ServiceAuthorizationMode" json:"authorization_mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceEndpoint) Reset() { + *x = ServiceEndpoint{} + mi := &file_openshell_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceEndpoint) ProtoMessage() {} + +func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[75] + 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 ServiceEndpoint.ProtoReflect.Descriptor instead. +func (*ServiceEndpoint) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{75} +} + +func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *ServiceEndpoint) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ServiceEndpoint) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ServiceEndpoint) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ServiceEndpoint) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + +func (x *ServiceEndpoint) GetDomain() bool { + if x != nil { + return x.Domain + } + return false +} + +func (x *ServiceEndpoint) GetAuthorizationMode() ServiceAuthorizationMode { + if x != nil { + return x.AuthorizationMode + } + return ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_UNSPECIFIED +} + +// Response containing a service endpoint and, when available, its local URL. +type ServiceEndpointResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoint *ServiceEndpoint `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceEndpointResponse) Reset() { + *x = ServiceEndpointResponse{} + mi := &file_openshell_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceEndpointResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceEndpointResponse) ProtoMessage() {} + +func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[76] + 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 ServiceEndpointResponse.ProtoReflect.Descriptor instead. +func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{76} +} + +func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { + if x != nil { + return x.Endpoint + } + return nil +} + +func (x *ServiceEndpointResponse) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +// Revoke SSH session request. +type RevokeSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Session token to revoke. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + // A missing token is NOT_FOUND unless this is true. Revoking an existing, + // already-revoked session succeeds with COMPLETED. + AllowMissing bool `protobuf:"varint,2,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeSshSessionRequest) Reset() { + *x = RevokeSshSessionRequest{} + mi := &file_openshell_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeSshSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSshSessionRequest) ProtoMessage() {} + +func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[77] + 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 RevokeSshSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{77} +} + +func (x *RevokeSshSessionRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *RevokeSshSessionRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +// Revoke SSH session response. +type RevokeSshSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeSshSessionResponse) Reset() { + *x = RevokeSshSessionResponse{} + mi := &file_openshell_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeSshSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSshSessionResponse) ProtoMessage() {} + +func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[78] + 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 RevokeSshSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{78} +} + +func (x *RevokeSshSessionResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Execute command request. +type ExecSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,12,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Canonical sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Command and arguments. + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + // Optional working directory. + Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` + // Optional environment overrides. + Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional execution timeout. Absence means no timeout. + ExecutionTimeout *durationpb.Duration `protobuf:"bytes,105,opt,name=execution_timeout,json=executionTimeout,proto3" json:"execution_timeout,omitempty"` + // Optional stdin payload passed to the command. + Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` + // Request a pseudo-terminal for the remote command. + Tty bool `protobuf:"varint,7,opt,name=tty,proto3" json:"tty,omitempty"` + // Initial terminal columns (used when tty=true, 0 = use default). + Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` + // Initial terminal rows (used when tty=true, 0 = use default). + Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` + // Skip sourcing shell login/profile startup files before running the command. + // When false (the default), the command runs through a login shell + // (`bash -lc`) so user startup files (.bash_profile/.profile, and .bashrc if + // sourced by them) are applied. When true, the command runs without those + // files (`bash -c`), for automation that needs predictable startup behavior. + NoLoginShell bool `protobuf:"varint,10,opt,name=no_login_shell,json=noLoginShell,proto3" json:"no_login_shell,omitempty"` + // Optional nonzero UUID for durable launch admission. Also applies to the + // initial ExecSandboxInteractive start message. Duplicates never relaunch, + // reattach, or replay output/stdin. Unconfirmed executions remain fenced; + // confirmed terminal executions retain the fence for 24 hours. + RequestId string `protobuf:"bytes,11,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxRequest) Reset() { + *x = ExecSandboxRequest{} + mi := &file_openshell_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxRequest) ProtoMessage() {} + +func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[79] + 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 ExecSandboxRequest.ProtoReflect.Descriptor instead. +func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{79} +} + +func (x *ExecSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ExecSandboxRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ExecSandboxRequest) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ExecSandboxRequest) GetWorkdir() string { + if x != nil { + return x.Workdir + } + return "" +} + +func (x *ExecSandboxRequest) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *ExecSandboxRequest) GetExecutionTimeout() *durationpb.Duration { + if x != nil { + return x.ExecutionTimeout + } + return nil +} + +func (x *ExecSandboxRequest) GetStdin() []byte { + if x != nil { + return x.Stdin + } + return nil +} + +func (x *ExecSandboxRequest) GetTty() bool { + if x != nil { + return x.Tty + } + return false +} + +func (x *ExecSandboxRequest) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *ExecSandboxRequest) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + +func (x *ExecSandboxRequest) GetNoLoginShell() bool { + if x != nil { + return x.NoLoginShell + } + return false +} + +func (x *ExecSandboxRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// One stdout chunk from a sandbox exec. +type ExecSandboxStdout struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxStdout) Reset() { + *x = ExecSandboxStdout{} + mi := &file_openshell_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxStdout) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxStdout) ProtoMessage() {} + +func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[80] + 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 ExecSandboxStdout.ProtoReflect.Descriptor instead. +func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{80} +} + +func (x *ExecSandboxStdout) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// One stderr chunk from a sandbox exec. +type ExecSandboxStderr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxStderr) Reset() { + *x = ExecSandboxStderr{} + mi := &file_openshell_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxStderr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxStderr) ProtoMessage() {} + +func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[81] + 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 ExecSandboxStderr.ProtoReflect.Descriptor instead. +func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{81} +} + +func (x *ExecSandboxStderr) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// Final exit status for a sandbox exec. +type ExecSandboxExit struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxExit) Reset() { + *x = ExecSandboxExit{} + mi := &file_openshell_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxExit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxExit) ProtoMessage() {} + +func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[82] + 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 ExecSandboxExit.ProtoReflect.Descriptor instead. +func (*ExecSandboxExit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{82} +} + +func (x *ExecSandboxExit) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +// One event in a sandbox exec stream. +type ExecSandboxEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ExecSandboxEvent_Stdout + // *ExecSandboxEvent_Stderr + // *ExecSandboxEvent_Exit + Payload isExecSandboxEvent_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxEvent) Reset() { + *x = ExecSandboxEvent{} + mi := &file_openshell_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxEvent) ProtoMessage() {} + +func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[83] + 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 ExecSandboxEvent.ProtoReflect.Descriptor instead. +func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{83} +} + +func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ExecSandboxEvent) GetStdout() *ExecSandboxStdout { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Stdout); ok { + return x.Stdout + } + } + return nil +} + +func (x *ExecSandboxEvent) GetStderr() *ExecSandboxStderr { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Stderr); ok { + return x.Stderr + } + } + return nil +} + +func (x *ExecSandboxEvent) GetExit() *ExecSandboxExit { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Exit); ok { + return x.Exit + } + } + return nil +} + +type isExecSandboxEvent_Payload interface { + isExecSandboxEvent_Payload() +} + +type ExecSandboxEvent_Stdout struct { + Stdout *ExecSandboxStdout `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` +} + +type ExecSandboxEvent_Stderr struct { + Stderr *ExecSandboxStderr `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` +} + +type ExecSandboxEvent_Exit struct { + Exit *ExecSandboxExit `protobuf:"bytes,3,opt,name=exit,proto3,oneof"` +} + +func (*ExecSandboxEvent_Stdout) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Stderr) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} + +// Initial frame for one TCP forward stream. +type TcpForwardInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Optional service identifier for audit/correlation. + ServiceId string `protobuf:"bytes,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + // Target the gateway should request from the supervisor. + // + // Types that are valid to be assigned to Target: + // + // *TcpForwardInit_Ssh + // *TcpForwardInit_Tcp + Target isTcpForwardInit_Target `protobuf_oneof:"target"` + // Optional target-specific authorization token. SSH targets use this as the + // short-lived SSH session token issued by CreateSshSession. + AuthorizationToken string `protobuf:"bytes,7,opt,name=authorization_token,json=authorizationToken,proto3" json:"authorization_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpForwardInit) Reset() { + *x = TcpForwardInit{} + mi := &file_openshell_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpForwardInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardInit) ProtoMessage() {} + +func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[84] + 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 TcpForwardInit.ProtoReflect.Descriptor instead. +func (*TcpForwardInit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{84} +} + +func (x *TcpForwardInit) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *TcpForwardInit) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *TcpForwardInit) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *TcpForwardInit) GetTarget() isTcpForwardInit_Target { + if x != nil { + return x.Target + } + return nil +} + +func (x *TcpForwardInit) GetSsh() *SshRelayTarget { + if x != nil { + if x, ok := x.Target.(*TcpForwardInit_Ssh); ok { + return x.Ssh + } + } + return nil +} + +func (x *TcpForwardInit) GetTcp() *TcpRelayTarget { + if x != nil { + if x, ok := x.Target.(*TcpForwardInit_Tcp); ok { + return x.Tcp + } + } + return nil +} + +func (x *TcpForwardInit) GetAuthorizationToken() string { + if x != nil { + return x.AuthorizationToken + } + return "" +} + +type isTcpForwardInit_Target interface { + isTcpForwardInit_Target() +} + +type TcpForwardInit_Ssh struct { + Ssh *SshRelayTarget `protobuf:"bytes,5,opt,name=ssh,proto3,oneof"` +} + +type TcpForwardInit_Tcp struct { + Tcp *TcpRelayTarget `protobuf:"bytes,6,opt,name=tcp,proto3,oneof"` +} + +func (*TcpForwardInit_Ssh) isTcpForwardInit_Target() {} + +func (*TcpForwardInit_Tcp) isTcpForwardInit_Target() {} + +// A single frame on the CLI-to-gateway TCP forward stream. +type TcpForwardFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *TcpForwardFrame_Init + // *TcpForwardFrame_Data + Payload isTcpForwardFrame_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpForwardFrame) Reset() { + *x = TcpForwardFrame{} + mi := &file_openshell_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpForwardFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardFrame) ProtoMessage() {} + +func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[85] + 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 TcpForwardFrame.ProtoReflect.Descriptor instead. +func (*TcpForwardFrame) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{85} +} + +func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *TcpForwardFrame) GetInit() *TcpForwardInit { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Init); ok { + return x.Init + } + } + return nil +} + +func (x *TcpForwardFrame) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Data); ok { + return x.Data + } + } + return nil +} + +type isTcpForwardFrame_Payload interface { + isTcpForwardFrame_Payload() +} + +type TcpForwardFrame_Init struct { + Init *TcpForwardInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type TcpForwardFrame_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*TcpForwardFrame_Init) isTcpForwardFrame_Payload() {} + +func (*TcpForwardFrame_Data) isTcpForwardFrame_Payload() {} + +// Client-to-server message for interactive exec. +type ExecSandboxInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ExecSandboxInput_Start + // *ExecSandboxInput_Stdin + // *ExecSandboxInput_Resize + Payload isExecSandboxInput_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxInput) Reset() { + *x = ExecSandboxInput{} + mi := &file_openshell_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxInput) ProtoMessage() {} + +func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[86] + 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 ExecSandboxInput.ProtoReflect.Descriptor instead. +func (*ExecSandboxInput) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{86} +} + +func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ExecSandboxInput) GetStart() *ExecSandboxRequest { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Start); ok { + return x.Start + } + } + return nil +} + +func (x *ExecSandboxInput) GetStdin() []byte { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Stdin); ok { + return x.Stdin + } + } + return nil +} + +func (x *ExecSandboxInput) GetResize() *ExecSandboxWindowResize { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Resize); ok { + return x.Resize + } + } + return nil +} + +type isExecSandboxInput_Payload interface { + isExecSandboxInput_Payload() +} + +type ExecSandboxInput_Start struct { + // First message: exec request metadata. + Start *ExecSandboxRequest `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +} + +type ExecSandboxInput_Stdin struct { + // Subsequent messages: raw stdin bytes. + Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` +} + +type ExecSandboxInput_Resize struct { + // Terminal window size change. + Resize *ExecSandboxWindowResize `protobuf:"bytes,3,opt,name=resize,proto3,oneof"` +} + +func (*ExecSandboxInput_Start) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Stdin) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Resize) isExecSandboxInput_Payload() {} + +// Terminal window resize event for interactive exec. +type ExecSandboxWindowResize struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"` + Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxWindowResize) Reset() { + *x = ExecSandboxWindowResize{} + mi := &file_openshell_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxWindowResize) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxWindowResize) ProtoMessage() {} + +func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[87] + 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 ExecSandboxWindowResize.ProtoReflect.Descriptor instead. +func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{87} +} + +func (x *ExecSandboxWindowResize) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *ExecSandboxWindowResize) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + +// SSH session record stored in persistence. +type SshSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox id. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + // Absolute expiry. Absence means no expiry. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,104,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // Revoked flag. + Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SshSession) Reset() { + *x = SshSession{} + mi := &file_openshell_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SshSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshSession) ProtoMessage() {} + +func (x *SshSession) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[88] + 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 SshSession.ProtoReflect.Descriptor instead. +func (*SshSession) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{88} +} + +func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SshSession) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SshSession) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *SshSession) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +func (x *SshSession) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +// Watch sandbox request. +type WatchSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,11,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Canonical sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Stream sandbox status snapshots. + FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` + // Stream openshell-server process logs correlated to this sandbox. + FollowLogs bool `protobuf:"varint,3,opt,name=follow_logs,json=followLogs,proto3" json:"follow_logs,omitempty"` + // Stream platform events correlated to this sandbox. + FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` + // Replay the last N log lines (best-effort) before following. + LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` + // Replay the last N platform events (best-effort) before following. + EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` + // Stop streaming once the sandbox reaches READY or a terminal result phase + // (COMPLETED, STOPPED, or ERROR). + StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` + // Only include log lines at or after this time. Absence means no time filter. + // Applies to both tail replay and live streaming. + SinceTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=since_time,json=sinceTime,proto3" json:"since_time,omitempty"` + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` + // Resume streaming after this cursor. Empty means no cursor resume: the + // server falls back to tail-limited replay controlled by log_tail_lines and + // event_tail. Otherwise set it to the highest `SandboxStreamEvent.cursor` + // already processed; the server replays only log and platform events after + // it, merged in cursor order, before resuming live delivery. If the requested + // cursor has already been trimmed from the server's buffer, the resume is + // unrecoverable and the stream terminates with OUT_OF_RANGE (see + // SandboxStreamWarning for the recoverable case). + // + // A cursor is bound to the cursor space that issued it. A gateway restart, + // teardown of the sandbox's buffers, or a reconnect to a different gateway + // replica starts a new space, and cursors from the previous one are rejected + // with OUT_OF_RANGE rather than silently suppressing live events beneath + // them. OUT_OF_RANGE is terminal for that cursor: restart the watch with an + // empty resume_after_cursor, because retrying the same token fails + // identically. A cursor this server could not have issued is rejected with + // INVALID_ARGUMENT. + ResumeAfterCursor string `protobuf:"bytes,12,opt,name=resume_after_cursor,json=resumeAfterCursor,proto3" json:"resume_after_cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSandboxRequest) Reset() { + *x = WatchSandboxRequest{} + mi := &file_openshell_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSandboxRequest) ProtoMessage() {} + +func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[89] + 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 WatchSandboxRequest.ProtoReflect.Descriptor instead. +func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{89} +} + +func (x *WatchSandboxRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *WatchSandboxRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *WatchSandboxRequest) GetFollowStatus() bool { + if x != nil { + return x.FollowStatus + } + return false +} + +func (x *WatchSandboxRequest) GetFollowLogs() bool { + if x != nil { + return x.FollowLogs + } + return false +} + +func (x *WatchSandboxRequest) GetFollowEvents() bool { + if x != nil { + return x.FollowEvents + } + return false +} + +func (x *WatchSandboxRequest) GetLogTailLines() uint32 { + if x != nil { + return x.LogTailLines + } + return 0 +} + +func (x *WatchSandboxRequest) GetEventTail() uint32 { + if x != nil { + return x.EventTail + } + return 0 +} + +func (x *WatchSandboxRequest) GetStopOnTerminal() bool { + if x != nil { + return x.StopOnTerminal + } + return false +} + +func (x *WatchSandboxRequest) GetSinceTime() *timestamppb.Timestamp { + if x != nil { + return x.SinceTime + } + return nil +} + +func (x *WatchSandboxRequest) GetLogSources() []string { + if x != nil { + return x.LogSources + } + return nil +} + +func (x *WatchSandboxRequest) GetLogMinLevel() string { + if x != nil { + return x.LogMinLevel + } + return "" +} + +func (x *WatchSandboxRequest) GetResumeAfterCursor() string { + if x != nil { + return x.ResumeAfterCursor + } + return "" +} + +// One event in a sandbox watch stream. +type SandboxStreamEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *SandboxStreamEvent_Sandbox + // *SandboxStreamEvent_Log + // *SandboxStreamEvent_Event + // *SandboxStreamEvent_Warning + // *SandboxStreamEvent_DraftPolicyUpdate + Payload isSandboxStreamEvent_Payload `protobuf_oneof:"payload"` + // Opaque position in this sandbox's cursor space, shared across the resumable + // log and platform event sources. Empty for non-resumable events (status + // snapshots, warnings). + // + // Do not parse this token; its encoding is not part of the contract. The only + // supported operation is comparing two non-empty cursors observed on the same + // stream and keeping the greater one, then passing it as + // WatchSandboxRequest.resume_after_cursor to resume without loss or + // duplication. That comparison is a plain byte-wise string comparison. It is + // well defined only within one stream: a stream never spans two cursor + // spaces, because a reset ends it. + Cursor string `protobuf:"bytes,6,opt,name=cursor,proto3" json:"cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamEvent) Reset() { + *x = SandboxStreamEvent{} + mi := &file_openshell_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamEvent) ProtoMessage() {} + +func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[90] + 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 SandboxStreamEvent.ProtoReflect.Descriptor instead. +func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{90} +} + +func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SandboxStreamEvent) GetSandbox() *Sandbox { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Sandbox); ok { + return x.Sandbox + } + } + return nil +} + +func (x *SandboxStreamEvent) GetLog() *SandboxLogLine { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Log); ok { + return x.Log + } + } + return nil +} + +func (x *SandboxStreamEvent) GetEvent() *PlatformEvent { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Event); ok { + return x.Event + } + } + return nil +} + +func (x *SandboxStreamEvent) GetWarning() *SandboxStreamWarning { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Warning); ok { + return x.Warning + } + } + return nil +} + +func (x *SandboxStreamEvent) GetDraftPolicyUpdate() *DraftPolicyUpdate { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_DraftPolicyUpdate); ok { + return x.DraftPolicyUpdate + } + } + return nil +} + +func (x *SandboxStreamEvent) GetCursor() string { + if x != nil { + return x.Cursor + } + return "" +} + +type isSandboxStreamEvent_Payload interface { + isSandboxStreamEvent_Payload() +} + +type SandboxStreamEvent_Sandbox struct { + // Latest sandbox snapshot. + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3,oneof"` +} + +type SandboxStreamEvent_Log struct { + // One server log line/event. + Log *SandboxLogLine `protobuf:"bytes,2,opt,name=log,proto3,oneof"` +} + +type SandboxStreamEvent_Event struct { + // One platform event. + Event *PlatformEvent `protobuf:"bytes,3,opt,name=event,proto3,oneof"` +} + +type SandboxStreamEvent_Warning struct { + // Recoverable warning from the server, e.g. messages dropped because a + // broadcast receiver lagged. The stream continues after this warning; the + // client can detect the gap from the warning itself. + Warning *SandboxStreamWarning `protobuf:"bytes,4,opt,name=warning,proto3,oneof"` +} + +type SandboxStreamEvent_DraftPolicyUpdate struct { + // Draft policy update notification. + DraftPolicyUpdate *DraftPolicyUpdate `protobuf:"bytes,5,opt,name=draft_policy_update,json=draftPolicyUpdate,proto3,oneof"` +} + +func (*SandboxStreamEvent_Sandbox) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Log) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Event) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Warning) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_DraftPolicyUpdate) isSandboxStreamEvent_Payload() {} + +// Log line correlated to a sandbox. +type SandboxLogLine struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + EventTime *timestamppb.Timestamp `protobuf:"bytes,102,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + // Log source: "gateway" (server-side) or "sandbox" (supervisor). + // Empty is treated as "gateway" for backward compatibility. + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + // Structured key-value fields from the tracing event (e.g. dst_host, action). + Fields map[string]string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxLogLine) Reset() { + *x = SandboxLogLine{} + mi := &file_openshell_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxLogLine) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxLogLine) ProtoMessage() {} + +func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[91] + 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 SandboxLogLine.ProtoReflect.Descriptor instead. +func (*SandboxLogLine) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{91} +} + +func (x *SandboxLogLine) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SandboxLogLine) GetEventTime() *timestamppb.Timestamp { + if x != nil { + return x.EventTime + } + return nil +} + +func (x *SandboxLogLine) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *SandboxLogLine) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *SandboxLogLine) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SandboxLogLine) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SandboxLogLine) GetFields() map[string]string { + if x != nil { + return x.Fields + } + return nil +} + +// Recoverable loss notification on a watch stream. Emitted when the server +// skips ahead after a broadcast lag instead of terminating; the stream keeps +// running. Cursors are opaque, so this message is the only signal that events +// were skipped. Unrecoverable loss (a trimmed or foreign resume cursor) is +// reported as an OUT_OF_RANGE stream status, not this message. +type SandboxStreamWarning struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamWarning) Reset() { + *x = SandboxStreamWarning{} + mi := &file_openshell_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamWarning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamWarning) ProtoMessage() {} + +func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[92] + 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 SandboxStreamWarning.ProtoReflect.Descriptor instead. +func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{92} +} + +func (x *SandboxStreamWarning) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Create provider request. +type CreateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateProviderRequest) Reset() { + *x = CreateProviderRequest{} + mi := &file_openshell_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateProviderRequest) ProtoMessage() {} + +func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[93] + 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 CreateProviderRequest.ProtoReflect.Descriptor instead. +func (*CreateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{93} +} + +func (x *CreateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *CreateProviderRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Get provider request. +type GetProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderRequest) Reset() { + *x = GetProviderRequest{} + mi := &file_openshell_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderRequest) ProtoMessage() {} + +func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[94] + 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 GetProviderRequest.ProtoReflect.Descriptor instead. +func (*GetProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{94} +} + +func (x *GetProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetProviderRequest) GetName() string { + if x != nil { + return x.Name + } + 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"` + // Workspace scope. Named and all-workspaces selections are accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of providers to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListProviders response. All other request parameters + // except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + LabelSelector string `protobuf:"bytes,4,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersRequest) Reset() { + *x = ListProvidersRequest{} + mi := &file_openshell_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersRequest) ProtoMessage() {} + +func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[98] + 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 ListProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListProvidersRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{98} +} + +func (x *ListProvidersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListProvidersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListProvidersRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListProvidersRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +// Update provider request. +type UpdateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Optional per-credential expiry timestamps to merge into the provider. + // Omitted keys are unchanged. Use clear_credential_expiration_keys to remove + // an existing expiry. + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,102,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Credential keys whose existing expiry should be removed. + ClearCredentialExpirationKeys []string `protobuf:"bytes,103,rep,name=clear_credential_expiration_keys,json=clearCredentialExpirationKeys,proto3" json:"clear_credential_expiration_keys,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProviderRequest) Reset() { + *x = UpdateProviderRequest{} + mi := &file_openshell_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProviderRequest) ProtoMessage() {} + +func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[99] + 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 UpdateProviderRequest.ProtoReflect.Descriptor instead. +func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{99} +} + +func (x *UpdateProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *UpdateProviderRequest) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { + if x != nil { + return x.CredentialExpirationTimes + } + return nil +} + +func (x *UpdateProviderRequest) GetClearCredentialExpirationKeys() []string { + if x != nil { + return x.ClearCredentialExpirationKeys + } + return nil +} + +func (x *UpdateProviderRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Delete provider request. +type DeleteProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + AllowMissing bool `protobuf:"varint,4,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderRequest) Reset() { + *x = DeleteProviderRequest{} + mi := &file_openshell_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderRequest) ProtoMessage() {} + +func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[100] + 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 DeleteProviderRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{100} +} + +func (x *DeleteProviderRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteProviderRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteProviderRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Provider response. +type ProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Selection-time sandbox target set for an update, with one receipt per target. + // Sandboxes attached later are outside this operation's readiness result. + TargetReceipts []*ProviderMutationReceipt `protobuf:"bytes,2,rep,name=target_receipts,json=targetReceipts,proto3" json:"target_receipts,omitempty"` + // Identifies the update even when its target set is empty. + MutationId string `protobuf:"bytes,3,opt,name=mutation_id,json=mutationId,proto3" json:"mutation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderResponse) Reset() { + *x = ProviderResponse{} + mi := &file_openshell_proto_msgTypes[101] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderResponse) ProtoMessage() {} + +func (x *ProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[101] + 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 ProviderResponse.ProtoReflect.Descriptor instead. +func (*ProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{101} +} + +func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *ProviderResponse) GetTargetReceipts() []*ProviderMutationReceipt { + if x != nil { + return x.TargetReceipts + } + return nil +} + +func (x *ProviderResponse) GetMutationId() string { + if x != nil { + return x.MutationId + } + return "" +} + +// List providers response. +type ListProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersResponse) Reset() { + *x = ListProvidersResponse{} + mi := &file_openshell_proto_msgTypes[102] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersResponse) ProtoMessage() {} + +func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[102] + 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 ListProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListProvidersResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{102} +} + +func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { + if x != nil { + return x.Providers + } + return nil +} + +func (x *ListProvidersResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// List provider type profiles request. +type ListProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of profiles to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListProviderProfiles response. All other request + // parameters except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProviderProfilesRequest) Reset() { + *x = ListProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[103] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProviderProfilesRequest) ProtoMessage() {} + +func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[103] + 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 ListProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{103} +} + +func (x *ListProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListProviderProfilesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListProviderProfilesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// Fetch provider type profile request. +type GetProviderProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderProfileRequest) Reset() { + *x = GetProviderProfileRequest{} + mi := &file_openshell_proto_msgTypes[104] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderProfileRequest) ProtoMessage() {} + +func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[104] + 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 GetProviderProfileRequest.ProtoReflect.Descriptor instead. +func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{104} +} + +func (x *GetProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetProviderProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Provider profile payload with optional source metadata for diagnostics. +type ProviderProfileImportItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileImportItem) Reset() { + *x = ProviderProfileImportItem{} + mi := &file_openshell_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileImportItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileImportItem) ProtoMessage() {} + +func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[105] + 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 ProviderProfileImportItem.ProtoReflect.Descriptor instead. +func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{105} +} + +func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { + if x != nil { + return x.Profile + } + return nil +} + +func (x *ProviderProfileImportItem) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +// Provider profile validation diagnostic. +type ProviderProfileDiagnostic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + ProfileId string `protobuf:"bytes,2,opt,name=profile_id,json=profileId,proto3" json:"profile_id,omitempty"` + Field string `protobuf:"bytes,3,opt,name=field,proto3" json:"field,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Severity string `protobuf:"bytes,5,opt,name=severity,proto3" json:"severity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileDiagnostic) Reset() { + *x = ProviderProfileDiagnostic{} + mi := &file_openshell_proto_msgTypes[106] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileDiagnostic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileDiagnostic) ProtoMessage() {} + +func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[106] + 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 ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. +func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{106} +} + +func (x *ProviderProfileDiagnostic) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ProviderProfileDiagnostic) GetProfileId() string { + if x != nil { + return x.ProfileId + } + return "" +} + +func (x *ProviderProfileDiagnostic) GetField() string { + if x != nil { + return x.Field + } + return "" +} + +func (x *ProviderProfileDiagnostic) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ProviderProfileDiagnostic) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +// Endpoint selector for token grant audience overrides. +type ProviderCredentialTokenGrantAudienceOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional: endpoint host selector. If omitted, inherits the profile endpoint host. + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + // Optional: endpoint port selector. If omitted, matches the expanded profile endpoint port. + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + // Optional: endpoint path selector. If omitted, inherits the profile endpoint path. + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + // Resource audience to request for matching endpoints. + Audience string `protobuf:"bytes,4,opt,name=audience,proto3" json:"audience,omitempty"` + // Optional: OAuth2 scopes to request. If omitted, inherits the token grant scopes. + Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { + *x = ProviderCredentialTokenGrantAudienceOverride{} + mi := &file_openshell_proto_msgTypes[107] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} + +func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[107] + 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 ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{107} +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *ProviderCredentialTokenGrantAudienceOverride) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +type ProviderCredentialTokenGrantSubjectToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Source for the token exchange subject token. Phase one supports + // "provider_credential". + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + // Provider credential key that stores the subject token. + Credential string `protobuf:"bytes,2,opt,name=credential,proto3" json:"credential,omitempty"` + // OAuth2 subject_token_type. If omitted, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + SubjectTokenType string `protobuf:"bytes,3,opt,name=subject_token_type,json=subjectTokenType,proto3" json:"subject_token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { + *x = ProviderCredentialTokenGrantSubjectToken{} + mi := &file_openshell_proto_msgTypes[108] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialTokenGrantSubjectToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} + +func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[108] + 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 ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{108} +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetCredential() string { + if x != nil { + return x.Credential + } + return "" +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetSubjectTokenType() string { + if x != nil { + return x.SubjectTokenType + } + return "" +} + +type ProviderCredentialTokenGrant struct { + state protoimpl.MessageState `protogen:"open.v1"` + // OAuth2 token endpoint URL (e.g., https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token) + TokenEndpoint string `protobuf:"bytes,1,opt,name=token_endpoint,json=tokenEndpoint,proto3" json:"token_endpoint,omitempty"` + // Optional: default resource audience to request from the token service + Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` + // Optional: audience to request when fetching the JWT-SVID from SPIRE. + // If omitted, the sandbox derives this from token_endpoint. + JwtSvidAudience string `protobuf:"bytes,6,opt,name=jwt_svid_audience,json=jwtSvidAudience,proto3" json:"jwt_svid_audience,omitempty"` + // Optional: OAuth2 scopes to request + Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` + // Optional token cache TTL override. If absent, use expires_in from the token response. + CacheTtl *durationpb.Duration `protobuf:"bytes,104,opt,name=cache_ttl,json=cacheTtl,proto3" json:"cache_ttl,omitempty"` + // Optional: endpoint-specific resource audience overrides. + AudienceOverrides []*ProviderCredentialTokenGrantAudienceOverride `protobuf:"bytes,5,rep,name=audience_overrides,json=audienceOverrides,proto3" json:"audience_overrides,omitempty"` + // Optional: OAuth2 client_assertion_type value. If omitted, OpenShell uses + // urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + ClientAssertionType string `protobuf:"bytes,7,opt,name=client_assertion_type,json=clientAssertionType,proto3" json:"client_assertion_type,omitempty"` + // Grant type. If omitted/unspecified, OpenShell treats this as client_credentials + // for backwards compatibility. + GrantType ProviderCredentialTokenGrantType `protobuf:"varint,8,opt,name=grant_type,json=grantType,proto3,enum=openshell.v1.ProviderCredentialTokenGrantType" json:"grant_type,omitempty"` + // Subject token metadata for token_exchange grants. + SubjectToken *ProviderCredentialTokenGrantSubjectToken `protobuf:"bytes,9,opt,name=subject_token,json=subjectToken,proto3" json:"subject_token,omitempty"` + // OAuth2 requested_token_type. If omitted for token_exchange, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + RequestedTokenType string `protobuf:"bytes,10,opt,name=requested_token_type,json=requestedTokenType,proto3" json:"requested_token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialTokenGrant) Reset() { + *x = ProviderCredentialTokenGrant{} + mi := &file_openshell_proto_msgTypes[109] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialTokenGrant) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialTokenGrant) ProtoMessage() {} + +func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[109] + 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 ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{109} +} + +func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { + if x != nil { + return x.TokenEndpoint + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetJwtSvidAudience() string { + if x != nil { + return x.JwtSvidAudience + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetCacheTtl() *durationpb.Duration { + if x != nil { + return x.CacheTtl + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetAudienceOverrides() []*ProviderCredentialTokenGrantAudienceOverride { + if x != nil { + return x.AudienceOverrides + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetClientAssertionType() string { + if x != nil { + return x.ClientAssertionType + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetGrantType() ProviderCredentialTokenGrantType { + if x != nil { + return x.GrantType + } + return ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED +} + +func (x *ProviderCredentialTokenGrant) GetSubjectToken() *ProviderCredentialTokenGrantSubjectToken { + if x != nil { + return x.SubjectToken + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetRequestedTokenType() string { + if x != nil { + return x.RequestedTokenType + } + return "" +} + +// Provider credential declaration. +type ProviderProfileCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` + QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` + Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` + PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` + TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` + // Output-only gateway-derived endpoint authorities for resolved token grants. + // For inspected L7 requests, empty means no endpoint may obtain this grant. + // L4 injection uses selectors without checking these authorities. + // Profile-authored values are ignored; normal policies name the originating + // provider endpoint, while a global policy uses its own endpoint authorities. + TokenGrantOwners []string `protobuf:"bytes,11,rep,name=token_grant_owners,json=tokenGrantOwners,proto3" json:"token_grant_owners,omitempty"` + // Opt in to an opaque identity-bound environment placeholder for every env + // var declared by this credential. Use this only when an external credential + // updater requires one placeholder across value-only provider revisions. + // Gateway-managed refresh and token-grant credentials cannot opt in because + // they require identity-bearing handles for authorization-epoch revocation. + StablePlaceholder bool `protobuf:"varint,12,opt,name=stable_placeholder,json=stablePlaceholder,proto3" json:"stable_placeholder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileCredential) Reset() { + *x = ProviderProfileCredential{} + mi := &file_openshell_proto_msgTypes[110] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileCredential) ProtoMessage() {} + +func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[110] + 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 ProviderProfileCredential.ProtoReflect.Descriptor instead. +func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{110} +} + +func (x *ProviderProfileCredential) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProviderProfileCredential) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProviderProfileCredential) GetEnvVars() []string { + if x != nil { + return x.EnvVars + } + return nil +} + +func (x *ProviderProfileCredential) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *ProviderProfileCredential) GetAuthStyle() string { + if x != nil { + return x.AuthStyle + } + return "" +} + +func (x *ProviderProfileCredential) GetHeaderName() string { + if x != nil { + return x.HeaderName + } + return "" +} + +func (x *ProviderProfileCredential) GetQueryParam() string { + if x != nil { + return x.QueryParam + } + return "" +} + +func (x *ProviderProfileCredential) GetRefresh() *ProviderCredentialRefresh { + if x != nil { + return x.Refresh + } + return nil +} + +func (x *ProviderProfileCredential) GetPathTemplate() string { + if x != nil { + return x.PathTemplate + } + return "" +} + +func (x *ProviderProfileCredential) GetTokenGrant() *ProviderCredentialTokenGrant { + if x != nil { + return x.TokenGrant + } + return nil +} + +func (x *ProviderProfileCredential) GetTokenGrantOwners() []string { + if x != nil { + return x.TokenGrantOwners + } + return nil +} + +func (x *ProviderProfileCredential) GetStablePlaceholder() bool { + if x != nil { + return x.StablePlaceholder + } + return false +} + +type ProviderCredentialRefreshMaterial struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` + Secret bool `protobuf:"varint,4,opt,name=secret,proto3" json:"secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefreshMaterial) Reset() { + *x = ProviderCredentialRefreshMaterial{} + mi := &file_openshell_proto_msgTypes[111] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialRefreshMaterial) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} + +func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[111] + 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 ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{111} +} + +func (x *ProviderCredentialRefreshMaterial) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProviderCredentialRefreshMaterial) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProviderCredentialRefreshMaterial) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *ProviderCredentialRefreshMaterial) GetSecret() bool { + if x != nil { + return x.Secret + } + return false +} + +// Declares that a single refresh operation mints more than one credential. +// The refresh is attached to a primary credential; each additional output +// maps a strategy-defined semantic output id to a sibling credential whose +// env_vars receive the minted value. +type ProviderCredentialRefreshOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // strategy-defined semantic output id (e.g. "session_token") + Credential string `protobuf:"bytes,2,opt,name=credential,proto3" json:"credential,omitempty"` // sibling credential name whose env_vars receive this output + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefreshOutput) Reset() { + *x = ProviderCredentialRefreshOutput{} + mi := &file_openshell_proto_msgTypes[112] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialRefreshOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialRefreshOutput) ProtoMessage() {} + +func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[112] + 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 ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{112} +} + +func (x *ProviderCredentialRefreshOutput) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *ProviderCredentialRefreshOutput) GetCredential() string { + if x != nil { + return x.Credential + } + return "" +} + +type ProviderCredentialRefresh struct { + state protoimpl.MessageState `protogen:"open.v1"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,1,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` + RefreshBefore *durationpb.Duration `protobuf:"bytes,104,opt,name=refresh_before,json=refreshBefore,proto3" json:"refresh_before,omitempty"` + MaxLifetime *durationpb.Duration `protobuf:"bytes,105,opt,name=max_lifetime,json=maxLifetime,proto3" json:"max_lifetime,omitempty"` + Material []*ProviderCredentialRefreshMaterial `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty"` + AdditionalOutputs []*ProviderCredentialRefreshOutput `protobuf:"bytes,7,rep,name=additional_outputs,json=additionalOutputs,proto3" json:"additional_outputs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefresh) Reset() { + *x = ProviderCredentialRefresh{} + mi := &file_openshell_proto_msgTypes[113] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialRefresh) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialRefresh) ProtoMessage() {} + +func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[113] + 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 ProviderCredentialRefresh.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{113} +} + +func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { + if x != nil { + return x.Strategy + } + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +} + +func (x *ProviderCredentialRefresh) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *ProviderCredentialRefresh) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ProviderCredentialRefresh) GetRefreshBefore() *durationpb.Duration { + if x != nil { + return x.RefreshBefore + } + return nil +} + +func (x *ProviderCredentialRefresh) GetMaxLifetime() *durationpb.Duration { + if x != nil { + return x.MaxLifetime + } + return nil +} + +func (x *ProviderCredentialRefresh) GetMaterial() []*ProviderCredentialRefreshMaterial { + if x != nil { + return x.Material + } + return nil +} + +func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredentialRefreshOutput { + if x != nil { + return x.AdditionalOutputs + } + return nil +} + +type ProviderCredentialRefreshStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // Next automatic refresh time. Absence means no automatic retry is scheduled; + // use recovery_action to determine the required recovery workflow. + NextRefreshTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=next_refresh_time,json=nextRefreshTime,proto3" json:"next_refresh_time,omitempty"` + LastRefreshTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=last_refresh_time,json=lastRefreshTime,proto3" json:"last_refresh_time,omitempty"` + LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + RecoveryAction ProviderCredentialRefreshRecoveryAction `protobuf:"varint,10,opt,name=recovery_action,json=recoveryAction,proto3,enum=openshell.v1.ProviderCredentialRefreshRecoveryAction" json:"recovery_action,omitempty"` + // Stable gateway-owned failure identifier, for example + // "oauth_invalid_grant". This is not provider-controlled prose and + // incorporates any recognized top-level OAuth error classification. + FailureCode string `protobuf:"bytes,11,opt,name=failure_code,json=failureCode,proto3" json:"failure_code,omitempty"` + // A bounded, recognized provider subtype that refines failure_code; clients + // do not need a separate provider_error field. Unknown provider-controlled + // values are not persisted or returned. + ProviderErrorSubtype string `protobuf:"bytes,12,opt,name=provider_error_subtype,json=providerErrorSubtype,proto3" json:"provider_error_subtype,omitempty"` + LastErrorTime *timestamppb.Timestamp `protobuf:"bytes,113,opt,name=last_error_time,json=lastErrorTime,proto3" json:"last_error_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefreshStatus) Reset() { + *x = ProviderCredentialRefreshStatus{} + mi := &file_openshell_proto_msgTypes[114] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialRefreshStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialRefreshStatus) ProtoMessage() {} + +func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[114] + 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 ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{114} +} + +func (x *ProviderCredentialRefreshStatus) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetStrategy() ProviderCredentialRefreshStrategy { + if x != nil { + return x.Strategy + } + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +} + +func (x *ProviderCredentialRefreshStatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +func (x *ProviderCredentialRefreshStatus) GetNextRefreshTime() *timestamppb.Timestamp { + if x != nil { + return x.NextRefreshTime + } + return nil +} + +func (x *ProviderCredentialRefreshStatus) GetLastRefreshTime() *timestamppb.Timestamp { + if x != nil { + return x.LastRefreshTime + } + return nil +} + +func (x *ProviderCredentialRefreshStatus) GetLastError() string { + if x != nil { + return x.LastError + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetRecoveryAction() ProviderCredentialRefreshRecoveryAction { + if x != nil { + return x.RecoveryAction + } + return ProviderCredentialRefreshRecoveryAction_PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED +} + +func (x *ProviderCredentialRefreshStatus) GetFailureCode() string { + if x != nil { + return x.FailureCode + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetProviderErrorSubtype() string { + if x != nil { + return x.ProviderErrorSubtype + } + return "" +} + +func (x *ProviderCredentialRefreshStatus) GetLastErrorTime() *timestamppb.Timestamp { + if x != nil { + return x.LastErrorTime + } + return nil +} + +// Provider profile local discovery declaration. +type ProviderProfileDiscovery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Credential names from ProviderProfile.credentials eligible for local discovery. + Credentials []string `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileDiscovery) Reset() { + *x = ProviderProfileDiscovery{} + mi := &file_openshell_proto_msgTypes[115] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileDiscovery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileDiscovery) ProtoMessage() {} + +func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[115] + 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 ProviderProfileDiscovery.ProtoReflect.Descriptor instead. +func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{115} +} + +func (x *ProviderProfileDiscovery) GetCredentials() []string { + if x != nil { + return x.Credentials + } + return nil +} + +type GetProviderRefreshStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderRefreshStatusRequest) Reset() { + *x = GetProviderRefreshStatusRequest{} + mi := &file_openshell_proto_msgTypes[116] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderRefreshStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderRefreshStatusRequest) ProtoMessage() {} + +func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[116] + 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 GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. +func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{116} +} + +func (x *GetProviderRefreshStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetProviderRefreshStatusRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *GetProviderRefreshStatusRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +type GetProviderRefreshStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credentials []*ProviderCredentialRefreshStatus `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderRefreshStatusResponse) Reset() { + *x = GetProviderRefreshStatusResponse{} + mi := &file_openshell_proto_msgTypes[117] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderRefreshStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderRefreshStatusResponse) ProtoMessage() {} + +func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[117] + 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 GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. +func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{117} +} + +func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { + if x != nil { + return x.Credentials + } + return nil +} + +type ConfigureProviderRefreshRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,7,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,3,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Material map[string]string `protobuf:"bytes,4,rep,name=material,proto3" json:"material,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Additional material names the caller requests be stored as secrets. Every + // name must be present in material. The server also classifies secrets from + // the authoritative provider profile and refresh strategy. + SecretMaterialKeys []string `protobuf:"bytes,5,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,8,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureProviderRefreshRequest) Reset() { + *x = ConfigureProviderRefreshRequest{} + mi := &file_openshell_proto_msgTypes[118] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureProviderRefreshRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureProviderRefreshRequest) ProtoMessage() {} + +func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[118] + 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 ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. +func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{118} +} + +func (x *ConfigureProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ConfigureProviderRefreshRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ConfigureProviderRefreshRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *ConfigureProviderRefreshRequest) GetStrategy() ProviderCredentialRefreshStrategy { + if x != nil { + return x.Strategy + } + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +} + +func (x *ConfigureProviderRefreshRequest) GetMaterial() map[string]string { + if x != nil { + return x.Material + } + return nil +} + +func (x *ConfigureProviderRefreshRequest) GetSecretMaterialKeys() []string { + if x != nil { + return x.SecretMaterialKeys + } + return nil +} + +func (x *ConfigureProviderRefreshRequest) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +func (x *ConfigureProviderRefreshRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ConfigureProviderRefreshResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *ProviderCredentialRefreshStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureProviderRefreshResponse) Reset() { + *x = ConfigureProviderRefreshResponse{} + mi := &file_openshell_proto_msgTypes[119] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureProviderRefreshResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureProviderRefreshResponse) ProtoMessage() {} + +func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[119] + 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 ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. +func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{119} +} + +func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { + if x != nil { + return x.Status + } + return nil +} + +type RotateProviderCredentialRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RotateProviderCredentialRequest) Reset() { + *x = RotateProviderCredentialRequest{} + mi := &file_openshell_proto_msgTypes[120] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RotateProviderCredentialRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateProviderCredentialRequest) ProtoMessage() {} + +func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[120] + 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 RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. +func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{120} +} + +func (x *RotateProviderCredentialRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *RotateProviderCredentialRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *RotateProviderCredentialRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *RotateProviderCredentialRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type RotateProviderCredentialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status *ProviderCredentialRefreshStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RotateProviderCredentialResponse) Reset() { + *x = RotateProviderCredentialResponse{} + mi := &file_openshell_proto_msgTypes[121] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RotateProviderCredentialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RotateProviderCredentialResponse) ProtoMessage() {} + +func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[121] + 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 RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. +func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{121} +} + +func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { + if x != nil { + return x.Status + } + return nil +} + +type DeleteProviderRefreshRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + CredentialKey string `protobuf:"bytes,2,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + AllowMissing bool `protobuf:"varint,5,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,6,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderRefreshRequest) Reset() { + *x = DeleteProviderRefreshRequest{} + mi := &file_openshell_proto_msgTypes[122] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderRefreshRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderRefreshRequest) ProtoMessage() {} + +func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[122] + 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 DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{122} +} + +func (x *DeleteProviderRefreshRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteProviderRefreshRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *DeleteProviderRefreshRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *DeleteProviderRefreshRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteProviderRefreshRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type DeleteProviderRefreshResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderRefreshResponse) Reset() { + *x = DeleteProviderRefreshResponse{} + mi := &file_openshell_proto_msgTypes[123] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderRefreshResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderRefreshResponse) ProtoMessage() {} + +func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[123] + 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 DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. +func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{123} +} + +func (x *DeleteProviderRefreshResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Provider type profile metadata exposed to clients. +type ProviderProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Category ProviderProfileCategory `protobuf:"varint,4,opt,name=category,proto3,enum=openshell.v1.ProviderProfileCategory" json:"category,omitempty"` + Credentials []*ProviderProfileCredential `protobuf:"bytes,5,rep,name=credentials,proto3" json:"credentials,omitempty"` + Endpoints []*sandboxv1.NetworkEndpoint `protobuf:"bytes,6,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + Binaries []*sandboxv1.NetworkBinary `protobuf:"bytes,7,rep,name=binaries,proto3" json:"binaries,omitempty"` + InferenceCapable bool `protobuf:"varint,8,opt,name=inference_capable,json=inferenceCapable,proto3" json:"inference_capable,omitempty"` + Discovery *ProviderProfileDiscovery `protobuf:"bytes,9,opt,name=discovery,proto3" json:"discovery,omitempty"` + // Storage resource version for custom profiles. Built-in profiles and new + // profile files use 0. Gateway responses set this for stored custom profiles. + // Update calls use this for optimistic concurrency. + ResourceVersion uint64 `protobuf:"varint,10,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + // Optional non-secret annotations attached by profile sources or importers. + Annotations map[string]string `protobuf:"bytes,11,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Server-set provenance: "builtin", "user", or "interceptor/{name}". + // Ignored on import/update payloads. + Source string `protobuf:"bytes,12,opt,name=source,proto3" json:"source,omitempty"` + // Server-set visibility: "platform", "workspace", or empty for + // non-scoped sources. Ignored on import/update payloads. + Scope string `protobuf:"bytes,13,opt,name=scope,proto3" json:"scope,omitempty"` + // EXPERIMENTAL: Non-secret files rendered from provider configuration for the + // workload. This API and its behavior may change or be removed. + Files []*ProviderProfileFile `protobuf:"bytes,14,rep,name=files,proto3" json:"files,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfile) Reset() { + *x = ProviderProfile{} + mi := &file_openshell_proto_msgTypes[124] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfile) ProtoMessage() {} + +func (x *ProviderProfile) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[124] + 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 ProviderProfile.ProtoReflect.Descriptor instead. +func (*ProviderProfile) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{124} +} + +func (x *ProviderProfile) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProviderProfile) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *ProviderProfile) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProviderProfile) GetCategory() ProviderProfileCategory { + if x != nil { + return x.Category + } + return ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_UNSPECIFIED +} + +func (x *ProviderProfile) GetCredentials() []*ProviderProfileCredential { + if x != nil { + return x.Credentials + } + return nil +} + +func (x *ProviderProfile) GetEndpoints() []*sandboxv1.NetworkEndpoint { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *ProviderProfile) GetBinaries() []*sandboxv1.NetworkBinary { + if x != nil { + return x.Binaries + } + return nil +} + +func (x *ProviderProfile) GetInferenceCapable() bool { + if x != nil { + return x.InferenceCapable + } + return false +} + +func (x *ProviderProfile) GetDiscovery() *ProviderProfileDiscovery { + if x != nil { + return x.Discovery + } + return nil +} + +func (x *ProviderProfile) GetResourceVersion() uint64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + +func (x *ProviderProfile) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *ProviderProfile) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ProviderProfile) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *ProviderProfile) GetFiles() []*ProviderProfileFile { + if x != nil { + return x.Files + } + return nil +} + +// EXPERIMENTAL: Provider file templates may change or be removed. +type ProviderProfileFile struct { + state protoimpl.MessageState `protogen:"open.v1"` + // One virtual file name below /run/openshell/providers//. + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // UTF-8 template. Only {{config.KEY}} references are supported. + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + // Optional environment variable containing the virtual absolute path. + EnvVar string `protobuf:"bytes,3,opt,name=env_var,json=envVar,proto3" json:"env_var,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileFile) Reset() { + *x = ProviderProfileFile{} + mi := &file_openshell_proto_msgTypes[125] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileFile) ProtoMessage() {} + +func (x *ProviderProfileFile) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[125] + 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 ProviderProfileFile.ProtoReflect.Descriptor instead. +func (*ProviderProfileFile) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{125} +} + +func (x *ProviderProfileFile) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ProviderProfileFile) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *ProviderProfileFile) GetEnvVar() string { + if x != nil { + return x.EnvVar + } + return "" +} + +// Provider profile response. +type ProviderProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileResponse) Reset() { + *x = ProviderProfileResponse{} + mi := &file_openshell_proto_msgTypes[126] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileResponse) ProtoMessage() {} + +func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[126] + 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 ProviderProfileResponse.ProtoReflect.Descriptor instead. +func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{126} +} + +func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { + if x != nil { + return x.Profile + } + return nil +} + +// List provider profiles response. +type ListProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profiles []*ProviderProfile `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProviderProfilesResponse) Reset() { + *x = ListProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[127] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProviderProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProviderProfilesResponse) ProtoMessage() {} + +func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[127] + 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 ListProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{127} +} + +func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *ListProviderProfilesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Import custom provider profiles request. +type ImportProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Profiles []*ProviderProfileImportItem `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportProviderProfilesRequest) Reset() { + *x = ImportProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[128] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportProviderProfilesRequest) ProtoMessage() {} + +func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[128] + 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 ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{128} +} + +func (x *ImportProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *ImportProviderProfilesRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Import custom provider profiles response. +type ImportProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*ProviderProfileDiagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Profiles []*ProviderProfile `protobuf:"bytes,2,rep,name=profiles,proto3" json:"profiles,omitempty"` + Imported bool `protobuf:"varint,3,opt,name=imported,proto3" json:"imported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportProviderProfilesResponse) Reset() { + *x = ImportProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[129] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportProviderProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportProviderProfilesResponse) ProtoMessage() {} + +func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[129] + 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 ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{129} +} + +func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *ImportProviderProfilesResponse) GetProfiles() []*ProviderProfile { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *ImportProviderProfilesResponse) GetImported() bool { + if x != nil { + return x.Imported + } + return false +} + +// Update one custom provider profile request. +type UpdateProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Profile *ProviderProfileImportItem `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + // Expected storage resource version for optimistic concurrency control. + // If 0, the server uses the resource_version embedded in profile.profile. + // Updates without a non-zero version are rejected to prevent stale files from + // silently overwriting newer profile definitions. + ExpectedResourceVersion uint64 `protobuf:"varint,2,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Existing custom provider profile ID to update. The payload ID must match. + Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProviderProfilesRequest) Reset() { + *x = UpdateProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[130] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProviderProfilesRequest) ProtoMessage() {} + +func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[130] + 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 UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{130} +} + +func (x *UpdateProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *UpdateProviderProfilesRequest) GetProfile() *ProviderProfileImportItem { + if x != nil { + return x.Profile + } + return nil +} + +func (x *UpdateProviderProfilesRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *UpdateProviderProfilesRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateProviderProfilesRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Update one custom provider profile response. +type UpdateProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*ProviderProfileDiagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Profile *ProviderProfile `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` + Updated bool `protobuf:"varint,3,opt,name=updated,proto3" json:"updated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProviderProfilesResponse) Reset() { + *x = UpdateProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[131] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProviderProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProviderProfilesResponse) ProtoMessage() {} + +func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[131] + 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 UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{131} +} + +func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *UpdateProviderProfilesResponse) GetProfile() *ProviderProfile { + if x != nil { + return x.Profile + } + return nil +} + +func (x *UpdateProviderProfilesResponse) GetUpdated() bool { + if x != nil { + return x.Updated + } + return false +} + +// Lint provider profiles request. +type LintProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Profiles []*ProviderProfileImportItem `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LintProviderProfilesRequest) Reset() { + *x = LintProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[132] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LintProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LintProviderProfilesRequest) ProtoMessage() {} + +func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[132] + 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 LintProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{132} +} + +func (x *LintProviderProfilesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { + if x != nil { + return x.Profiles + } + return nil +} + +// Lint provider profiles response. +type LintProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*ProviderProfileDiagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Valid bool `protobuf:"varint,2,opt,name=valid,proto3" json:"valid,omitempty"` + // Clients enabling stable_placeholder check this before import or update, + // since older gateways silently discard unknown credential fields. + SupportsStablePlaceholder bool `protobuf:"varint,3,opt,name=supports_stable_placeholder,json=supportsStablePlaceholder,proto3" json:"supports_stable_placeholder,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LintProviderProfilesResponse) Reset() { + *x = LintProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[133] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LintProviderProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LintProviderProfilesResponse) ProtoMessage() {} + +func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[133] + 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 LintProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{133} +} + +func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *LintProviderProfilesResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *LintProviderProfilesResponse) GetSupportsStablePlaceholder() bool { + if x != nil { + return x.SupportsStablePlaceholder + } + return false +} + +// Delete provider response. +type DeleteProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderResponse) Reset() { + *x = DeleteProviderResponse{} + mi := &file_openshell_proto_msgTypes[134] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderResponse) ProtoMessage() {} + +func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[134] + 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 DeleteProviderResponse.ProtoReflect.Descriptor instead. +func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{134} +} + +func (x *DeleteProviderResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Delete custom provider profile request. +type DeleteProviderProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderProfileRequest) Reset() { + *x = DeleteProviderProfileRequest{} + mi := &file_openshell_proto_msgTypes[135] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderProfileRequest) ProtoMessage() {} + +func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[135] + 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 DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{135} +} + +func (x *DeleteProviderProfileRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *DeleteProviderProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *DeleteProviderProfileRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteProviderProfileRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Delete custom provider profile response. +type DeleteProviderProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderProfileResponse) Reset() { + *x = DeleteProviderProfileResponse{} + mi := &file_openshell_proto_msgTypes[136] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderProfileResponse) ProtoMessage() {} + +func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[136] + 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 DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. +func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{136} +} + +func (x *DeleteProviderProfileResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Get sandbox provider environment request. +type GetSandboxProviderEnvironmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The sandbox ID. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Whether the requesting supervisor enforces endpoint bindings for static + // provider credentials. Gateways withhold static credential material when + // this capability is absent. + SupportsStaticCredentialBindings bool `protobuf:"varint,2,opt,name=supports_static_credential_bindings,json=supportsStaticCredentialBindings,proto3" json:"supports_static_credential_bindings,omitempty"` + // Whether the requesting supervisor understands stable external-placeholder + // metadata. Gateways withhold credentials that require this behavior when + // the capability is absent. + SupportsStablePlaceholderEnvironmentKeys bool `protobuf:"varint,3,opt,name=supports_stable_placeholder_environment_keys,json=supportsStablePlaceholderEnvironmentKeys,proto3" json:"supports_stable_placeholder_environment_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderEnvironmentRequest) Reset() { + *x = GetSandboxProviderEnvironmentRequest{} + mi := &file_openshell_proto_msgTypes[137] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderEnvironmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} + +func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[137] + 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 GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{137} +} + +func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *GetSandboxProviderEnvironmentRequest) GetSupportsStaticCredentialBindings() bool { + if x != nil { + return x.SupportsStaticCredentialBindings + } + return false +} + +func (x *GetSandboxProviderEnvironmentRequest) GetSupportsStablePlaceholderEnvironmentKeys() bool { + if x != nil { + return x.SupportsStablePlaceholderEnvironmentKeys + } + return false +} + +// One network endpoint at which a static provider credential may be resolved. +type StaticCredentialEndpointBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticCredentialEndpointBinding) Reset() { + *x = StaticCredentialEndpointBinding{} + mi := &file_openshell_proto_msgTypes[138] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticCredentialEndpointBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticCredentialEndpointBinding) ProtoMessage() {} + +func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[138] + 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 StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. +func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{138} +} + +func (x *StaticCredentialEndpointBinding) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *StaticCredentialEndpointBinding) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *StaticCredentialEndpointBinding) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Endpoint allowlist for one static provider credential environment variable. +type StaticCredentialBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*StaticCredentialEndpointBinding `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + // Stable identity of the provider credential that produced this binding. + // Supervisors use it to retain old revision placeholders only across + // rotations of the same provider credential. + CredentialIdentity string `protobuf:"bytes,2,opt,name=credential_identity,json=credentialIdentity,proto3" json:"credential_identity,omitempty"` + // Opaque gateway-issued handle for a stable credential lifecycle. When + // non-empty, supervisors keep the workload placeholder stable across value + // updates and replace only the resolver value. The handle changes when the + // sandbox, provider, credential key, endpoint authorization, or an applicable + // refresh authorization epoch changes. + WorkloadCredentialHandle string `protobuf:"bytes,3,opt,name=workload_credential_handle,json=workloadCredentialHandle,proto3" json:"workload_credential_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticCredentialBinding) Reset() { + *x = StaticCredentialBinding{} + mi := &file_openshell_proto_msgTypes[139] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticCredentialBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticCredentialBinding) ProtoMessage() {} + +func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[139] + 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 StaticCredentialBinding.ProtoReflect.Descriptor instead. +func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{139} +} + +func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *StaticCredentialBinding) GetCredentialIdentity() string { + if x != nil { + return x.CredentialIdentity + } + return "" +} + +func (x *StaticCredentialBinding) GetWorkloadCredentialHandle() string { + if x != nil { + return x.WorkloadCredentialHandle + } + return "" +} + +// Get sandbox provider environment response. +type GetSandboxProviderEnvironmentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Provider credential environment variables. + Environment map[string]string `protobuf:"bytes,1,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Fingerprint for the provider credential inputs that produced environment. + ProviderEnvRevision uint64 `protobuf:"varint,2,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + // Expiration timestamps for returned environment variables. + CredentialExpirationTimes map[string]*timestamppb.Timestamp `protobuf:"bytes,103,rep,name=credential_expiration_times,json=credentialExpirationTimes,proto3" json:"credential_expiration_times,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Dynamic credentials that require token grants or other runtime injection. + // Maps endpoint-bound provider metadata to credential metadata. + // Supervisor uses this to inject Authorization headers for token grant credentials. + DynamicCredentials map[string]*ProviderProfileCredential `protobuf:"bytes,4,rep,name=dynamic_credentials,json=dynamicCredentials,proto3" json:"dynamic_credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Endpoint allowlists for static credential environment variables. Metadata + // can be incomplete when a provider profile is invalid; capable supervisors + // reject all static material in that snapshot while preserving independently + // endpoint-bound dynamic credentials. + StaticCredentialBindings map[string]*StaticCredentialBinding `protobuf:"bytes,5,rep,name=static_credential_bindings,json=staticCredentialBindings,proto3" json:"static_credential_bindings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Environment variables that contain provider configuration rather than + // credentials and therefore do not require endpoint-scoped resolution. + NonSecretEnvironmentKeys []string `protobuf:"bytes,6,rep,name=non_secret_environment_keys,json=nonSecretEnvironmentKeys,proto3" json:"non_secret_environment_keys,omitempty"` + // Attachment identity captured with the returned provider records. + ProviderAttachmentEpoch string `protobuf:"bytes,7,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + // Effective policy identity used to derive this snapshot's endpoint bindings. + PolicyHash string `protobuf:"bytes,8,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Nonzero when material was withheld; installing an empty map is not readiness. + ReadinessReason ProviderReadinessReason `protobuf:"varint,9,opt,name=readiness_reason,json=readinessReason,proto3,enum=openshell.v1.ProviderReadinessReason" json:"readiness_reason,omitempty"` + // Complete desired set of non-secret managed files, keyed by absolute path. + Files map[string]string `protobuf:"bytes,10,rep,name=files,proto3" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Credential environment variables whose opaque resolver handle remains + // stable across value-only provider revisions. These keys are still secrets; + // this metadata only selects the stable external-placeholder behavior. + StablePlaceholderEnvironmentKeys []string `protobuf:"bytes,11,rep,name=stable_placeholder_environment_keys,json=stablePlaceholderEnvironmentKeys,proto3" json:"stable_placeholder_environment_keys,omitempty"` + // Supporting gateways always acknowledge the stable-placeholder delivery + // contract, including responses without opted-in credentials. Supervisors + // require acknowledgment for declared or previously activated external + // stable credentials; ordinary legacy delivery does not require it. + SupportsStablePlaceholderEnvironmentKeys bool `protobuf:"varint,12,opt,name=supports_stable_placeholder_environment_keys,json=supportsStablePlaceholderEnvironmentKeys,proto3" json:"supports_stable_placeholder_environment_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderEnvironmentResponse) Reset() { + *x = GetSandboxProviderEnvironmentResponse{} + mi := &file_openshell_proto_msgTypes[140] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderEnvironmentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} + +func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[140] + 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 GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{140} +} + +func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *GetSandboxProviderEnvironmentResponse) GetCredentialExpirationTimes() map[string]*timestamppb.Timestamp { + if x != nil { + return x.CredentialExpirationTimes + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetDynamicCredentials() map[string]*ProviderProfileCredential { + if x != nil { + return x.DynamicCredentials + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetStaticCredentialBindings() map[string]*StaticCredentialBinding { + if x != nil { + return x.StaticCredentialBindings + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() []string { + if x != nil { + return x.NonSecretEnvironmentKeys + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + +func (x *GetSandboxProviderEnvironmentResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *GetSandboxProviderEnvironmentResponse) GetReadinessReason() ProviderReadinessReason { + if x != nil { + return x.ReadinessReason + } + return ProviderReadinessReason_PROVIDER_READINESS_REASON_UNSPECIFIED +} + +func (x *GetSandboxProviderEnvironmentResponse) GetFiles() map[string]string { + if x != nil { + return x.Files + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetStablePlaceholderEnvironmentKeys() []string { + if x != nil { + return x.StablePlaceholderEnvironmentKeys + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetSupportsStablePlaceholderEnvironmentKeys() bool { + if x != nil { + return x.SupportsStablePlaceholderEnvironmentKeys + } + return false +} + +type ExchangeProviderSubjectTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The sandbox ID. Must match the authenticated sandbox principal. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Attached provider record holding the configured subject token credential. + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // Provider profile credential that declares the token_exchange grant. + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + // Supervisor JWT-SVID. The gateway verifies this and uses its `sub` claim + // as the requested audience for the intermediate token. + SupervisorJwtSvid string `protobuf:"bytes,4,opt,name=supervisor_jwt_svid,json=supervisorJwtSvid,proto3" json:"supervisor_jwt_svid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeProviderSubjectTokenRequest) Reset() { + *x = ExchangeProviderSubjectTokenRequest{} + mi := &file_openshell_proto_msgTypes[141] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeProviderSubjectTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} + +func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[141] + 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 ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{141} +} + +func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetSupervisorJwtSvid() string { + if x != nil { + return x.SupervisorJwtSvid + } + return "" +} + +type ExchangeProviderSubjectTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + ExpiresAfter *durationpb.Duration `protobuf:"bytes,102,opt,name=expires_after,json=expiresAfter,proto3" json:"expires_after,omitempty"` + TokenType string `protobuf:"bytes,3,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeProviderSubjectTokenResponse) Reset() { + *x = ExchangeProviderSubjectTokenResponse{} + mi := &file_openshell_proto_msgTypes[142] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeProviderSubjectTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} + +func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[142] + 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 ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{142} +} + +func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *ExchangeProviderSubjectTokenResponse) GetExpiresAfter() *durationpb.Duration { + if x != nil { + return x.ExpiresAfter + } + return nil +} + +func (x *ExchangeProviderSubjectTokenResponse) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + +// Update sandbox policy request. +type UpdateConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for global updates; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,10,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The new policy to apply. + // + // Sandbox scope (`global=false`): + // - only network_policies may differ from create-time + // policy; static fields must match version 1. + // + // Global scope (`global=true`): + // - applies to all sandboxes in full (no merge). + Policy *sandboxv1.SandboxPolicy `protobuf:"bytes,2,opt,name=policy,proto3" json:"policy,omitempty"` + // Optional single setting key to mutate. + SettingKey string `protobuf:"bytes,3,opt,name=setting_key,json=settingKey,proto3" json:"setting_key,omitempty"` + // Setting value for upsert operations. + SettingValue *sandboxv1.SettingValue `protobuf:"bytes,4,opt,name=setting_value,json=settingValue,proto3" json:"setting_value,omitempty"` + // Delete the setting key from scope. + // Sandbox-scoped deletes are rejected; only global delete is supported. + DeleteSetting bool `protobuf:"varint,5,opt,name=delete_setting,json=deleteSetting,proto3" json:"delete_setting,omitempty"` + // Apply mutation at gateway-global scope. + Global bool `protobuf:"varint,6,opt,name=global,proto3" json:"global,omitempty"` + // Batched incremental policy merge operations. Sandbox-scoped only. + MergeOperations []*PolicyMergeOperation `protobuf:"bytes,7,rep,name=merge_operations,json=mergeOperations,proto3" json:"merge_operations,omitempty"` + // Expected resource version for optimistic concurrency control (sandbox-scoped only). + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + // Ignored for global-scoped updates. + ExpectedResourceVersion uint64 `protobuf:"varint,8,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Caller-provided annotations associated with a sandbox-scoped update. Values + // must not contain secrets; the gateway treats them as opaque metadata and does + // not interpret or verify their semantics. For policy updates, the gateway + // stores the annotations immutably with the revision and merges them into + // sandbox metadata as a convenience projection. For setting-only updates, it + // only merges them into sandbox metadata. + Annotations map[string]string `protobuf:"bytes,9,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Required for sandbox-scoped updates and empty for global updates. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,11,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateConfigRequest) Reset() { + *x = UpdateConfigRequest{} + mi := &file_openshell_proto_msgTypes[143] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateConfigRequest) ProtoMessage() {} + +func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[143] + 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 UpdateConfigRequest.ProtoReflect.Descriptor instead. +func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{143} +} + +func (x *UpdateConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *UpdateConfigRequest) GetPolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *UpdateConfigRequest) GetSettingKey() string { + if x != nil { + return x.SettingKey + } + return "" +} + +func (x *UpdateConfigRequest) GetSettingValue() *sandboxv1.SettingValue { + if x != nil { + return x.SettingValue + } + return nil +} + +func (x *UpdateConfigRequest) GetDeleteSetting() bool { + if x != nil { + return x.DeleteSetting + } + return false +} + +func (x *UpdateConfigRequest) GetGlobal() bool { + if x != nil { + return x.Global + } + return false +} + +func (x *UpdateConfigRequest) GetMergeOperations() []*PolicyMergeOperation { + if x != nil { + return x.MergeOperations + } + return nil +} + +func (x *UpdateConfigRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *UpdateConfigRequest) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +func (x *UpdateConfigRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *UpdateConfigRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type PolicyMergeOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Operation: + // + // *PolicyMergeOperation_AddRule + // *PolicyMergeOperation_RemoveEndpoint + // *PolicyMergeOperation_RemoveRule + // *PolicyMergeOperation_AddDenyRules + // *PolicyMergeOperation_AddAllowRules + // *PolicyMergeOperation_RemoveBinary + Operation isPolicyMergeOperation_Operation `protobuf_oneof:"operation"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyMergeOperation) Reset() { + *x = PolicyMergeOperation{} + mi := &file_openshell_proto_msgTypes[144] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyMergeOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyMergeOperation) ProtoMessage() {} + +func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[144] + 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 PolicyMergeOperation.ProtoReflect.Descriptor instead. +func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{144} +} + +func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { + if x != nil { + return x.Operation + } + return nil +} + +func (x *PolicyMergeOperation) GetAddRule() *AddNetworkRule { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_AddRule); ok { + return x.AddRule + } + } + return nil +} + +func (x *PolicyMergeOperation) GetRemoveEndpoint() *RemoveNetworkEndpoint { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_RemoveEndpoint); ok { + return x.RemoveEndpoint + } + } + return nil +} + +func (x *PolicyMergeOperation) GetRemoveRule() *RemoveNetworkRule { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_RemoveRule); ok { + return x.RemoveRule + } + } + return nil +} + +func (x *PolicyMergeOperation) GetAddDenyRules() *AddDenyRules { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_AddDenyRules); ok { + return x.AddDenyRules + } + } + return nil +} + +func (x *PolicyMergeOperation) GetAddAllowRules() *AddAllowRules { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_AddAllowRules); ok { + return x.AddAllowRules + } + } + return nil +} + +func (x *PolicyMergeOperation) GetRemoveBinary() *RemoveNetworkBinary { + if x != nil { + if x, ok := x.Operation.(*PolicyMergeOperation_RemoveBinary); ok { + return x.RemoveBinary + } + } + return nil +} + +type isPolicyMergeOperation_Operation interface { + isPolicyMergeOperation_Operation() +} + +type PolicyMergeOperation_AddRule struct { + AddRule *AddNetworkRule `protobuf:"bytes,1,opt,name=add_rule,json=addRule,proto3,oneof"` +} + +type PolicyMergeOperation_RemoveEndpoint struct { + RemoveEndpoint *RemoveNetworkEndpoint `protobuf:"bytes,2,opt,name=remove_endpoint,json=removeEndpoint,proto3,oneof"` +} + +type PolicyMergeOperation_RemoveRule struct { + RemoveRule *RemoveNetworkRule `protobuf:"bytes,3,opt,name=remove_rule,json=removeRule,proto3,oneof"` +} + +type PolicyMergeOperation_AddDenyRules struct { + AddDenyRules *AddDenyRules `protobuf:"bytes,4,opt,name=add_deny_rules,json=addDenyRules,proto3,oneof"` +} + +type PolicyMergeOperation_AddAllowRules struct { + AddAllowRules *AddAllowRules `protobuf:"bytes,5,opt,name=add_allow_rules,json=addAllowRules,proto3,oneof"` +} + +type PolicyMergeOperation_RemoveBinary struct { + RemoveBinary *RemoveNetworkBinary `protobuf:"bytes,6,opt,name=remove_binary,json=removeBinary,proto3,oneof"` +} + +func (*PolicyMergeOperation_AddRule) isPolicyMergeOperation_Operation() {} + +func (*PolicyMergeOperation_RemoveEndpoint) isPolicyMergeOperation_Operation() {} + +func (*PolicyMergeOperation_RemoveRule) isPolicyMergeOperation_Operation() {} + +func (*PolicyMergeOperation_AddDenyRules) isPolicyMergeOperation_Operation() {} + +func (*PolicyMergeOperation_AddAllowRules) isPolicyMergeOperation_Operation() {} + +func (*PolicyMergeOperation_RemoveBinary) isPolicyMergeOperation_Operation() {} + +type AddNetworkRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleName string `protobuf:"bytes,1,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + Rule *sandboxv1.NetworkPolicyRule `protobuf:"bytes,2,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddNetworkRule) Reset() { + *x = AddNetworkRule{} + mi := &file_openshell_proto_msgTypes[145] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddNetworkRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddNetworkRule) ProtoMessage() {} + +func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[145] + 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 AddNetworkRule.ProtoReflect.Descriptor instead. +func (*AddNetworkRule) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{145} +} + +func (x *AddNetworkRule) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +func (x *AddNetworkRule) GetRule() *sandboxv1.NetworkPolicyRule { + if x != nil { + return x.Rule + } + return nil +} + +type RemoveNetworkEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleName string `protobuf:"bytes,1,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveNetworkEndpoint) Reset() { + *x = RemoveNetworkEndpoint{} + mi := &file_openshell_proto_msgTypes[146] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveNetworkEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveNetworkEndpoint) ProtoMessage() {} + +func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[146] + 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 RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. +func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{146} +} + +func (x *RemoveNetworkEndpoint) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +func (x *RemoveNetworkEndpoint) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *RemoveNetworkEndpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +type RemoveNetworkRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleName string `protobuf:"bytes,1,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveNetworkRule) Reset() { + *x = RemoveNetworkRule{} + mi := &file_openshell_proto_msgTypes[147] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveNetworkRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveNetworkRule) ProtoMessage() {} + +func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[147] + 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 RemoveNetworkRule.ProtoReflect.Descriptor instead. +func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{147} +} + +func (x *RemoveNetworkRule) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +// Exact endpoint and complete authorization scope affected by an L7 append. +// All ports and binaries must match the stored target; omitted scope is invalid. +type L7RuleTarget struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleName string `protobuf:"bytes,1,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + Ports []uint32 `protobuf:"varint,3,rep,packed,name=ports,proto3" json:"ports,omitempty"` + // An absent path requires a unique endpoint. An empty path selects an + // endpoint without a path selector. This is not the appended request path. + Path *string `protobuf:"bytes,4,opt,name=path,proto3,oneof" json:"path,omitempty"` + // Declare either a nonempty binary list or any_binary, never both. + Binaries []*sandboxv1.NetworkBinary `protobuf:"bytes,5,rep,name=binaries,proto3" json:"binaries,omitempty"` + AnyBinary bool `protobuf:"varint,6,opt,name=any_binary,json=anyBinary,proto3" json:"any_binary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7RuleTarget) Reset() { + *x = L7RuleTarget{} + mi := &file_openshell_proto_msgTypes[148] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7RuleTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7RuleTarget) ProtoMessage() {} + +func (x *L7RuleTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[148] + 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 L7RuleTarget.ProtoReflect.Descriptor instead. +func (*L7RuleTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{148} +} + +func (x *L7RuleTarget) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +func (x *L7RuleTarget) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *L7RuleTarget) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *L7RuleTarget) GetPath() string { + if x != nil && x.Path != nil { + return *x.Path + } + return "" +} + +func (x *L7RuleTarget) GetBinaries() []*sandboxv1.NetworkBinary { + if x != nil { + return x.Binaries + } + return nil +} + +func (x *L7RuleTarget) GetAnyBinary() bool { + if x != nil { + return x.AnyBinary + } + return false +} + +type AddDenyRules struct { + state protoimpl.MessageState `protogen:"open.v1"` + DenyRules []*sandboxv1.L7DenyRule `protobuf:"bytes,3,rep,name=deny_rules,json=denyRules,proto3" json:"deny_rules,omitempty"` + Target *L7RuleTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddDenyRules) Reset() { + *x = AddDenyRules{} + mi := &file_openshell_proto_msgTypes[149] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddDenyRules) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddDenyRules) ProtoMessage() {} + +func (x *AddDenyRules) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[149] + 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 AddDenyRules.ProtoReflect.Descriptor instead. +func (*AddDenyRules) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{149} +} + +func (x *AddDenyRules) GetDenyRules() []*sandboxv1.L7DenyRule { + if x != nil { + return x.DenyRules + } + return nil +} + +func (x *AddDenyRules) GetTarget() *L7RuleTarget { + if x != nil { + return x.Target + } + return nil +} + +type AddAllowRules struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*sandboxv1.L7Rule `protobuf:"bytes,3,rep,name=rules,proto3" json:"rules,omitempty"` + Target *L7RuleTarget `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddAllowRules) Reset() { + *x = AddAllowRules{} + mi := &file_openshell_proto_msgTypes[150] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddAllowRules) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAllowRules) ProtoMessage() {} + +func (x *AddAllowRules) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[150] + 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 AddAllowRules.ProtoReflect.Descriptor instead. +func (*AddAllowRules) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{150} +} + +func (x *AddAllowRules) GetRules() []*sandboxv1.L7Rule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *AddAllowRules) GetTarget() *L7RuleTarget { + if x != nil { + return x.Target + } + return nil +} + +type RemoveNetworkBinary struct { + state protoimpl.MessageState `protogen:"open.v1"` + RuleName string `protobuf:"bytes,1,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + BinaryPath string `protobuf:"bytes,2,opt,name=binary_path,json=binaryPath,proto3" json:"binary_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveNetworkBinary) Reset() { + *x = RemoveNetworkBinary{} + mi := &file_openshell_proto_msgTypes[151] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveNetworkBinary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveNetworkBinary) ProtoMessage() {} + +func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[151] + 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 RemoveNetworkBinary.ProtoReflect.Descriptor instead. +func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{151} +} + +func (x *RemoveNetworkBinary) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +func (x *RemoveNetworkBinary) GetBinaryPath() string { + if x != nil { + return x.BinaryPath + } + return "" +} + +// Update sandbox policy response. +type UpdateConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Assigned policy version (monotonically increasing per sandbox). + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // SHA-256 hash of the serialized policy payload. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Settings revision for the scope that was modified. + SettingsRevision uint64 `protobuf:"varint,3,opt,name=settings_revision,json=settingsRevision,proto3" json:"settings_revision,omitempty"` + // True when a setting delete operation removed an existing key. + Deleted bool `protobuf:"varint,4,opt,name=deleted,proto3" json:"deleted,omitempty"` + // Sandbox metadata annotations after the update. Empty for global updates. + Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateConfigResponse) Reset() { + *x = UpdateConfigResponse{} + mi := &file_openshell_proto_msgTypes[152] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateConfigResponse) ProtoMessage() {} + +func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[152] + 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 UpdateConfigResponse.ProtoReflect.Descriptor instead. +func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{152} +} + +func (x *UpdateConfigResponse) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *UpdateConfigResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *UpdateConfigResponse) GetSettingsRevision() uint64 { + if x != nil { + return x.SettingsRevision + } + return 0 +} + +func (x *UpdateConfigResponse) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +func (x *UpdateConfigResponse) GetAnnotations() map[string]string { + if x != nil { + return x.Annotations + } + return nil +} + +// Get sandbox policy status request. +type GetSandboxPolicyStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for global queries; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The specific policy version to query. 0 means latest. + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // Query global policy revisions instead of a sandbox-scoped one. + Global bool `protobuf:"varint,3,opt,name=global,proto3" json:"global,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxPolicyStatusRequest) Reset() { + *x = GetSandboxPolicyStatusRequest{} + mi := &file_openshell_proto_msgTypes[153] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxPolicyStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} + +func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[153] + 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 GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{153} +} + +func (x *GetSandboxPolicyStatusRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxPolicyStatusRequest) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *GetSandboxPolicyStatusRequest) GetGlobal() bool { + if x != nil { + return x.Global + } + return false +} + +func (x *GetSandboxPolicyStatusRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +// Get sandbox policy status response. +type GetSandboxPolicyStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The queried policy revision. + Revision *SandboxPolicyRevision `protobuf:"bytes,1,opt,name=revision,proto3" json:"revision,omitempty"` + // The currently active (loaded) policy version for this sandbox. + ActiveVersion uint32 `protobuf:"varint,2,opt,name=active_version,json=activeVersion,proto3" json:"active_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxPolicyStatusResponse) Reset() { + *x = GetSandboxPolicyStatusResponse{} + mi := &file_openshell_proto_msgTypes[154] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxPolicyStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} + +func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[154] + 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 GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{154} +} + +func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { + if x != nil { + return x.Revision + } + return nil +} + +func (x *GetSandboxPolicyStatusResponse) GetActiveVersion() uint32 { + if x != nil { + return x.ActiveVersion + } + return 0 +} + +// List sandbox policies request. +type ListSandboxPoliciesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Omit for global queries; otherwise select one named workspace. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,5,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of revisions to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListSandboxPolicies response. All other request + // parameters except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // List global policy revisions instead of sandbox-scoped ones. + Global bool `protobuf:"varint,4,opt,name=global,proto3" json:"global,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxPoliciesRequest) Reset() { + *x = ListSandboxPoliciesRequest{} + mi := &file_openshell_proto_msgTypes[155] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxPoliciesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxPoliciesRequest) ProtoMessage() {} + +func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[155] + 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 ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{155} +} + +func (x *ListSandboxPoliciesRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListSandboxPoliciesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSandboxPoliciesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListSandboxPoliciesRequest) GetGlobal() bool { + if x != nil { + return x.Global + } + return false +} + +func (x *ListSandboxPoliciesRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +// List sandbox policies response. +type ListSandboxPoliciesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Invalid historical payloads remain visible as failed projections so one + // legacy row cannot hide the rest of the policy history. + Revisions []*SandboxPolicyRevision `protobuf:"bytes,1,rep,name=revisions,proto3" json:"revisions,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSandboxPoliciesResponse) Reset() { + *x = ListSandboxPoliciesResponse{} + mi := &file_openshell_proto_msgTypes[156] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSandboxPoliciesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSandboxPoliciesResponse) ProtoMessage() {} + +func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[156] + 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 ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{156} +} + +func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { + if x != nil { + return x.Revisions + } + return nil +} + +func (x *ListSandboxPoliciesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Report policy load status (called by sandbox runtime after reload attempt). +type ReportPolicyStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // The policy version that was attempted. + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // Load result status. + Status PolicyStatus `protobuf:"varint,3,opt,name=status,proto3,enum=openshell.v1.PolicyStatus" json:"status,omitempty"` + // Error message if status is FAILED. + LoadError string `protobuf:"bytes,4,opt,name=load_error,json=loadError,proto3" json:"load_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportPolicyStatusRequest) Reset() { + *x = ReportPolicyStatusRequest{} + mi := &file_openshell_proto_msgTypes[157] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportPolicyStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportPolicyStatusRequest) ProtoMessage() {} + +func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[157] + 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 ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{157} +} + +func (x *ReportPolicyStatusRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportPolicyStatusRequest) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ReportPolicyStatusRequest) GetStatus() PolicyStatus { + if x != nil { + return x.Status + } + return PolicyStatus_POLICY_STATUS_UNSPECIFIED +} + +func (x *ReportPolicyStatusRequest) GetLoadError() string { + if x != nil { + return x.LoadError + } + return "" +} + +// Report policy status response. +type ReportPolicyStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportPolicyStatusResponse) Reset() { + *x = ReportPolicyStatusResponse{} + mi := &file_openshell_proto_msgTypes[158] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportPolicyStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportPolicyStatusResponse) ProtoMessage() {} + +func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[158] + 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 ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{158} +} + +type SandboxConfigurationAdmission struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstanceId string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + State ConfigurationAdmissionState `protobuf:"varint,2,opt,name=state,proto3,enum=openshell.v1.ConfigurationAdmissionState" json:"state,omitempty"` + PolicyVersion uint32 `protobuf:"varint,3,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + PolicyHash string `protobuf:"bytes,4,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + ConfigRevision uint64 `protobuf:"varint,5,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + ProviderEnvRevision uint64 `protobuf:"varint,6,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxConfigurationAdmission) Reset() { + *x = SandboxConfigurationAdmission{} + mi := &file_openshell_proto_msgTypes[159] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxConfigurationAdmission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxConfigurationAdmission) ProtoMessage() {} + +func (x *SandboxConfigurationAdmission) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[159] + 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 SandboxConfigurationAdmission.ProtoReflect.Descriptor instead. +func (*SandboxConfigurationAdmission) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{159} +} + +func (x *SandboxConfigurationAdmission) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *SandboxConfigurationAdmission) GetState() ConfigurationAdmissionState { + if x != nil { + return x.State + } + return ConfigurationAdmissionState_CONFIGURATION_ADMISSION_STATE_UNSPECIFIED +} + +func (x *SandboxConfigurationAdmission) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *SandboxConfigurationAdmission) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *SandboxConfigurationAdmission) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type ReportSandboxConfigurationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + Admission *SandboxConfigurationAdmission `protobuf:"bytes,2,opt,name=admission,proto3" json:"admission,omitempty"` + // Pending registration replaces only this previously observed instance. + ExpectedInstanceId string `protobuf:"bytes,3,opt,name=expected_instance_id,json=expectedInstanceId,proto3" json:"expected_instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportSandboxConfigurationRequest) Reset() { + *x = ReportSandboxConfigurationRequest{} + mi := &file_openshell_proto_msgTypes[160] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportSandboxConfigurationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportSandboxConfigurationRequest) ProtoMessage() {} + +func (x *ReportSandboxConfigurationRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[160] + 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 ReportSandboxConfigurationRequest.ProtoReflect.Descriptor instead. +func (*ReportSandboxConfigurationRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{160} +} + +func (x *ReportSandboxConfigurationRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportSandboxConfigurationRequest) GetAdmission() *SandboxConfigurationAdmission { + if x != nil { + return x.Admission + } + return nil +} + +func (x *ReportSandboxConfigurationRequest) GetExpectedInstanceId() string { + if x != nil { + return x.ExpectedInstanceId + } + return "" +} + +type ReportSandboxConfigurationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportSandboxConfigurationResponse) Reset() { + *x = ReportSandboxConfigurationResponse{} + mi := &file_openshell_proto_msgTypes[161] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportSandboxConfigurationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportSandboxConfigurationResponse) ProtoMessage() {} + +func (x *ReportSandboxConfigurationResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[161] + 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 ReportSandboxConfigurationResponse.ProtoReflect.Descriptor instead. +func (*ReportSandboxConfigurationResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{161} +} + +// A versioned policy revision with metadata. +type SandboxPolicyRevision struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Policy version (monotonically increasing per sandbox). + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // SHA-256 hash of the canonical serialized policy payload. Empty in a + // ListSandboxPolicies projection when the stored payload is invalid under + // the current schema and therefore has no trusted canonical identity. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Load status of this revision. ListSandboxPolicies reports FAILED when a + // stored historical payload is invalid under the current schema, regardless + // of its persisted sandbox load status. + Status PolicyStatus `protobuf:"varint,3,opt,name=status,proto3,enum=openshell.v1.PolicyStatus" json:"status,omitempty"` + // Sandbox load error, or the schema-validation diagnostic for an invalid + // historical row returned by ListSandboxPolicies. + LoadError string `protobuf:"bytes,4,opt,name=load_error,json=loadError,proto3" json:"load_error,omitempty"` + // Time when this revision was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,105,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + // Time when this revision was loaded by the sandbox. Absent if not loaded. + LoadedTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=loaded_time,json=loadedTime,proto3" json:"loaded_time,omitempty"` + // The full policy (only populated when explicitly requested). + Policy *sandboxv1.SandboxPolicy `protobuf:"bytes,7,opt,name=policy,proto3" json:"policy,omitempty"` + // Immutable provenance supplied with this policy revision. + Provenance map[string]string `protobuf:"bytes,8,rep,name=provenance,proto3" json:"provenance,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxPolicyRevision) Reset() { + *x = SandboxPolicyRevision{} + mi := &file_openshell_proto_msgTypes[162] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxPolicyRevision) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxPolicyRevision) ProtoMessage() {} + +func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[162] + 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 SandboxPolicyRevision.ProtoReflect.Descriptor instead. +func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{162} +} + +func (x *SandboxPolicyRevision) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SandboxPolicyRevision) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *SandboxPolicyRevision) GetStatus() PolicyStatus { + if x != nil { + return x.Status + } + return PolicyStatus_POLICY_STATUS_UNSPECIFIED +} + +func (x *SandboxPolicyRevision) GetLoadError() string { + if x != nil { + return x.LoadError + } + return "" +} + +func (x *SandboxPolicyRevision) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *SandboxPolicyRevision) GetLoadedTime() *timestamppb.Timestamp { + if x != nil { + return x.LoadedTime + } + return nil +} + +func (x *SandboxPolicyRevision) GetPolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *SandboxPolicyRevision) GetProvenance() map[string]string { + if x != nil { + return x.Provenance + } + return nil +} + +// Get sandbox logs request (one-shot fetch). +type GetSandboxLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Canonical sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Maximum number of log lines to return. 0 means use default (2000). + Lines uint32 `protobuf:"varint,2,opt,name=lines,proto3" json:"lines,omitempty"` + // Only include logs at or after this time. Absence means no filter. + SinceTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=since_time,json=sinceTime,proto3" json:"since_time,omitempty"` + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + Sources []string `protobuf:"bytes,4,rep,name=sources,proto3" json:"sources,omitempty"` + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + MinLevel string `protobuf:"bytes,5,opt,name=min_level,json=minLevel,proto3" json:"min_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxLogsRequest) Reset() { + *x = GetSandboxLogsRequest{} + mi := &file_openshell_proto_msgTypes[163] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxLogsRequest) ProtoMessage() {} + +func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[163] + 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 GetSandboxLogsRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{163} +} + +func (x *GetSandboxLogsRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxLogsRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *GetSandboxLogsRequest) GetLines() uint32 { + if x != nil { + return x.Lines + } + return 0 +} + +func (x *GetSandboxLogsRequest) GetSinceTime() *timestamppb.Timestamp { + if x != nil { + return x.SinceTime + } + return nil +} + +func (x *GetSandboxLogsRequest) GetSources() []string { + if x != nil { + return x.Sources + } + return nil +} + +func (x *GetSandboxLogsRequest) GetMinLevel() string { + if x != nil { + return x.MinLevel + } + return "" +} + +// Batch of log lines pushed from sandbox to server. +type PushSandboxLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The sandbox ID. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Log lines to ingest. + Logs []*SandboxLogLine `protobuf:"bytes,2,rep,name=logs,proto3" json:"logs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PushSandboxLogsRequest) Reset() { + *x = PushSandboxLogsRequest{} + mi := &file_openshell_proto_msgTypes[164] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PushSandboxLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushSandboxLogsRequest) ProtoMessage() {} + +func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[164] + 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 PushSandboxLogsRequest.ProtoReflect.Descriptor instead. +func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{164} +} + +func (x *PushSandboxLogsRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *PushSandboxLogsRequest) GetLogs() []*SandboxLogLine { + if x != nil { + return x.Logs + } + return nil +} + +// Push sandbox logs response. +type PushSandboxLogsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PushSandboxLogsResponse) Reset() { + *x = PushSandboxLogsResponse{} + mi := &file_openshell_proto_msgTypes[165] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PushSandboxLogsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushSandboxLogsResponse) ProtoMessage() {} + +func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[165] + 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 PushSandboxLogsResponse.ProtoReflect.Descriptor instead. +func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{165} +} + +// Get sandbox logs response. +type GetSandboxLogsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Log lines in chronological order. + Logs []*SandboxLogLine `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` + // Total number of lines in the server's buffer for this sandbox. + BufferTotal uint32 `protobuf:"varint,2,opt,name=buffer_total,json=bufferTotal,proto3" json:"buffer_total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxLogsResponse) Reset() { + *x = GetSandboxLogsResponse{} + mi := &file_openshell_proto_msgTypes[166] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxLogsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxLogsResponse) ProtoMessage() {} + +func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[166] + 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 GetSandboxLogsResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{166} +} + +func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { + if x != nil { + return x.Logs + } + return nil +} + +func (x *GetSandboxLogsResponse) GetBufferTotal() uint32 { + if x != nil { + return x.BufferTotal + } + return 0 +} + +// Envelope for supervisor-to-gateway messages on the ConnectSupervisor stream. +type SupervisorMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *SupervisorMessage_Hello + // *SupervisorMessage_Heartbeat + // *SupervisorMessage_RelayOpenResult + // *SupervisorMessage_RelayClose + Payload isSupervisorMessage_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SupervisorMessage) Reset() { + *x = SupervisorMessage{} + mi := &file_openshell_proto_msgTypes[167] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SupervisorMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SupervisorMessage) ProtoMessage() {} + +func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[167] + 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 SupervisorMessage.ProtoReflect.Descriptor instead. +func (*SupervisorMessage) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{167} +} + +func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SupervisorMessage) GetHello() *SupervisorHello { + if x != nil { + if x, ok := x.Payload.(*SupervisorMessage_Hello); ok { + return x.Hello + } + } + return nil +} + +func (x *SupervisorMessage) GetHeartbeat() *SupervisorHeartbeat { + if x != nil { + if x, ok := x.Payload.(*SupervisorMessage_Heartbeat); ok { + return x.Heartbeat + } + } + return nil +} + +func (x *SupervisorMessage) GetRelayOpenResult() *RelayOpenResult { + if x != nil { + if x, ok := x.Payload.(*SupervisorMessage_RelayOpenResult); ok { + return x.RelayOpenResult + } + } + return nil +} + +func (x *SupervisorMessage) GetRelayClose() *RelayClose { + if x != nil { + if x, ok := x.Payload.(*SupervisorMessage_RelayClose); ok { + return x.RelayClose + } + } + return nil +} + +type isSupervisorMessage_Payload interface { + isSupervisorMessage_Payload() +} + +type SupervisorMessage_Hello struct { + Hello *SupervisorHello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` +} + +type SupervisorMessage_Heartbeat struct { + Heartbeat *SupervisorHeartbeat `protobuf:"bytes,2,opt,name=heartbeat,proto3,oneof"` +} + +type SupervisorMessage_RelayOpenResult struct { + RelayOpenResult *RelayOpenResult `protobuf:"bytes,3,opt,name=relay_open_result,json=relayOpenResult,proto3,oneof"` +} + +type SupervisorMessage_RelayClose struct { + RelayClose *RelayClose `protobuf:"bytes,4,opt,name=relay_close,json=relayClose,proto3,oneof"` +} + +func (*SupervisorMessage_Hello) isSupervisorMessage_Payload() {} + +func (*SupervisorMessage_Heartbeat) isSupervisorMessage_Payload() {} + +func (*SupervisorMessage_RelayOpenResult) isSupervisorMessage_Payload() {} + +func (*SupervisorMessage_RelayClose) isSupervisorMessage_Payload() {} + +// Envelope for gateway-to-supervisor messages on the ConnectSupervisor stream. +type GatewayMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *GatewayMessage_SessionAccepted + // *GatewayMessage_SessionRejected + // *GatewayMessage_Heartbeat + // *GatewayMessage_RelayOpen + // *GatewayMessage_RelayClose + // *GatewayMessage_SessionRedirect + Payload isGatewayMessage_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayMessage) Reset() { + *x = GatewayMessage{} + mi := &file_openshell_proto_msgTypes[168] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayMessage) ProtoMessage() {} + +func (x *GatewayMessage) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[168] + 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 GatewayMessage.ProtoReflect.Descriptor instead. +func (*GatewayMessage) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{168} +} + +func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *GatewayMessage) GetSessionAccepted() *SessionAccepted { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_SessionAccepted); ok { + return x.SessionAccepted + } + } + return nil +} + +func (x *GatewayMessage) GetSessionRejected() *SessionRejected { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_SessionRejected); ok { + return x.SessionRejected + } + } + return nil +} + +func (x *GatewayMessage) GetHeartbeat() *GatewayHeartbeat { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_Heartbeat); ok { + return x.Heartbeat + } + } + return nil +} + +func (x *GatewayMessage) GetRelayOpen() *RelayOpen { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_RelayOpen); ok { + return x.RelayOpen + } + } + return nil +} + +func (x *GatewayMessage) GetRelayClose() *RelayClose { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_RelayClose); ok { + return x.RelayClose + } + } + return nil +} + +func (x *GatewayMessage) GetSessionRedirect() *SessionRedirect { + if x != nil { + if x, ok := x.Payload.(*GatewayMessage_SessionRedirect); ok { + return x.SessionRedirect + } + } + return nil +} + +type isGatewayMessage_Payload interface { + isGatewayMessage_Payload() +} + +type GatewayMessage_SessionAccepted struct { + SessionAccepted *SessionAccepted `protobuf:"bytes,1,opt,name=session_accepted,json=sessionAccepted,proto3,oneof"` +} + +type GatewayMessage_SessionRejected struct { + SessionRejected *SessionRejected `protobuf:"bytes,2,opt,name=session_rejected,json=sessionRejected,proto3,oneof"` +} + +type GatewayMessage_Heartbeat struct { + Heartbeat *GatewayHeartbeat `protobuf:"bytes,3,opt,name=heartbeat,proto3,oneof"` +} + +type GatewayMessage_RelayOpen struct { + RelayOpen *RelayOpen `protobuf:"bytes,4,opt,name=relay_open,json=relayOpen,proto3,oneof"` +} + +type GatewayMessage_RelayClose struct { + RelayClose *RelayClose `protobuf:"bytes,5,opt,name=relay_close,json=relayClose,proto3,oneof"` +} + +type GatewayMessage_SessionRedirect struct { + SessionRedirect *SessionRedirect `protobuf:"bytes,6,opt,name=session_redirect,json=sessionRedirect,proto3,oneof"` +} + +func (*GatewayMessage_SessionAccepted) isGatewayMessage_Payload() {} + +func (*GatewayMessage_SessionRejected) isGatewayMessage_Payload() {} + +func (*GatewayMessage_Heartbeat) isGatewayMessage_Payload() {} + +func (*GatewayMessage_RelayOpen) isGatewayMessage_Payload() {} + +func (*GatewayMessage_RelayClose) isGatewayMessage_Payload() {} + +func (*GatewayMessage_SessionRedirect) isGatewayMessage_Payload() {} + +// Supervisor identifies itself and the sandbox it manages. +type SupervisorHello struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox ID this supervisor manages. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Supervisor instance ID (e.g. boot id or process epoch). + InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + // Monotonic counter scoped to instance_id. Incremented for each reconnect so + // gateways can distinguish a fresh supervisor connection from stale cleanup. + ConnectionEpoch uint64 `protobuf:"varint,3,opt,name=connection_epoch,json=connectionEpoch,proto3" json:"connection_epoch,omitempty"` + // The supervisor can report credential, policy, and launch-environment installation. + SupportsProviderReadiness bool `protobuf:"varint,4,opt,name=supports_provider_readiness,json=supportsProviderReadiness,proto3" json:"supports_provider_readiness,omitempty"` + // Set when this connection is the result of following a SessionRedirect. The + // receiving gateway serves a redirected hello locally instead of redirecting + // again, so a disagreement about membership cannot bounce the supervisor + // between replicas. A gateway that is shutting down may still redirect it. + Redirected bool `protobuf:"varint,5,opt,name=redirected,proto3" json:"redirected,omitempty"` + // The supervisor understands SessionRedirect. Gateways never redirect a + // supervisor that does not set this. + SupportsSessionRedirect bool `protobuf:"varint,6,opt,name=supports_session_redirect,json=supportsSessionRedirect,proto3" json:"supports_session_redirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SupervisorHello) Reset() { + *x = SupervisorHello{} + mi := &file_openshell_proto_msgTypes[169] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SupervisorHello) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SupervisorHello) ProtoMessage() {} + +func (x *SupervisorHello) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[169] + 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 SupervisorHello.ProtoReflect.Descriptor instead. +func (*SupervisorHello) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{169} +} + +func (x *SupervisorHello) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SupervisorHello) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *SupervisorHello) GetConnectionEpoch() uint64 { + if x != nil { + return x.ConnectionEpoch + } + return 0 +} + +func (x *SupervisorHello) GetSupportsProviderReadiness() bool { + if x != nil { + return x.SupportsProviderReadiness + } + return false +} + +func (x *SupervisorHello) GetRedirected() bool { + if x != nil { + return x.Redirected + } + return false +} + +func (x *SupervisorHello) GetSupportsSessionRedirect() bool { + if x != nil { + return x.SupportsSessionRedirect + } + return false +} + +// Gateway accepts the supervisor session. +type SessionAccepted struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-assigned session ID for this connection. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Recommended heartbeat interval. + HeartbeatInterval *durationpb.Duration `protobuf:"bytes,102,opt,name=heartbeat_interval,json=heartbeatInterval,proto3" json:"heartbeat_interval,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionAccepted) Reset() { + *x = SessionAccepted{} + mi := &file_openshell_proto_msgTypes[170] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionAccepted) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionAccepted) ProtoMessage() {} + +func (x *SessionAccepted) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[170] + 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 SessionAccepted.ProtoReflect.Descriptor instead. +func (*SessionAccepted) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{170} +} + +func (x *SessionAccepted) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionAccepted) GetHeartbeatInterval() *durationpb.Duration { + if x != nil { + return x.HeartbeatInterval + } + return nil +} + +// Gateway rejects the supervisor session. +type SessionRejected struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable rejection reason. + Reason string `protobuf:"bytes,1,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionRejected) Reset() { + *x = SessionRejected{} + mi := &file_openshell_proto_msgTypes[171] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionRejected) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionRejected) ProtoMessage() {} + +func (x *SessionRejected) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[171] + 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 SessionRejected.ProtoReflect.Descriptor instead. +func (*SessionRejected) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{171} +} + +func (x *SessionRejected) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// Gateway declines to own this session and names the replica that should. +// +// Sent instead of SessionAccepted when the placement ring puts the sandbox on +// a different live replica, and to established sessions when their gateway +// shuts down. The supervisor reconnects to peer_endpoint once with +// SupervisorHello.redirected set, and falls back to its configured gateway +// address if that fails. +type SessionRedirect struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Peer endpoint of the replica that should own this sandbox. + PeerEndpoint string `protobuf:"bytes,1,opt,name=peer_endpoint,json=peerEndpoint,proto3" json:"peer_endpoint,omitempty"` + // Replica ID that should own this sandbox, for logging. + OwnerReplicaId string `protobuf:"bytes,2,opt,name=owner_replica_id,json=ownerReplicaId,proto3" json:"owner_replica_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionRedirect) Reset() { + *x = SessionRedirect{} + mi := &file_openshell_proto_msgTypes[172] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionRedirect) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionRedirect) ProtoMessage() {} + +func (x *SessionRedirect) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[172] + 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 SessionRedirect.ProtoReflect.Descriptor instead. +func (*SessionRedirect) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{172} +} + +func (x *SessionRedirect) GetPeerEndpoint() string { + if x != nil { + return x.PeerEndpoint + } + return "" +} + +func (x *SessionRedirect) GetOwnerReplicaId() string { + if x != nil { + return x.OwnerReplicaId + } + return "" +} + +// Supervisor heartbeat. +type SupervisorHeartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SupervisorHeartbeat) Reset() { + *x = SupervisorHeartbeat{} + mi := &file_openshell_proto_msgTypes[173] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SupervisorHeartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SupervisorHeartbeat) ProtoMessage() {} + +func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[173] + 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 SupervisorHeartbeat.ProtoReflect.Descriptor instead. +func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{173} +} + +// Gateway heartbeat. +type GatewayHeartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayHeartbeat) Reset() { + *x = GatewayHeartbeat{} + mi := &file_openshell_proto_msgTypes[174] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayHeartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayHeartbeat) ProtoMessage() {} + +func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[174] + 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 GatewayHeartbeat.ProtoReflect.Descriptor instead. +func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{174} +} + +// Terminal result reported before the supervisor shuts down. A successful RPC +// response confirms that the result was durably handled by the gateway. +type ReportMainProcessExitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + // Normalized process result. Signal exits use 128 + signal number. + ExitCode int32 `protobuf:"varint,3,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportMainProcessExitRequest) Reset() { + *x = ReportMainProcessExitRequest{} + mi := &file_openshell_proto_msgTypes[175] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportMainProcessExitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportMainProcessExitRequest) ProtoMessage() {} + +func (x *ReportMainProcessExitRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[175] + 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 ReportMainProcessExitRequest.ProtoReflect.Descriptor instead. +func (*ReportMainProcessExitRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{175} +} + +func (x *ReportMainProcessExitRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportMainProcessExitRequest) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *ReportMainProcessExitRequest) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +type ReportMainProcessExitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportMainProcessExitResponse) Reset() { + *x = ReportMainProcessExitResponse{} + mi := &file_openshell_proto_msgTypes[176] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportMainProcessExitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportMainProcessExitResponse) ProtoMessage() {} + +func (x *ReportMainProcessExitResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[176] + 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 ReportMainProcessExitResponse.ProtoReflect.Descriptor instead. +func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{176} +} + +// Terminal-delivery completion reported after all expected foreground SSH +// attachments close. A successful response permits ephemeral cleanup. +type FinalizeMainProcessExitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeMainProcessExitRequest) Reset() { + *x = FinalizeMainProcessExitRequest{} + mi := &file_openshell_proto_msgTypes[177] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeMainProcessExitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeMainProcessExitRequest) ProtoMessage() {} + +func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[177] + 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 FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. +func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{177} +} + +func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *FinalizeMainProcessExitRequest) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +type FinalizeMainProcessExitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeMainProcessExitResponse) Reset() { + *x = FinalizeMainProcessExitResponse{} + mi := &file_openshell_proto_msgTypes[178] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeMainProcessExitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeMainProcessExitResponse) ProtoMessage() {} + +func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[178] + 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 FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. +func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{178} +} + +// Gateway requests the supervisor to open a relay channel. +// +// On receiving this, the supervisor should initiate a RelayStream RPC to +// the gateway, sending a RelayInit in the first RelayFrame to associate +// the new HTTP/2 stream with the pending relay slot. The supervisor +// bridges that stream to the requested local target. +type RelayOpen struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-allocated channel identifier (UUID). + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // Target the supervisor should dial inside the sandbox. + // If absent, supervisors treat the relay as SSH for compatibility. + // + // Types that are valid to be assigned to Target: + // + // *RelayOpen_Ssh + // *RelayOpen_Tcp + Target isRelayOpen_Target `protobuf_oneof:"target"` + // Optional service identifier for audit/correlation. + ServiceId string `protobuf:"bytes,5,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayOpen) Reset() { + *x = RelayOpen{} + mi := &file_openshell_proto_msgTypes[179] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayOpen) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayOpen) ProtoMessage() {} + +func (x *RelayOpen) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[179] + 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 RelayOpen.ProtoReflect.Descriptor instead. +func (*RelayOpen) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{179} +} + +func (x *RelayOpen) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *RelayOpen) GetTarget() isRelayOpen_Target { + if x != nil { + return x.Target + } + return nil +} + +func (x *RelayOpen) GetSsh() *SshRelayTarget { + if x != nil { + if x, ok := x.Target.(*RelayOpen_Ssh); ok { + return x.Ssh + } + } + return nil +} + +func (x *RelayOpen) GetTcp() *TcpRelayTarget { + if x != nil { + if x, ok := x.Target.(*RelayOpen_Tcp); ok { + return x.Tcp + } + } + return nil +} + +func (x *RelayOpen) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +type isRelayOpen_Target interface { + isRelayOpen_Target() +} + +type RelayOpen_Ssh struct { + Ssh *SshRelayTarget `protobuf:"bytes,2,opt,name=ssh,proto3,oneof"` +} + +type RelayOpen_Tcp struct { + Tcp *TcpRelayTarget `protobuf:"bytes,3,opt,name=tcp,proto3,oneof"` +} + +func (*RelayOpen_Ssh) isRelayOpen_Target() {} + +func (*RelayOpen_Tcp) isRelayOpen_Target() {} + +// Built-in SSH relay target. +type SshRelayTarget struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SshRelayTarget) Reset() { + *x = SshRelayTarget{} + mi := &file_openshell_proto_msgTypes[180] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SshRelayTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshRelayTarget) ProtoMessage() {} + +func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[180] + 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 SshRelayTarget.ProtoReflect.Descriptor instead. +func (*SshRelayTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{180} +} + +// TCP target dialed by the supervisor from inside the sandbox. +type TcpRelayTarget struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Phase 1 accepts loopback only: 127.0.0.1, ::1, or localhost. + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + // Target port. Must fit in u16 and be non-zero. + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpRelayTarget) Reset() { + *x = TcpRelayTarget{} + mi := &file_openshell_proto_msgTypes[181] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpRelayTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpRelayTarget) ProtoMessage() {} + +func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[181] + 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 TcpRelayTarget.ProtoReflect.Descriptor instead. +func (*TcpRelayTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{181} +} + +func (x *TcpRelayTarget) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *TcpRelayTarget) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +// Initial RelayStream frame sent by the supervisor to claim a pending relay. +type RelayInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-allocated channel identifier (UUID). + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayInit) Reset() { + *x = RelayInit{} + mi := &file_openshell_proto_msgTypes[182] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayInit) ProtoMessage() {} + +func (x *RelayInit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[182] + 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 RelayInit.ProtoReflect.Descriptor instead. +func (*RelayInit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{182} +} + +func (x *RelayInit) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +// A single frame on the RelayStream RPC. +// +// The supervisor MUST send `init` as the first frame. All subsequent frames +// in either direction carry raw bytes in `data`. +type RelayFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *RelayFrame_Init + // *RelayFrame_Data + Payload isRelayFrame_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayFrame) Reset() { + *x = RelayFrame{} + mi := &file_openshell_proto_msgTypes[183] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayFrame) ProtoMessage() {} + +func (x *RelayFrame) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[183] + 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 RelayFrame.ProtoReflect.Descriptor instead. +func (*RelayFrame) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{183} +} + +func (x *RelayFrame) GetPayload() isRelayFrame_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *RelayFrame) GetInit() *RelayInit { + if x != nil { + if x, ok := x.Payload.(*RelayFrame_Init); ok { + return x.Init + } + } + return nil +} + +func (x *RelayFrame) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*RelayFrame_Data); ok { + return x.Data + } + } + return nil +} + +type isRelayFrame_Payload interface { + isRelayFrame_Payload() +} + +type RelayFrame_Init struct { + Init *RelayInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type RelayFrame_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*RelayFrame_Init) isRelayFrame_Payload() {} + +func (*RelayFrame_Data) isRelayFrame_Payload() {} + +// Initial frame for gateway peer relay forwarding. +type PeerRelayInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable sandbox UUID whose supervisor relay should be opened. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Relay target to ask the owning gateway to open on its local supervisor + // session. The channel_id is assigned by the forwarding gateway. + RelayOpen *RelayOpen `protobuf:"bytes,2,opt,name=relay_open,json=relayOpen,proto3" json:"relay_open,omitempty"` + // Gateway replica id that initiated the peer relay. + RequesterReplicaId string `protobuf:"bytes,3,opt,name=requester_replica_id,json=requesterReplicaId,proto3" json:"requester_replica_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerRelayInit) Reset() { + *x = PeerRelayInit{} + mi := &file_openshell_proto_msgTypes[184] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerRelayInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerRelayInit) ProtoMessage() {} + +func (x *PeerRelayInit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[184] + 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 PeerRelayInit.ProtoReflect.Descriptor instead. +func (*PeerRelayInit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{184} +} + +func (x *PeerRelayInit) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *PeerRelayInit) GetRelayOpen() *RelayOpen { + if x != nil { + return x.RelayOpen + } + return nil +} + +func (x *PeerRelayInit) GetRequesterReplicaId() string { + if x != nil { + return x.RequesterReplicaId + } + return "" +} + +// A single frame on the gateway-to-gateway peer relay RPC. +type PeerRelayFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *PeerRelayFrame_Init + // *PeerRelayFrame_Data + Payload isPeerRelayFrame_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerRelayFrame) Reset() { + *x = PeerRelayFrame{} + mi := &file_openshell_proto_msgTypes[185] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerRelayFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerRelayFrame) ProtoMessage() {} + +func (x *PeerRelayFrame) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[185] + 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 PeerRelayFrame.ProtoReflect.Descriptor instead. +func (*PeerRelayFrame) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{185} +} + +func (x *PeerRelayFrame) GetPayload() isPeerRelayFrame_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *PeerRelayFrame) GetInit() *PeerRelayInit { + if x != nil { + if x, ok := x.Payload.(*PeerRelayFrame_Init); ok { + return x.Init + } + } + return nil +} + +func (x *PeerRelayFrame) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*PeerRelayFrame_Data); ok { + return x.Data + } + } + return nil +} + +type isPeerRelayFrame_Payload interface { + isPeerRelayFrame_Payload() +} + +type PeerRelayFrame_Init struct { + Init *PeerRelayInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type PeerRelayFrame_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*PeerRelayFrame_Init) isPeerRelayFrame_Payload() {} + +func (*PeerRelayFrame_Data) isPeerRelayFrame_Payload() {} + +// Supervisor reports the result of a relay open request. +type RelayOpenResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Channel identifier from the RelayOpen request. + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // True if the relay was successfully established. + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + // Error message if success is false. + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayOpenResult) Reset() { + *x = RelayOpenResult{} + mi := &file_openshell_proto_msgTypes[186] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayOpenResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayOpenResult) ProtoMessage() {} + +func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[186] + 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 RelayOpenResult.ProtoReflect.Descriptor instead. +func (*RelayOpenResult) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{186} +} + +func (x *RelayOpenResult) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *RelayOpenResult) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *RelayOpenResult) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// Either side requests closure of a relay channel. +type RelayClose struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Channel identifier to close. + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // Optional reason for closure. + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayClose) Reset() { + *x = RelayClose{} + mi := &file_openshell_proto_msgTypes[187] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayClose) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayClose) ProtoMessage() {} + +func (x *RelayClose) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[187] + 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 RelayClose.ProtoReflect.Descriptor instead. +func (*RelayClose) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{187} +} + +func (x *RelayClose) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +func (x *RelayClose) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// Observed HTTP method+path pattern from L7 inspection. +type L7RequestSample struct { + state protoimpl.MessageState `protogen:"open.v1"` + // HTTP method: GET, POST, PUT, DELETE, etc. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // HTTP path: /v1/models, /repos/myorg/issues + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // L7 decision: "audit" or "deny" (allowed requests not collected). + Decision string `protobuf:"bytes,3,opt,name=decision,proto3" json:"decision,omitempty"` + // Number of times this (method, path) was observed. + Count uint32 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7RequestSample) Reset() { + *x = L7RequestSample{} + mi := &file_openshell_proto_msgTypes[188] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7RequestSample) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7RequestSample) ProtoMessage() {} + +func (x *L7RequestSample) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[188] + 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 L7RequestSample.ProtoReflect.Descriptor instead. +func (*L7RequestSample) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{188} +} + +func (x *L7RequestSample) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *L7RequestSample) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *L7RequestSample) GetDecision() string { + if x != nil { + return x.Decision + } + return "" +} + +func (x *L7RequestSample) GetCount() uint32 { + if x != nil { + return x.Count + } + return 0 +} + +// Structured denial summary from sandbox aggregator. +type DenialSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox ID that produced this summary. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Denied destination host. + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + // Denied destination port. + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` + // Binary that attempted the connection. + Binary string `protobuf:"bytes,4,opt,name=binary,proto3" json:"binary,omitempty"` + // Process ancestor chain. + Ancestors []string `protobuf:"bytes,5,rep,name=ancestors,proto3" json:"ancestors,omitempty"` + // Denial reason from OPA evaluation. + DenyReason string `protobuf:"bytes,6,opt,name=deny_reason,json=denyReason,proto3" json:"deny_reason,omitempty"` + // Time of the first denial. + FirstSeenTime *timestamppb.Timestamp `protobuf:"bytes,107,opt,name=first_seen_time,json=firstSeenTime,proto3" json:"first_seen_time,omitempty"` + // Time of the most recent denial. + LastSeenTime *timestamppb.Timestamp `protobuf:"bytes,108,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"` + // Number of denials in the current window. + Count uint32 `protobuf:"varint,9,opt,name=count,proto3" json:"count,omitempty"` + // Events dropped during aggregator cooldown. + SuppressedCount uint32 `protobuf:"varint,10,opt,name=suppressed_count,json=suppressedCount,proto3" json:"suppressed_count,omitempty"` + // Cumulative lifetime count (never resets). + TotalCount uint32 `protobuf:"varint,11,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` + // Distinct cmdline strings observed (sanitized of credentials). + SampleCmdlines []string `protobuf:"bytes,12,rep,name=sample_cmdlines,json=sampleCmdlines,proto3" json:"sample_cmdlines,omitempty"` + // SHA-256 of the binary for audit trail. + BinarySha256 string `protobuf:"bytes,13,opt,name=binary_sha256,json=binarySha256,proto3" json:"binary_sha256,omitempty"` + // True if emitted by stale-flush rather than threshold. + Persistent bool `protobuf:"varint,14,opt,name=persistent,proto3" json:"persistent,omitempty"` + // Denial category: "l4_deny", "l7_deny", "l7_audit", "ssrf". + DenialStage string `protobuf:"bytes,15,opt,name=denial_stage,json=denialStage,proto3" json:"denial_stage,omitempty"` + // Observed HTTP request patterns (from L7 inspection). + L7RequestSamples []*L7RequestSample `protobuf:"bytes,16,rep,name=l7_request_samples,json=l7RequestSamples,proto3" json:"l7_request_samples,omitempty"` + // True if L7 inspection was active during observation window. + L7InspectionActive bool `protobuf:"varint,17,opt,name=l7_inspection_active,json=l7InspectionActive,proto3" json:"l7_inspection_active,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DenialSummary) Reset() { + *x = DenialSummary{} + mi := &file_openshell_proto_msgTypes[189] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DenialSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DenialSummary) ProtoMessage() {} + +func (x *DenialSummary) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[189] + 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 DenialSummary.ProtoReflect.Descriptor instead. +func (*DenialSummary) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{189} +} + +func (x *DenialSummary) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *DenialSummary) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *DenialSummary) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *DenialSummary) GetBinary() string { + if x != nil { + return x.Binary + } + return "" +} + +func (x *DenialSummary) GetAncestors() []string { + if x != nil { + return x.Ancestors + } + return nil +} + +func (x *DenialSummary) GetDenyReason() string { + if x != nil { + return x.DenyReason + } + return "" +} + +func (x *DenialSummary) GetFirstSeenTime() *timestamppb.Timestamp { + if x != nil { + return x.FirstSeenTime + } + return nil +} + +func (x *DenialSummary) GetLastSeenTime() *timestamppb.Timestamp { + if x != nil { + return x.LastSeenTime + } + return nil +} + +func (x *DenialSummary) GetCount() uint32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *DenialSummary) GetSuppressedCount() uint32 { + if x != nil { + return x.SuppressedCount + } + return 0 +} + +func (x *DenialSummary) GetTotalCount() uint32 { + if x != nil { + return x.TotalCount + } + return 0 +} + +func (x *DenialSummary) GetSampleCmdlines() []string { + if x != nil { + return x.SampleCmdlines + } + return nil +} + +func (x *DenialSummary) GetBinarySha256() string { + if x != nil { + return x.BinarySha256 + } + return "" +} + +func (x *DenialSummary) GetPersistent() bool { + if x != nil { + return x.Persistent + } + return false +} + +func (x *DenialSummary) GetDenialStage() string { + if x != nil { + return x.DenialStage + } + return "" +} + +func (x *DenialSummary) GetL7RequestSamples() []*L7RequestSample { + if x != nil { + return x.L7RequestSamples + } + return nil +} + +func (x *DenialSummary) GetL7InspectionActive() bool { + if x != nil { + return x.L7InspectionActive + } + return false +} + +// Count of denied actions grouped only by sanitized telemetry category. +type DenialGroupCount struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sanitized denial category, e.g. "connect_policy", "l7_policy", "ssrf". + DenyGroup string `protobuf:"bytes,1,opt,name=deny_group,json=denyGroup,proto3" json:"deny_group,omitempty"` + // Number of denied actions in this category. + DeniedCount uint32 `protobuf:"varint,2,opt,name=denied_count,json=deniedCount,proto3" json:"denied_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DenialGroupCount) Reset() { + *x = DenialGroupCount{} + mi := &file_openshell_proto_msgTypes[190] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DenialGroupCount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DenialGroupCount) ProtoMessage() {} + +func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[190] + 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 DenialGroupCount.ProtoReflect.Descriptor instead. +func (*DenialGroupCount) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{190} +} + +func (x *DenialGroupCount) GetDenyGroup() string { + if x != nil { + return x.DenyGroup + } + return "" +} + +func (x *DenialGroupCount) GetDeniedCount() uint32 { + if x != nil { + return x.DeniedCount + } + return 0 +} + +// Anonymous sandbox network activity counters. This intentionally excludes +// hosts, paths, binaries, raw deny reasons, sandbox IDs, and user content. +type NetworkActivitySummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Total observed network activities in the current window. + NetworkActivityCount uint32 `protobuf:"varint,1,opt,name=network_activity_count,json=networkActivityCount,proto3" json:"network_activity_count,omitempty"` + // Total denied actions in the current window. + DeniedActionCount uint32 `protobuf:"varint,2,opt,name=denied_action_count,json=deniedActionCount,proto3" json:"denied_action_count,omitempty"` + // Denied action counts grouped by sanitized category. + DenialsByGroup []*DenialGroupCount `protobuf:"bytes,3,rep,name=denials_by_group,json=denialsByGroup,proto3" json:"denials_by_group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkActivitySummary) Reset() { + *x = NetworkActivitySummary{} + mi := &file_openshell_proto_msgTypes[191] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkActivitySummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkActivitySummary) ProtoMessage() {} + +func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[191] + 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 NetworkActivitySummary.ProtoReflect.Descriptor instead. +func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{191} +} + +func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { + if x != nil { + return x.NetworkActivityCount + } + return 0 +} + +func (x *NetworkActivitySummary) GetDeniedActionCount() uint32 { + if x != nil { + return x.DeniedActionCount + } + return 0 +} + +func (x *NetworkActivitySummary) GetDenialsByGroup() []*DenialGroupCount { + if x != nil { + return x.DenialsByGroup + } + return nil +} + +// A proposed policy rule with rationale and approval status. +type PolicyChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique chunk identifier. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Approval status: "pending", "approved", "rejected". + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + // Proposed network_policies map key. + RuleName string `protobuf:"bytes,3,opt,name=rule_name,json=ruleName,proto3" json:"rule_name,omitempty"` + // The proposed network policy rule. + ProposedRule *sandboxv1.NetworkPolicyRule `protobuf:"bytes,4,opt,name=proposed_rule,json=proposedRule,proto3" json:"proposed_rule,omitempty"` + // Human-readable explanation of why this rule is proposed. + Rationale string `protobuf:"bytes,5,opt,name=rationale,proto3" json:"rationale,omitempty"` + // Security concerns flagged by analysis (empty if none). + SecurityNotes string `protobuf:"bytes,6,opt,name=security_notes,json=securityNotes,proto3" json:"security_notes,omitempty"` + // Analysis confidence (0.0-1.0). 0 for mechanistic mode. + Confidence float32 `protobuf:"fixed32,7,opt,name=confidence,proto3" json:"confidence,omitempty"` + // IDs of denial summaries that led to this chunk. + DenialSummaryIds []string `protobuf:"bytes,8,rep,name=denial_summary_ids,json=denialSummaryIds,proto3" json:"denial_summary_ids,omitempty"` + // Time when this chunk was created. + CreatedTime *timestamppb.Timestamp `protobuf:"bytes,109,opt,name=created_time,json=createdTime,proto3" json:"created_time,omitempty"` + // Time when the user approved or rejected the chunk. Absent if undecided. + DecidedTime *timestamppb.Timestamp `protobuf:"bytes,110,opt,name=decided_time,json=decidedTime,proto3" json:"decided_time,omitempty"` + // Recommendation stage: "initial" or "refined" (progressive L7 visibility). + Stage string `protobuf:"bytes,11,opt,name=stage,proto3" json:"stage,omitempty"` + // For stage="refined": the initial chunk this replaces. + SupersedesChunkId string `protobuf:"bytes,12,opt,name=supersedes_chunk_id,json=supersedesChunkId,proto3" json:"supersedes_chunk_id,omitempty"` + // How many times this endpoint has been seen across denial flush cycles. + HitCount int32 `protobuf:"varint,13,opt,name=hit_count,json=hitCount,proto3" json:"hit_count,omitempty"` + // First time this endpoint was proposed. + FirstSeenTime *timestamppb.Timestamp `protobuf:"bytes,114,opt,name=first_seen_time,json=firstSeenTime,proto3" json:"first_seen_time,omitempty"` + // Most recent time this endpoint was proposed again. + LastSeenTime *timestamppb.Timestamp `protobuf:"bytes,115,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"` + // Binary path that triggered the denial (denormalized for display convenience). + Binary string `protobuf:"bytes,16,opt,name=binary,proto3" json:"binary,omitempty"` + // Validation verdict from gateway-side static checks (prover output). + // Free-form summary string for human consumption in the inbox card. + // Empty until the prover has run for this chunk. + ValidationResult string `protobuf:"bytes,17,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` + // Operator-supplied free-form text accompanying a rejection. Populated + // when the reviewer rejects via `RejectDraftChunkRequest.reason`; surfaced + // back to the in-sandbox agent so it can revise the proposal. + // Empty for non-rejected chunks. + RejectionReason string `protobuf:"bytes,18,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + // Gateway-side merge/application preflight failure. Kept separate from + // prover output and operator rejection so clients can explain why a + // prover-clean proposal is not currently applicable. + ApplicationError string `protobuf:"bytes,19,opt,name=application_error,json=applicationError,proto3" json:"application_error,omitempty"` + // Opaque digest binding review to the exact live inputs and complete + // effective candidate evaluated by the gateway. + ReviewToken string `protobuf:"bytes,20,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + // Deterministic hashes for compact candidate display and diagnostics. + CurrentEffectivePolicyHash string `protobuf:"bytes,21,opt,name=current_effective_policy_hash,json=currentEffectivePolicyHash,proto3" json:"current_effective_policy_hash,omitempty"` + CandidateEffectivePolicyHash string `protobuf:"bytes,22,opt,name=candidate_effective_policy_hash,json=candidateEffectivePolicyHash,proto3" json:"candidate_effective_policy_hash,omitempty"` + // Complete effective policies used for review. These contain policy + // configuration only; credential secret values are never materialized. + CurrentEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,23,opt,name=current_effective_policy,json=currentEffectivePolicy,proto3" json:"current_effective_policy,omitempty"` + CandidateEffectivePolicy *sandboxv1.SandboxPolicy `protobuf:"bytes,24,opt,name=candidate_effective_policy,json=candidateEffectivePolicy,proto3" json:"candidate_effective_policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyChunk) Reset() { + *x = PolicyChunk{} + mi := &file_openshell_proto_msgTypes[192] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyChunk) ProtoMessage() {} + +func (x *PolicyChunk) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[192] + 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 PolicyChunk.ProtoReflect.Descriptor instead. +func (*PolicyChunk) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{192} +} + +func (x *PolicyChunk) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PolicyChunk) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PolicyChunk) GetRuleName() string { + if x != nil { + return x.RuleName + } + return "" +} + +func (x *PolicyChunk) GetProposedRule() *sandboxv1.NetworkPolicyRule { + if x != nil { + return x.ProposedRule + } + return nil +} + +func (x *PolicyChunk) GetRationale() string { + if x != nil { + return x.Rationale + } + return "" +} + +func (x *PolicyChunk) GetSecurityNotes() string { + if x != nil { + return x.SecurityNotes + } + return "" +} + +func (x *PolicyChunk) GetConfidence() float32 { + if x != nil { + return x.Confidence + } + return 0 +} + +func (x *PolicyChunk) GetDenialSummaryIds() []string { + if x != nil { + return x.DenialSummaryIds + } + return nil +} + +func (x *PolicyChunk) GetCreatedTime() *timestamppb.Timestamp { + if x != nil { + return x.CreatedTime + } + return nil +} + +func (x *PolicyChunk) GetDecidedTime() *timestamppb.Timestamp { + if x != nil { + return x.DecidedTime + } + return nil +} + +func (x *PolicyChunk) GetStage() string { + if x != nil { + return x.Stage + } + return "" +} + +func (x *PolicyChunk) GetSupersedesChunkId() string { + if x != nil { + return x.SupersedesChunkId + } + return "" +} + +func (x *PolicyChunk) GetHitCount() int32 { + if x != nil { + return x.HitCount + } + return 0 +} + +func (x *PolicyChunk) GetFirstSeenTime() *timestamppb.Timestamp { + if x != nil { + return x.FirstSeenTime + } + return nil +} + +func (x *PolicyChunk) GetLastSeenTime() *timestamppb.Timestamp { + if x != nil { + return x.LastSeenTime + } + return nil +} + +func (x *PolicyChunk) GetBinary() string { + if x != nil { + return x.Binary + } + return "" +} + +func (x *PolicyChunk) GetValidationResult() string { + if x != nil { + return x.ValidationResult + } + return "" +} + +func (x *PolicyChunk) GetRejectionReason() string { + if x != nil { + return x.RejectionReason + } + return "" +} + +func (x *PolicyChunk) GetApplicationError() string { + if x != nil { + return x.ApplicationError + } + return "" +} + +func (x *PolicyChunk) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +func (x *PolicyChunk) GetCurrentEffectivePolicyHash() string { + if x != nil { + return x.CurrentEffectivePolicyHash + } + return "" +} + +func (x *PolicyChunk) GetCandidateEffectivePolicyHash() string { + if x != nil { + return x.CandidateEffectivePolicyHash + } + return "" +} + +func (x *PolicyChunk) GetCurrentEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CurrentEffectivePolicy + } + return nil +} + +func (x *PolicyChunk) GetCandidateEffectivePolicy() *sandboxv1.SandboxPolicy { + if x != nil { + return x.CandidateEffectivePolicy + } + return nil +} + +// Notification that the draft policy was updated. +type DraftPolicyUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Current draft version. + DraftVersion uint64 `protobuf:"varint,1,opt,name=draft_version,json=draftVersion,proto3" json:"draft_version,omitempty"` + // Number of new chunks added in this update. + NewChunks uint32 `protobuf:"varint,2,opt,name=new_chunks,json=newChunks,proto3" json:"new_chunks,omitempty"` + // Total pending chunks awaiting approval. + TotalPending uint32 `protobuf:"varint,3,opt,name=total_pending,json=totalPending,proto3" json:"total_pending,omitempty"` + // Brief description of what changed. + Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DraftPolicyUpdate) Reset() { + *x = DraftPolicyUpdate{} + mi := &file_openshell_proto_msgTypes[193] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DraftPolicyUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DraftPolicyUpdate) ProtoMessage() {} + +func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[193] + 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 DraftPolicyUpdate.ProtoReflect.Descriptor instead. +func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{193} +} + +func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { + if x != nil { + return x.DraftVersion + } + return 0 +} + +func (x *DraftPolicyUpdate) GetNewChunks() uint32 { + if x != nil { + return x.NewChunks + } + return 0 +} + +func (x *DraftPolicyUpdate) GetTotalPending() uint32 { + if x != nil { + return x.TotalPending + } + return 0 +} + +func (x *DraftPolicyUpdate) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +// Submit analysis results from sandbox to gateway. +type SubmitPolicyAnalysisRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,6,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Aggregated denial summaries. + Summaries []*DenialSummary `protobuf:"bytes,1,rep,name=summaries,proto3" json:"summaries,omitempty"` + // Proposed policy chunks (validated by sandbox OPA engine). + ProposedChunks []*PolicyChunk `protobuf:"bytes,2,rep,name=proposed_chunks,json=proposedChunks,proto3" json:"proposed_chunks,omitempty"` + // Analysis mode. `mechanistic` is the observation-driven path from the + // denial aggregator — chunks targeting the same host|port|binary fold + // into one row with hit_count incremented. `agent_authored` is an + // intentional proposal from an in-sandbox agent — each submission lands + // as its own chunk so the redraft-after-rejection loop has a stable id + // to watch. Other values are treated as agent-style (no dedup) so a new + // mode does not silently collapse proposals. + AnalysisMode string `protobuf:"bytes,3,opt,name=analysis_mode,json=analysisMode,proto3" json:"analysis_mode,omitempty"` + // Sandbox name. The authenticated sandbox principal remains authoritative + // for this internal callback. + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + // Anonymous network activity counters. + NetworkActivitySummaries []*NetworkActivitySummary `protobuf:"bytes,5,rep,name=network_activity_summaries,json=networkActivitySummaries,proto3" json:"network_activity_summaries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitPolicyAnalysisRequest) Reset() { + *x = SubmitPolicyAnalysisRequest{} + mi := &file_openshell_proto_msgTypes[194] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitPolicyAnalysisRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} + +func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[194] + 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 SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. +func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{194} +} + +func (x *SubmitPolicyAnalysisRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { + if x != nil { + return x.Summaries + } + return nil +} + +func (x *SubmitPolicyAnalysisRequest) GetProposedChunks() []*PolicyChunk { + if x != nil { + return x.ProposedChunks + } + return nil +} + +func (x *SubmitPolicyAnalysisRequest) GetAnalysisMode() string { + if x != nil { + return x.AnalysisMode + } + return "" +} + +func (x *SubmitPolicyAnalysisRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SubmitPolicyAnalysisRequest) GetNetworkActivitySummaries() []*NetworkActivitySummary { + if x != nil { + return x.NetworkActivitySummaries + } + return nil +} + +type SubmitPolicyAnalysisResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Number of chunks accepted by the gateway. + AcceptedChunks uint32 `protobuf:"varint,1,opt,name=accepted_chunks,json=acceptedChunks,proto3" json:"accepted_chunks,omitempty"` + // Number of chunks rejected by gateway validation. + RejectedChunks uint32 `protobuf:"varint,2,opt,name=rejected_chunks,json=rejectedChunks,proto3" json:"rejected_chunks,omitempty"` + // Reasons for each rejected chunk. + RejectionReasons []string `protobuf:"bytes,3,rep,name=rejection_reasons,json=rejectionReasons,proto3" json:"rejection_reasons,omitempty"` + // Server-assigned chunk IDs for the accepted chunks, in submission order. + // Agents use these to watch proposal state via policy.local's + // GET /v1/proposals/{id} and /wait endpoints. + AcceptedChunkIds []string `protobuf:"bytes,4,rep,name=accepted_chunk_ids,json=acceptedChunkIds,proto3" json:"accepted_chunk_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitPolicyAnalysisResponse) Reset() { + *x = SubmitPolicyAnalysisResponse{} + mi := &file_openshell_proto_msgTypes[195] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitPolicyAnalysisResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} + +func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[195] + 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 SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. +func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{195} +} + +func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { + if x != nil { + return x.AcceptedChunks + } + return 0 +} + +func (x *SubmitPolicyAnalysisResponse) GetRejectedChunks() uint32 { + if x != nil { + return x.RejectedChunks + } + return 0 +} + +func (x *SubmitPolicyAnalysisResponse) GetRejectionReasons() []string { + if x != nil { + return x.RejectionReasons + } + return nil +} + +func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunkIds() []string { + if x != nil { + return x.AcceptedChunkIds + } + return nil +} + +// Get draft policy for a sandbox. +type GetDraftPolicyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Optional status filter: "pending", "approved", "rejected", or "" for all. + StatusFilter string `protobuf:"bytes,2,opt,name=status_filter,json=statusFilter,proto3" json:"status_filter,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDraftPolicyRequest) Reset() { + *x = GetDraftPolicyRequest{} + mi := &file_openshell_proto_msgTypes[196] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDraftPolicyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDraftPolicyRequest) ProtoMessage() {} + +func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[196] + 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 GetDraftPolicyRequest.ProtoReflect.Descriptor instead. +func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{196} +} + +func (x *GetDraftPolicyRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetDraftPolicyRequest) GetStatusFilter() string { + if x != nil { + return x.StatusFilter + } + return "" +} + +func (x *GetDraftPolicyRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +type GetDraftPolicyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Draft policy chunks. + Chunks []*PolicyChunk `protobuf:"bytes,1,rep,name=chunks,proto3" json:"chunks,omitempty"` + // LLM-generated summary of all analysis (empty in mechanistic mode). + RollingSummary string `protobuf:"bytes,2,opt,name=rolling_summary,json=rollingSummary,proto3" json:"rolling_summary,omitempty"` + // Current draft version. + DraftVersion uint64 `protobuf:"varint,3,opt,name=draft_version,json=draftVersion,proto3" json:"draft_version,omitempty"` + // Time when the last analysis completed. + LastAnalyzedTime *timestamppb.Timestamp `protobuf:"bytes,104,opt,name=last_analyzed_time,json=lastAnalyzedTime,proto3" json:"last_analyzed_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDraftPolicyResponse) Reset() { + *x = GetDraftPolicyResponse{} + mi := &file_openshell_proto_msgTypes[197] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDraftPolicyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDraftPolicyResponse) ProtoMessage() {} + +func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[197] + 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 GetDraftPolicyResponse.ProtoReflect.Descriptor instead. +func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{197} +} + +func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { + if x != nil { + return x.Chunks + } + return nil +} + +func (x *GetDraftPolicyResponse) GetRollingSummary() string { + if x != nil { + return x.RollingSummary + } + return "" +} + +func (x *GetDraftPolicyResponse) GetDraftVersion() uint64 { + if x != nil { + return x.DraftVersion + } + return 0 +} + +func (x *GetDraftPolicyResponse) GetLastAnalyzedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastAnalyzedTime + } + return nil +} + +// Approve a single draft chunk. +type ApproveDraftChunkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Chunk ID to approve. + ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + // Token returned with the reviewed PolicyChunk. Approval fails with + // FAILED_PRECONDITION if live decision inputs no longer match it. + ReviewToken string `protobuf:"bytes,4,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApproveDraftChunkRequest) Reset() { + *x = ApproveDraftChunkRequest{} + mi := &file_openshell_proto_msgTypes[198] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApproveDraftChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApproveDraftChunkRequest) ProtoMessage() {} + +func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[198] + 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 ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. +func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{198} +} + +func (x *ApproveDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ApproveDraftChunkRequest) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *ApproveDraftChunkRequest) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +func (x *ApproveDraftChunkRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ApproveDraftChunkRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ApproveDraftChunkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // New policy version after merge. + PolicyVersion uint32 `protobuf:"varint,1,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + // SHA-256 hash of the new policy. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApproveDraftChunkResponse) Reset() { + *x = ApproveDraftChunkResponse{} + mi := &file_openshell_proto_msgTypes[199] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApproveDraftChunkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApproveDraftChunkResponse) ProtoMessage() {} + +func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[199] + 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 ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. +func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{199} +} + +func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *ApproveDraftChunkResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +// Reject a single draft chunk. +type RejectDraftChunkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Chunk ID to reject. + ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + // Optional reason for rejection (fed to LLM context in future analysis). + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RejectDraftChunkRequest) Reset() { + *x = RejectDraftChunkRequest{} + mi := &file_openshell_proto_msgTypes[200] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RejectDraftChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RejectDraftChunkRequest) ProtoMessage() {} + +func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[200] + 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 RejectDraftChunkRequest.ProtoReflect.Descriptor instead. +func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{200} +} + +func (x *RejectDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *RejectDraftChunkRequest) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *RejectDraftChunkRequest) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *RejectDraftChunkRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *RejectDraftChunkRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type RejectDraftChunkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RejectDraftChunkResponse) Reset() { + *x = RejectDraftChunkResponse{} + mi := &file_openshell_proto_msgTypes[201] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RejectDraftChunkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RejectDraftChunkResponse) ProtoMessage() {} + +func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[201] + 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 RejectDraftChunkResponse.ProtoReflect.Descriptor instead. +func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{201} +} + +// Approve all pending chunks. +type DraftChunkApproval struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChunkId string `protobuf:"bytes,1,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + ReviewToken string `protobuf:"bytes,2,opt,name=review_token,json=reviewToken,proto3" json:"review_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DraftChunkApproval) Reset() { + *x = DraftChunkApproval{} + mi := &file_openshell_proto_msgTypes[202] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DraftChunkApproval) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DraftChunkApproval) ProtoMessage() {} + +func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[202] + 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 DraftChunkApproval.ProtoReflect.Descriptor instead. +func (*DraftChunkApproval) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{202} +} + +func (x *DraftChunkApproval) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *DraftChunkApproval) GetReviewToken() string { + if x != nil { + return x.ReviewToken + } + return "" +} + +type ApproveAllDraftChunksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Include chunks with security_notes (default false: skips them). + IncludeSecurityFlagged bool `protobuf:"varint,2,opt,name=include_security_flagged,json=includeSecurityFlagged,proto3" json:"include_security_flagged,omitempty"` + // Exact reviewed chunks and tokens. The server validates them against one + // live snapshot, stages compatible operations in order, and writes once. + Approvals []*DraftChunkApproval `protobuf:"bytes,3,rep,name=approvals,proto3" json:"approvals,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApproveAllDraftChunksRequest) Reset() { + *x = ApproveAllDraftChunksRequest{} + mi := &file_openshell_proto_msgTypes[203] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApproveAllDraftChunksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApproveAllDraftChunksRequest) ProtoMessage() {} + +func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[203] + 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 ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. +func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{203} +} + +func (x *ApproveAllDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ApproveAllDraftChunksRequest) GetIncludeSecurityFlagged() bool { + if x != nil { + return x.IncludeSecurityFlagged + } + return false +} + +func (x *ApproveAllDraftChunksRequest) GetApprovals() []*DraftChunkApproval { + if x != nil { + return x.Approvals + } + return nil +} + +func (x *ApproveAllDraftChunksRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ApproveAllDraftChunksRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ApproveAllDraftChunksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // New policy version after merge. + PolicyVersion uint32 `protobuf:"varint,1,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + // SHA-256 hash of the new policy. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Number of chunks approved. + ChunksApproved uint32 `protobuf:"varint,3,opt,name=chunks_approved,json=chunksApproved,proto3" json:"chunks_approved,omitempty"` + // Number of chunks skipped for any reason, including security flags, + // stale or invalid candidates, and conflicts within the staged batch. + ChunksSkipped uint32 `protobuf:"varint,4,opt,name=chunks_skipped,json=chunksSkipped,proto3" json:"chunks_skipped,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApproveAllDraftChunksResponse) Reset() { + *x = ApproveAllDraftChunksResponse{} + mi := &file_openshell_proto_msgTypes[204] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApproveAllDraftChunksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApproveAllDraftChunksResponse) ProtoMessage() {} + +func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[204] + 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 ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. +func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{204} +} + +func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *ApproveAllDraftChunksResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *ApproveAllDraftChunksResponse) GetChunksApproved() uint32 { + if x != nil { + return x.ChunksApproved + } + return 0 +} + +func (x *ApproveAllDraftChunksResponse) GetChunksSkipped() uint32 { + if x != nil { + return x.ChunksSkipped + } + return 0 +} + +// Edit a pending chunk in-place. +type EditDraftChunkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,4,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Chunk ID to edit. + ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + // The modified rule (replaces existing proposed_rule). + ProposedRule *sandboxv1.NetworkPolicyRule `protobuf:"bytes,3,opt,name=proposed_rule,json=proposedRule,proto3" json:"proposed_rule,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,5,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EditDraftChunkRequest) Reset() { + *x = EditDraftChunkRequest{} + mi := &file_openshell_proto_msgTypes[205] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EditDraftChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EditDraftChunkRequest) ProtoMessage() {} + +func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[205] + 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 EditDraftChunkRequest.ProtoReflect.Descriptor instead. +func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{205} +} + +func (x *EditDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *EditDraftChunkRequest) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *EditDraftChunkRequest) GetProposedRule() *sandboxv1.NetworkPolicyRule { + if x != nil { + return x.ProposedRule + } + return nil +} + +func (x *EditDraftChunkRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *EditDraftChunkRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type EditDraftChunkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EditDraftChunkResponse) Reset() { + *x = EditDraftChunkResponse{} + mi := &file_openshell_proto_msgTypes[206] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EditDraftChunkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EditDraftChunkResponse) ProtoMessage() {} + +func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[206] + 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 EditDraftChunkResponse.ProtoReflect.Descriptor instead. +func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{206} +} + +// Reverse an approval (remove merged rule from active policy). +type UndoDraftChunkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // Chunk ID to undo. + ChunkId string `protobuf:"bytes,2,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UndoDraftChunkRequest) Reset() { + *x = UndoDraftChunkRequest{} + mi := &file_openshell_proto_msgTypes[207] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UndoDraftChunkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UndoDraftChunkRequest) ProtoMessage() {} + +func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[207] + 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 UndoDraftChunkRequest.ProtoReflect.Descriptor instead. +func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{207} +} + +func (x *UndoDraftChunkRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *UndoDraftChunkRequest) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +func (x *UndoDraftChunkRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *UndoDraftChunkRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type UndoDraftChunkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // New policy version after removal. + PolicyVersion uint32 `protobuf:"varint,1,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"` + // SHA-256 hash of the updated policy. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UndoDraftChunkResponse) Reset() { + *x = UndoDraftChunkResponse{} + mi := &file_openshell_proto_msgTypes[208] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UndoDraftChunkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UndoDraftChunkResponse) ProtoMessage() {} + +func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[208] + 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 UndoDraftChunkResponse.ProtoReflect.Descriptor instead. +func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{208} +} + +func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { + if x != nil { + return x.PolicyVersion + } + return 0 +} + +func (x *UndoDraftChunkResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +// Clear all pending draft chunks for a sandbox. +type ClearDraftChunksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearDraftChunksRequest) Reset() { + *x = ClearDraftChunksRequest{} + mi := &file_openshell_proto_msgTypes[209] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearDraftChunksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearDraftChunksRequest) ProtoMessage() {} + +func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[209] + 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 ClearDraftChunksRequest.ProtoReflect.Descriptor instead. +func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{209} +} + +func (x *ClearDraftChunksRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ClearDraftChunksRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *ClearDraftChunksRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ClearDraftChunksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Number of chunks cleared. + ChunksCleared uint32 `protobuf:"varint,1,opt,name=chunks_cleared,json=chunksCleared,proto3" json:"chunks_cleared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearDraftChunksResponse) Reset() { + *x = ClearDraftChunksResponse{} + mi := &file_openshell_proto_msgTypes[210] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearDraftChunksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearDraftChunksResponse) ProtoMessage() {} + +func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[210] + 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 ClearDraftChunksResponse.ProtoReflect.Descriptor instead. +func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{210} +} + +func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { + if x != nil { + return x.ChunksCleared + } + return 0 +} + +// Get decision history for a sandbox's draft policy. +type GetDraftHistoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,2,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDraftHistoryRequest) Reset() { + *x = GetDraftHistoryRequest{} + mi := &file_openshell_proto_msgTypes[211] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDraftHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDraftHistoryRequest) ProtoMessage() {} + +func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[211] + 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 GetDraftHistoryRequest.ProtoReflect.Descriptor instead. +func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{211} +} + +func (x *GetDraftHistoryRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetDraftHistoryRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +type DraftHistoryEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Time when the event occurred. + EventTime *timestamppb.Timestamp `protobuf:"bytes,101,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` + // Event type: "denial_detected", "analysis_cycle", "approved", + // "rejected", "edited", "undone", "cleared". + EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"` + // Human-readable description. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Associated chunk ID (if applicable). + ChunkId string `protobuf:"bytes,4,opt,name=chunk_id,json=chunkId,proto3" json:"chunk_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DraftHistoryEntry) Reset() { + *x = DraftHistoryEntry{} + mi := &file_openshell_proto_msgTypes[212] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DraftHistoryEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DraftHistoryEntry) ProtoMessage() {} + +func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[212] + 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 DraftHistoryEntry.ProtoReflect.Descriptor instead. +func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{212} +} + +func (x *DraftHistoryEntry) GetEventTime() *timestamppb.Timestamp { + if x != nil { + return x.EventTime + } + return nil +} + +func (x *DraftHistoryEntry) GetEventType() string { + if x != nil { + return x.EventType + } + return "" +} + +func (x *DraftHistoryEntry) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *DraftHistoryEntry) GetChunkId() string { + if x != nil { + return x.ChunkId + } + return "" +} + +type GetDraftHistoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Chronological decision history. + Entries []*DraftHistoryEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDraftHistoryResponse) Reset() { + *x = GetDraftHistoryResponse{} + mi := &file_openshell_proto_msgTypes[213] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDraftHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDraftHistoryResponse) ProtoMessage() {} + +func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[213] + 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 GetDraftHistoryResponse.ProtoReflect.Descriptor instead. +func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{213} +} + +func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { + if x != nil { + return x.Entries + } + return nil +} + +// Create workspace request. +type CreateWorkspaceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace name. Must be a valid DNS-1123 label. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional labels for the workspace (key-value metadata). + Labels map[string]string `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateWorkspaceRequest) Reset() { + *x = CreateWorkspaceRequest{} + mi := &file_openshell_proto_msgTypes[214] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateWorkspaceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkspaceRequest) ProtoMessage() {} + +func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[214] + 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 CreateWorkspaceRequest.ProtoReflect.Descriptor instead. +func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{214} +} + +func (x *CreateWorkspaceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateWorkspaceRequest) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *CreateWorkspaceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Create workspace response. +type CreateWorkspaceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workspace *datamodelv1.Workspace `protobuf:"bytes,1,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateWorkspaceResponse) Reset() { + *x = CreateWorkspaceResponse{} + mi := &file_openshell_proto_msgTypes[215] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateWorkspaceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWorkspaceResponse) ProtoMessage() {} + +func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[215] + 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 CreateWorkspaceResponse.ProtoReflect.Descriptor instead. +func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{215} +} + +func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { + if x != nil { + return x.Workspace + } + return nil +} + +// Get workspace request. +type GetWorkspaceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkspaceRequest) Reset() { + *x = GetWorkspaceRequest{} + mi := &file_openshell_proto_msgTypes[216] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkspaceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkspaceRequest) ProtoMessage() {} + +func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[216] + 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 GetWorkspaceRequest.ProtoReflect.Descriptor instead. +func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{216} +} + +func (x *GetWorkspaceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Get workspace response. +type GetWorkspaceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workspace *datamodelv1.Workspace `protobuf:"bytes,1,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkspaceResponse) Reset() { + *x = GetWorkspaceResponse{} + mi := &file_openshell_proto_msgTypes[217] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkspaceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkspaceResponse) ProtoMessage() {} + +func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[217] + 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 GetWorkspaceResponse.ProtoReflect.Descriptor instead. +func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{217} +} + +func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { + if x != nil { + return x.Workspace + } + return nil +} + +// List workspaces request. +type ListWorkspacesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The maximum number of workspaces to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListWorkspaces response. All other request parameters + // except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkspacesRequest) Reset() { + *x = ListWorkspacesRequest{} + mi := &file_openshell_proto_msgTypes[218] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkspacesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkspacesRequest) ProtoMessage() {} + +func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[218] + 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 ListWorkspacesRequest.ProtoReflect.Descriptor instead. +func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{218} +} + +func (x *ListWorkspacesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListWorkspacesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListWorkspacesRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +// List workspaces response. +type ListWorkspacesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workspaces []*datamodelv1.Workspace `protobuf:"bytes,1,rep,name=workspaces,proto3" json:"workspaces,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkspacesResponse) Reset() { + *x = ListWorkspacesResponse{} + mi := &file_openshell_proto_msgTypes[219] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkspacesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkspacesResponse) ProtoMessage() {} + +func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[219] + 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 ListWorkspacesResponse.ProtoReflect.Descriptor instead. +func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{219} +} + +func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { + if x != nil { + return x.Workspaces + } + return nil +} + +func (x *ListWorkspacesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Delete workspace request. +type DeleteWorkspaceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + AllowMissing bool `protobuf:"varint,2,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkspaceRequest) Reset() { + *x = DeleteWorkspaceRequest{} + mi := &file_openshell_proto_msgTypes[220] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkspaceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkspaceRequest) ProtoMessage() {} + +func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[220] + 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 DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. +func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{220} +} + +func (x *DeleteWorkspaceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteWorkspaceRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *DeleteWorkspaceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Delete workspace response. +type DeleteWorkspaceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWorkspaceResponse) Reset() { + *x = DeleteWorkspaceResponse{} + mi := &file_openshell_proto_msgTypes[221] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWorkspaceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWorkspaceResponse) ProtoMessage() {} + +func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[221] + 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 DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. +func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{221} +} + +func (x *DeleteWorkspaceResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// Workspace membership record. +type WorkspaceMember struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // OIDC subject claim identifying the principal. + PrincipalSubject string `protobuf:"bytes,2,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + // Role assigned to the principal within the workspace. + Role WorkspaceRole `protobuf:"varint,3,opt,name=role,proto3,enum=openshell.v1.WorkspaceRole" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceMember) Reset() { + *x = WorkspaceMember{} + mi := &file_openshell_proto_msgTypes[222] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMember) ProtoMessage() {} + +func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[222] + 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 WorkspaceMember.ProtoReflect.Descriptor instead. +func (*WorkspaceMember) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{222} +} + +func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *WorkspaceMember) GetPrincipalSubject() string { + if x != nil { + return x.PrincipalSubject + } + return "" +} + +func (x *WorkspaceMember) GetRole() WorkspaceRole { + if x != nil { + return x.Role + } + return WorkspaceRole_WORKSPACE_ROLE_UNSPECIFIED +} + +// Add workspace member request. +type AddWorkspaceMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,1,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // OIDC subject claim identifying the principal. + PrincipalSubject string `protobuf:"bytes,2,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + // Role to assign. + Role WorkspaceRole `protobuf:"varint,3,opt,name=role,proto3,enum=openshell.v1.WorkspaceRole" json:"role,omitempty"` + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddWorkspaceMemberRequest) Reset() { + *x = AddWorkspaceMemberRequest{} + mi := &file_openshell_proto_msgTypes[223] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddWorkspaceMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddWorkspaceMemberRequest) ProtoMessage() {} + +func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[223] + 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 AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. +func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{223} +} + +func (x *AddWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *AddWorkspaceMemberRequest) GetPrincipalSubject() string { + if x != nil { + return x.PrincipalSubject + } + return "" +} + +func (x *AddWorkspaceMemberRequest) GetRole() WorkspaceRole { + if x != nil { + return x.Role + } + return WorkspaceRole_WORKSPACE_ROLE_UNSPECIFIED +} + +func (x *AddWorkspaceMemberRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Add workspace member response. +type AddWorkspaceMemberResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Member *WorkspaceMember `protobuf:"bytes,1,opt,name=member,proto3" json:"member,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddWorkspaceMemberResponse) Reset() { + *x = AddWorkspaceMemberResponse{} + mi := &file_openshell_proto_msgTypes[224] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddWorkspaceMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddWorkspaceMemberResponse) ProtoMessage() {} + +func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[224] + 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 AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. +func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{224} +} + +func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { + if x != nil { + return x.Member + } + return nil +} + +// Remove workspace member request. +type RemoveWorkspaceMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,1,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // OIDC subject claim identifying the principal to remove. + PrincipalSubject string `protobuf:"bytes,2,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveWorkspaceMemberRequest) Reset() { + *x = RemoveWorkspaceMemberRequest{} + mi := &file_openshell_proto_msgTypes[225] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveWorkspaceMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} + +func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[225] + 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 RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. +func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{225} +} + +func (x *RemoveWorkspaceMemberRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *RemoveWorkspaceMemberRequest) GetPrincipalSubject() string { + if x != nil { + return x.PrincipalSubject + } + return "" +} + +func (x *RemoveWorkspaceMemberRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +func (x *RemoveWorkspaceMemberRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +// Remove workspace member response. +type RemoveWorkspaceMemberResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outcome DeletionOutcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=openshell.v1.DeletionOutcome" json:"outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveWorkspaceMemberResponse) Reset() { + *x = RemoveWorkspaceMemberResponse{} + mi := &file_openshell_proto_msgTypes[226] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveWorkspaceMemberResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} + +func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[226] + 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 RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. +func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{226} +} + +func (x *RemoveWorkspaceMemberResponse) GetOutcome() DeletionOutcome { + if x != nil { + return x.Outcome + } + return DeletionOutcome_DELETION_OUTCOME_UNSPECIFIED +} + +// List workspace members request. +type ListWorkspaceMembersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Only a named workspace selection is accepted. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,1,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + // The maximum number of members to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Token from a previous ListWorkspaceMembers response. All other request + // parameters except page_size must match the request that produced it. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkspaceMembersRequest) Reset() { + *x = ListWorkspaceMembersRequest{} + mi := &file_openshell_proto_msgTypes[227] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkspaceMembersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkspaceMembersRequest) ProtoMessage() {} + +func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[227] + 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 ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. +func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{227} +} + +func (x *ListWorkspaceMembersRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *ListWorkspaceMembersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListWorkspaceMembersRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// List workspace members response. +type ListWorkspaceMembersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Members []*WorkspaceMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` + // Token for the next page. Empty when there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWorkspaceMembersResponse) Reset() { + *x = ListWorkspaceMembersResponse{} + mi := &file_openshell_proto_msgTypes[228] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWorkspaceMembersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWorkspaceMembersResponse) ProtoMessage() {} + +func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[228] + 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 ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. +func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{228} +} + +func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { + if x != nil { + return x.Members + } + return nil +} + +func (x *ListWorkspaceMembersResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Short-lived credential for one policy-authorized extension service. +// Kept at the end of the file so adding it does not renumber existing +// generated message descriptors. +type ExtensionServiceCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Operator registration name used to correlate the credential with the + // stable service registration delivered by GetSandboxConfig. + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // Gateway-minted JWT with an audience derived from the registration. + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + // Absolute expiry of the token. + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExtensionServiceCredential) Reset() { + *x = ExtensionServiceCredential{} + mi := &file_openshell_proto_msgTypes[229] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExtensionServiceCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExtensionServiceCredential) ProtoMessage() {} + +func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[229] + 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 ExtensionServiceCredential.ProtoReflect.Descriptor instead. +func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{229} +} + +func (x *ExtensionServiceCredential) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *ExtensionServiceCredential) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *ExtensionServiceCredential) GetExpirationTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +// One redacted endpoint result in a supervisor's complete status report. +type EndpointObservation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable identifier derived from the configured host, ports, and path. + EndpointId string `protobuf:"bytes,1,opt,name=endpoint_id,json=endpointId,proto3" json:"endpoint_id,omitempty"` + // Latest result under the reported configuration and supervisor session. + Result EndpointResult `protobuf:"varint,2,opt,name=result,proto3,enum=openshell.v1.EndpointResult" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndpointObservation) Reset() { + *x = EndpointObservation{} + mi := &file_openshell_proto_msgTypes[230] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndpointObservation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndpointObservation) ProtoMessage() {} + +func (x *EndpointObservation) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[230] + 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 EndpointObservation.ProtoReflect.Descriptor instead. +func (*EndpointObservation) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{230} +} + +func (x *EndpointObservation) GetEndpointId() string { + if x != nil { + return x.EndpointId + } + return "" +} + +func (x *EndpointObservation) GetResult() EndpointResult { + if x != nil { + return x.Result + } + return EndpointResult_ENDPOINT_RESULT_UNSPECIFIED +} + +// Complete endpoint status report for the caller's current configuration. +type ReportEndpointStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. Must match the authenticated sandbox principal. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Hash from the active effective policy delivered by the gateway. + PolicyHash string `protobuf:"bytes,2,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Provider environment revision delivered with the active configuration. + ProviderEnvRevision uint64 `protobuf:"varint,3,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + // Exactly one result for every distinct observed endpoint in the policy. + Observations []*EndpointObservation `protobuf:"bytes,4,rep,name=observations,proto3" json:"observations,omitempty"` + // Endpoints with a new observation in this batch. Omitted endpoints retain + // their prior report time; an identical retry never advances report time. + ObservedEndpointIds []string `protobuf:"bytes,5,rep,name=observed_endpoint_ids,json=observedEndpointIds,proto3" json:"observed_endpoint_ids,omitempty"` + // Active ConnectSupervisor session that owns these observations. + SupervisorSessionId string `protobuf:"bytes,6,opt,name=supervisor_session_id,json=supervisorSessionId,proto3" json:"supervisor_session_id,omitempty"` + // Monotonically increasing sequence within the authenticated session. Gaps + // are allowed when an inventory reset supersedes a frozen snapshot. Retrying + // a report preserves its complete body and sequence for idempotent acknowledgement. + ReportSequence uint64 `protobuf:"varint,7,opt,name=report_sequence,json=reportSequence,proto3" json:"report_sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportEndpointStatusRequest) Reset() { + *x = ReportEndpointStatusRequest{} + mi := &file_openshell_proto_msgTypes[231] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportEndpointStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportEndpointStatusRequest) ProtoMessage() {} + +func (x *ReportEndpointStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[231] + 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 ReportEndpointStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportEndpointStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{231} +} + +func (x *ReportEndpointStatusRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ReportEndpointStatusRequest) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *ReportEndpointStatusRequest) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *ReportEndpointStatusRequest) GetObservations() []*EndpointObservation { + if x != nil { + return x.Observations + } + return nil +} + +func (x *ReportEndpointStatusRequest) GetObservedEndpointIds() []string { + if x != nil { + return x.ObservedEndpointIds + } + return nil +} + +func (x *ReportEndpointStatusRequest) GetSupervisorSessionId() string { + if x != nil { + return x.SupervisorSessionId + } + return "" +} + +func (x *ReportEndpointStatusRequest) GetReportSequence() uint64 { + if x != nil { + return x.ReportSequence + } + return 0 +} + +// Empty acknowledgement for a persisted endpoint status report. +type ReportEndpointStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportEndpointStatusResponse) Reset() { + *x = ReportEndpointStatusResponse{} + mi := &file_openshell_proto_msgTypes[232] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportEndpointStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportEndpointStatusResponse) ProtoMessage() {} + +func (x *ReportEndpointStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[232] + 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 ReportEndpointStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportEndpointStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{232} +} + +// A configured endpoint and its last accepted network result in one record. +// Address fields contain policy selectors, never request URLs or credentials. +type EndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable identifier for selecting this endpoint without parsing display text. + EndpointId string `protobuf:"bytes,1,opt,name=endpoint_id,json=endpointId,proto3" json:"endpoint_id,omitempty"` + // Lowercase configured endpoint host. + Host string `protobuf:"bytes,2,opt,name=host,proto3" json:"host,omitempty"` + // Sorted, deduplicated effective endpoint ports. Validated endpoints have at + // least one port. + Ports []uint32 `protobuf:"varint,3,rep,packed,name=ports,proto3" json:"ports,omitempty"` + // Canonical configured path selector; an unrestricted path is /**. + Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` + // Last accepted result, aggregated across configured callers and ports. + // NoObservedExchange retains the address and has no report timestamp. + LastResult EndpointResult `protobuf:"varint,5,opt,name=last_result,json=lastResult,proto3,enum=openshell.v1.EndpointResult" json:"last_result,omitempty"` + // Time when the gateway accepted the observation. This is not the request + // time: still-valid evidence can be reaccepted after a reset. Identical + // same-sequence retries do not advance it. Absent until a result is reported. + LastReportedTime *timestamppb.Timestamp `protobuf:"bytes,106,opt,name=last_reported_time,json=lastReportedTime,proto3" json:"last_reported_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndpointStatus) Reset() { + *x = EndpointStatus{} + mi := &file_openshell_proto_msgTypes[233] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndpointStatus) ProtoMessage() {} + +func (x *EndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[233] + 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 EndpointStatus.ProtoReflect.Descriptor instead. +func (*EndpointStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{233} +} + +func (x *EndpointStatus) GetEndpointId() string { + if x != nil { + return x.EndpointId + } + return "" +} + +func (x *EndpointStatus) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *EndpointStatus) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *EndpointStatus) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *EndpointStatus) GetLastResult() EndpointResult { + if x != nil { + return x.LastResult + } + return EndpointResult_ENDPOINT_RESULT_UNSPECIFIED +} + +func (x *EndpointStatus) GetLastReportedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastReportedTime + } + return nil +} + +// Durable provisioning attempt, independent of supervisor registration and polling. +type SandboxProvisioning struct { + state protoimpl.MessageState `protogen:"open.v1"` + AttemptId string `protobuf:"bytes,1,opt,name=attempt_id,json=attemptId,proto3" json:"attempt_id,omitempty"` + ConfigurationChangeId string `protobuf:"bytes,2,opt,name=configuration_change_id,json=configurationChangeId,proto3" json:"configuration_change_id,omitempty"` + ConfigurationChangeTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=configuration_change_time,json=configurationChangeTime,proto3" json:"configuration_change_time,omitempty"` + FirstRejectionTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=first_rejection_time,json=firstRejectionTime,proto3" json:"first_rejection_time,omitempty"` + // Active preparation or admission-repair deadline. Absent after Ready/timeout. + Deadline *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=deadline,proto3" json:"deadline,omitempty"` + TimeoutTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=timeout_time,json=timeoutTime,proto3" json:"timeout_time,omitempty"` + // Set only after both supervisor and workload compute have been reclaimed. + CleanupCompletedTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=cleanup_completed_time,json=cleanupCompletedTime,proto3" json:"cleanup_completed_time,omitempty"` + // A safe gateway-authored diagnostic; never a raw driver error. + CleanupError string `protobuf:"bytes,8,opt,name=cleanup_error,json=cleanupError,proto3" json:"cleanup_error,omitempty"` + // Durable backoff for interrupted or failed reclamation. + CleanupRetryTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=cleanup_retry_time,json=cleanupRetryTime,proto3" json:"cleanup_retry_time,omitempty"` + // Attachment edits have their own durable clock; status writes do not change it. + AttachmentChangeId string `protobuf:"bytes,10,opt,name=attachment_change_id,json=attachmentChangeId,proto3" json:"attachment_change_id,omitempty"` + AttachmentChangeTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=attachment_change_time,json=attachmentChangeTime,proto3" json:"attachment_change_time,omitempty"` + // Absolute ceiling for image preparation and initial supervisor startup. + // Set once per new attempt; progress, configuration writes, and restarts + // cannot extend it. Absent on attempts created before preparation timing. + PreparationDeadline *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=preparation_deadline,json=preparationDeadline,proto3" json:"preparation_deadline,omitempty"` + // First authenticated supervisor configuration report for this attempt. + // Starts the admission repair window. Absent while preparing, including + // after a preparation timeout. Duplicate reports never change this value. + AdmissionStartTime *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=admission_start_time,json=admissionStartTime,proto3" json:"admission_start_time,omitempty"` + // An owned driver create/start future is still pending. Claimed before + // dispatch and cleared on its actual success or error response. Monitor + // interruption, owner loss, and StopSandbox/NotFound do not clear it. + // While set, cleanup cannot complete and retry/deletion remain blocked. + // This does not strengthen a driver's transport-error settlement contract. + DriverOperationPending bool `protobuf:"varint,14,opt,name=driver_operation_pending,json=driverOperationPending,proto3" json:"driver_operation_pending,omitempty"` + // Unique for each claimed driver operation, including recovery and automatic + // restart. Retained after settlement to reject delayed callbacks from an + // earlier operation on the same attempt and runtime generation. + DriverOperationId string `protobuf:"bytes,15,opt,name=driver_operation_id,json=driverOperationId,proto3" json:"driver_operation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxProvisioning) Reset() { + *x = SandboxProvisioning{} + mi := &file_openshell_proto_msgTypes[234] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxProvisioning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxProvisioning) ProtoMessage() {} + +func (x *SandboxProvisioning) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[234] + 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 SandboxProvisioning.ProtoReflect.Descriptor instead. +func (*SandboxProvisioning) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{234} +} + +func (x *SandboxProvisioning) GetAttemptId() string { + if x != nil { + return x.AttemptId + } + return "" +} + +func (x *SandboxProvisioning) GetConfigurationChangeId() string { + if x != nil { + return x.ConfigurationChangeId + } + return "" +} + +func (x *SandboxProvisioning) GetConfigurationChangeTime() *timestamppb.Timestamp { + if x != nil { + return x.ConfigurationChangeTime + } + return nil +} + +func (x *SandboxProvisioning) GetFirstRejectionTime() *timestamppb.Timestamp { + if x != nil { + return x.FirstRejectionTime + } + return nil +} + +func (x *SandboxProvisioning) GetDeadline() *timestamppb.Timestamp { + if x != nil { + return x.Deadline + } + return nil +} + +func (x *SandboxProvisioning) GetTimeoutTime() *timestamppb.Timestamp { + if x != nil { + return x.TimeoutTime + } + return nil +} + +func (x *SandboxProvisioning) GetCleanupCompletedTime() *timestamppb.Timestamp { + if x != nil { + return x.CleanupCompletedTime + } + return nil +} + +func (x *SandboxProvisioning) GetCleanupError() string { + if x != nil { + return x.CleanupError + } + return "" +} + +func (x *SandboxProvisioning) GetCleanupRetryTime() *timestamppb.Timestamp { + if x != nil { + return x.CleanupRetryTime + } + return nil +} + +func (x *SandboxProvisioning) GetAttachmentChangeId() string { + if x != nil { + return x.AttachmentChangeId + } + return "" +} + +func (x *SandboxProvisioning) GetAttachmentChangeTime() *timestamppb.Timestamp { + if x != nil { + return x.AttachmentChangeTime + } + return nil +} + +func (x *SandboxProvisioning) GetPreparationDeadline() *timestamppb.Timestamp { + if x != nil { + return x.PreparationDeadline + } + return nil +} + +func (x *SandboxProvisioning) GetAdmissionStartTime() *timestamppb.Timestamp { + if x != nil { + return x.AdmissionStartTime + } + return nil +} + +func (x *SandboxProvisioning) GetDriverOperationPending() bool { + if x != nil { + return x.DriverOperationPending + } + return false +} + +func (x *SandboxProvisioning) GetDriverOperationId() string { + if x != nil { + return x.DriverOperationId + } + return "" +} + +// Create-time request to expose one loopback HTTP service in a sandbox. +type SandboxServiceExposure struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,2,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Application authorization behavior. Omission resolves to STRIP. + AuthorizationMode ServiceAuthorizationMode `protobuf:"varint,3,opt,name=authorization_mode,json=authorizationMode,proto3,enum=openshell.v1.ServiceAuthorizationMode" json:"authorization_mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxServiceExposure) Reset() { + *x = SandboxServiceExposure{} + mi := &file_openshell_proto_msgTypes[235] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxServiceExposure) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxServiceExposure) ProtoMessage() {} + +func (x *SandboxServiceExposure) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[235] + 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 SandboxServiceExposure.ProtoReflect.Descriptor instead. +func (*SandboxServiceExposure) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{235} +} + +func (x *SandboxServiceExposure) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *SandboxServiceExposure) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + +func (x *SandboxServiceExposure) GetAuthorizationMode() ServiceAuthorizationMode { + if x != nil { + return x.AuthorizationMode + } + return ServiceAuthorizationMode_SERVICE_AUTHORIZATION_MODE_UNSPECIFIED +} + +var File_openshell_proto protoreflect.FileDescriptor + +const file_openshell_proto_rawDesc = "" + + "\n" + + "\x0fopenshell.proto\x12\fopenshell.v1\x1a\x0fdatamodel.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\roptions.proto\x1a\rsandbox.proto\"\x1a\n" + + "\x18IssueSandboxTokenRequest\"\x91\x01\n" + + "\x19IssueSandboxTokenResponse\x12\x1a\n" + + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x02\x10\x03R\rexpires_at_ms\"T\n" + + "\x1aRefreshSandboxTokenRequest\x126\n" + + "\x17extension_service_names\x18\x01 \x03(\tR\x15extensionServiceNames\"\xd8\x03\n" + + "\x1bRefreshSandboxTokenResponse\x12\x1a\n" + + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12]\n" + + "\x15extension_credentials\x18\x03 \x03(\v2(.openshell.v1.ExtensionServiceCredentialR\x14extensionCredentials\x12)\n" + + "\rsandbox_token\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\fsandboxToken\x12R\n" + + "\x17sandbox_expiration_time\x18i \x01(\v2\x1a.google.protobuf.TimestampR\x15sandboxExpirationTime\x12\x1d\n" + + "\n" + + "session_id\x18\x06 \x01(\tR\tsessionId\x12)\n" + + "\x10credential_epoch\x18\a \x01(\x04R\x0fcredentialEpochJ\x04\b\x02\x10\x03J\x04\b\x05\x10\x06R\rexpires_at_msR\x15sandbox_expires_at_ms\"\x0f\n" + + "\rHealthRequest\"_\n" + + "\x0eHealthResponse\x123\n" + + "\x06status\x18\x01 \x01(\x0e2\x1b.openshell.v1.ServiceStatusR\x06status\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\"\x17\n" + + "\x15GetCurrentUserRequest\"\xb0\x01\n" + + "\x16GetCurrentUserResponse\x12\x18\n" + + "\asubject\x18\x01 \x01(\tR\asubject\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" + + "\x05roles\x18\x03 \x03(\tR\x05roles\x12\x16\n" + + "\x06scopes\x18\x04 \x03(\tR\x06scopes\x12+\n" + + "\x11identity_provider\x18\x05 \x01(\tR\x10identityProvider\"\x17\n" + + "\x15GetGatewayInfoRequest\"\x87\x02\n" + + "\x16GetGatewayInfoResponse\x123\n" + + "\x06status\x18\x01 \x01(\x0e2\x1b.openshell.v1.ServiceStatusR\x06status\x12'\n" + + "\x0fgateway_version\x18\x02 \x01(\tR\x0egatewayVersion\x12H\n" + + "\x0fcompute_drivers\x18\x03 \x03(\v2\x1f.openshell.v1.ComputeDriverInfoR\x0ecomputeDrivers\x12E\n" + + "\n" + + "extensions\x18\x04 \x03(\v2%.openshell.v1.NegotiatedExtensionInfoR\n" + + "extensions\"\x95\x03\n" + + "\x17NegotiatedExtensionInfo\x12/\n" + + "\x04kind\x18\x01 \x01(\x0e2\x1b.openshell.v1.ExtensionKindR\x04kind\x12'\n" + + "\x0fconfigured_name\x18\x02 \x01(\tR\x0econfiguredName\x12/\n" + + "\x13implementation_name\x18\x03 \x01(\tR\x12implementationName\x125\n" + + "\x16implementation_version\x18\x04 \x01(\tR\x15implementationVersion\x12%\n" + + "\x0eprotocol_major\x18\x05 \x01(\rR\rprotocolMajor\x12%\n" + + "\x0eprotocol_minor\x18\x06 \x01(\rR\rprotocolMinor\x125\n" + + "\x16supported_capabilities\x18\a \x03(\tR\x15supportedCapabilities\x123\n" + + "\x15required_capabilities\x18\b \x03(\tR\x14requiredCapabilities\"t\n" + + "\x11ComputeDriverInfo\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12K\n" + + "\fcapabilities\x18\x02 \x01(\v2'.openshell.v1.ComputeDriverCapabilitiesR\fcapabilities\"\xbc\x01\n" + + "\x19ComputeDriverCapabilities\x12\x1f\n" + + "\vdriver_name\x18\x01 \x01(\tR\n" + + "driverName\x12%\n" + + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\x12W\n" + + "\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\"\xca\x01\n" + + "\x14ResourceCapabilities\x127\n" + + "\x03cpu\x18\x01 \x01(\v2%.openshell.v1.CpuResourceCapabilitiesR\x03cpu\x12@\n" + + "\x06memory\x18\x02 \x01(\v2(.openshell.v1.MemoryResourceCapabilitiesR\x06memory\x127\n" + + "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceCapabilitiesR\x03gpu\"B\n" + + "\x17CpuResourceCapabilities\x12'\n" + + "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\"E\n" + + "\x1aMemoryResourceCapabilities\x12'\n" + + "\x0flimit_supported\x18\x01 \x01(\bR\x0elimitSupported\"\x95\x01\n" + + "\x17GpuResourceCapabilities\x12>\n" + + "\x1bdefault_selection_supported\x18\x01 \x01(\bR\x19defaultSelectionSupported\x12:\n" + + "\x19count_selection_supported\x18\x02 \x01(\bR\x17countSelectionSupported\"\x80\x03\n" + + "\aSandbox\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12-\n" + + "\x04spec\x18\x02 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x123\n" + + "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06status\x120\n" + + "\x14host_key_fingerprint\x18\x06 \x01(\tR\x12hostKeyFingerprint\x12t\n" + + "\x1ecreated_from_workload_template\x18\x14 \x01(\v2/.openshell.v1.SandboxWorkloadTemplateProvenanceR\x1bcreatedFromWorkloadTemplateJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\x8a\x05\n" + + "\vSandboxSpec\x12\x1b\n" + + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12L\n" + + "\venvironment\x18\x05 \x03(\v2*.openshell.v1.SandboxSpec.EnvironmentEntryR\venvironment\x129\n" + + "\btemplate\x18\x06 \x01(\v2\x1d.openshell.v1.SandboxTemplateR\btemplate\x12;\n" + + "\x06policy\x18\a \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1c\n" + + "\tproviders\x18\b \x03(\tR\tproviders\x12W\n" + + "\x15resource_requirements\x18\t \x01(\v2\".openshell.v1.ResourceRequirementsR\x14resourceRequirements\x12\x18\n" + + "\acommand\x18\f \x03(\tR\acommand\x12\x10\n" + + "\x03tty\x18\r \x01(\bR\x03tty\x12:\n" + + "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x12I\n" + + "\x0erestart_policy\x18\x0f \x01(\x0e2\".openshell.v1.SandboxRestartPolicyR\rrestartPolicy\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\n" + + "\x10\vJ\x04\b\v\x10\fR\n" + + "gpu_deviceR\x16proposal_approval_mode\"O\n" + + "\x14ResourceRequirements\x127\n" + + "\x03gpu\x18\x01 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\">\n" + + "\x17GpuResourceRequirements\x12\x19\n" + + "\x05count\x18\x01 \x01(\rH\x00R\x05count\x88\x01\x01B\b\n" + + "\x06_count\"\xef\x05\n" + + "\x0fSandboxTemplate\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12,\n" + + "\x12runtime_class_name\x18\x02 \x01(\tR\x10runtimeClassName\x12!\n" + + "\fagent_socket\x18\x03 \x01(\tR\vagentSocket\x12A\n" + + "\x06labels\x18\x04 \x03(\v2).openshell.v1.SandboxTemplate.LabelsEntryR\x06labels\x12P\n" + + "\vannotations\x18\x05 \x03(\v2..openshell.v1.SandboxTemplate.AnnotationsEntryR\vannotations\x12P\n" + + "\venvironment\x18\x06 \x03(\v2..openshell.v1.SandboxTemplate.EnvironmentEntryR\venvironment\x125\n" + + "\tresources\x18\a \x01(\v2\x17.google.protobuf.StructR\tresources\x12,\n" + + "\x0fuser_namespaces\x18\n" + + " \x01(\bH\x00R\x0euserNamespaces\x88\x01\x01\x12<\n" + + "\rdriver_config\x18\v \x01(\v2\x17.google.protobuf.StructR\fdriverConfig\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x12\n" + + "\x10_user_namespacesJ\x04\b\t\x10\n" + + "R\x16volume_claim_templates\"\x98\x01\n" + + "\x17SandboxWorkloadTemplate\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12=\n" + + "\x04spec\x18\x02 \x01(\v2).openshell.v1.SandboxWorkloadTemplateSpecR\x04spec\"\xf3\x01\n" + + "\x1bSandboxWorkloadTemplateSpec\x12?\n" + + "\bworkload\x18\x01 \x01(\v2#.openshell.v1.SandboxWorkloadConfigR\bworkload\x12<\n" + + "\rdriver_config\x18\x02 \x01(\v2\x17.google.protobuf.StructR\fdriverConfig\x12U\n" + + "\x15desired_service_level\x18\x03 \x01(\v2!.openshell.v1.SandboxServiceLevelR\x13desiredServiceLevel\"\x83\x02\n" + + "\x15SandboxWorkloadConfig\x12\x14\n" + + "\x05image\x18\x01 \x01(\tR\x05image\x12V\n" + + "\venvironment\x18\x02 \x03(\v24.openshell.v1.SandboxWorkloadConfig.EnvironmentEntryR\venvironment\x12<\n" + + "\tresources\x18\x03 \x01(\v2\x1e.openshell.v1.SandboxResourcesR\tresources\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"u\n" + + "\x10SandboxResources\x12\x10\n" + + "\x03cpu\x18\x01 \x01(\tR\x03cpu\x12\x16\n" + + "\x06memory\x18\x02 \x01(\tR\x06memory\x127\n" + + "\x03gpu\x18\x03 \x01(\v2%.openshell.v1.GpuResourceRequirementsR\x03gpu\"M\n" + + "\x13SandboxServiceLevel\x126\n" + + "\astartup\x18\x01 \x01(\v2\x1c.openshell.v1.SandboxStartupR\astartup\"k\n" + + "\x0eSandboxStartup\x12<\n" + + "\fready_within\x18\x01 \x01(\v2\x19.google.protobuf.DurationR\vreadyWithin\x12\x1b\n" + + "\tmax_burst\x18\x02 \x01(\rR\bmaxBurst\"b\n" + + "!SandboxWorkloadTemplateProvenance\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12)\n" + + "\x10resource_version\x18\x02 \x01(\tR\x0fresourceVersion\"\x8d\a\n" + + "\rSandboxStatus\x12\x1b\n" + + "\tagent_pod\x18\x02 \x01(\tR\bagentPod\x12\x19\n" + + "\bagent_fd\x18\x03 \x01(\tR\aagentFd\x12\x1d\n" + + "\n" + + "sandbox_fd\x18\x04 \x01(\tR\tsandboxFd\x12>\n" + + "\n" + + "conditions\x18\x05 \x03(\v2\x1e.openshell.v1.SandboxConditionR\n" + + "conditions\x120\n" + + "\x05phase\x18\x06 \x01(\x0e2\x1a.openshell.v1.SandboxPhaseR\x05phase\x124\n" + + "\x16current_policy_version\x18\a \x01(\rR\x14currentPolicyVersion\x127\n" + + "\x18main_process_instance_id\x18\b \x01(\tR\x15mainProcessInstanceId\x12 \n" + + "\texit_code\x18\t \x01(\x05H\x00R\bexitCode\x88\x01\x01\x12I\n" + + "\x11endpoint_statuses\x18\n" + + " \x03(\v2\x1c.openshell.v1.EndpointStatusR\x10endpointStatuses\x12d\n" + + "\x17configuration_admission\x18\v \x01(\v2+.openshell.v1.SandboxConfigurationAdmissionR\x16configurationAdmission\x12<\n" + + "\x17configuration_activated\x18\f \x01(\bH\x01R\x16configurationActivated\x88\x01\x01\x12E\n" + + "\fprovisioning\x18\r \x01(\v2!.openshell.v1.SandboxProvisioningR\fprovisioning\x12#\n" + + "\rrestart_count\x18\x0e \x01(\rR\frestartCount\x12F\n" + + "\x11next_restart_time\x18\x0f \x01(\v2\x1a.google.protobuf.TimestampR\x0fnextRestartTime\x12U\n" + + "\x19main_process_started_time\x18\x10 \x01(\v2\x1a.google.protobuf.TimestampR\x16mainProcessStartedTimeB\f\n" + + "\n" + + "_exit_codeB\x1a\n" + + "\x18_configuration_activated\"\xd1\x01\n" + + "\x10SandboxCondition\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12C\n" + + "\x0ftransition_time\x18i \x01(\v2\x1a.google.protobuf.TimestampR\x0etransitionTimeJ\x04\b\x05\x10\x06R\x14last_transition_time\"\xc0\x02\n" + + "\rPlatformEvent\x129\n" + + "\n" + + "event_time\x18e \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x16\n" + + "\x06reason\x18\x04 \x01(\tR\x06reason\x12\x18\n" + + "\amessage\x18\x05 \x01(\tR\amessage\x12E\n" + + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02R\ftimestamp_ms\"\xa9\x05\n" + + "\x14CreateSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\a \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12-\n" + + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + + "\x06labels\x18\x03 \x03(\v2..openshell.v1.CreateSandboxRequest.LabelsEntryR\x06labels\x12U\n" + + "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12A\n" + + "\x1dawait_main_process_attachment\x18\x05 \x01(\bR\x1aawaitMainProcessAttachment\x12+\n" + + "\x11workload_template\x18\x06 \x01(\tR\x10workloadTemplate\x12\x1d\n" + + "\n" + + "request_id\x18\b \x01(\tR\trequestId\x12Q\n" + + "\x11service_exposures\x18\t \x03(\v2$.openshell.v1.SandboxServiceExposureR\x10serviceExposures\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x01\n" + + "\x1cCreateSandboxTemplateRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12A\n" + + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\x83\x01\n" + + "\x19GetSandboxTemplateRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\xd4\x01\n" + + "\x1bListSandboxTemplatesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x12%\n" + + "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\"\xca\x01\n" + + "\x1cDeleteSandboxTemplateRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rallow_missing\x18\x03 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"\\\n" + + "\x17SandboxTemplateResponse\x12A\n" + + "\btemplate\x18\x01 \x01(\v2%.openshell.v1.SandboxWorkloadTemplateR\btemplate\"\x8b\x01\n" + + "\x1cListSandboxTemplatesResponse\x12C\n" + + "\ttemplates\x18\x01 \x03(\v2%.openshell.v1.SandboxWorkloadTemplateR\ttemplates\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"g\n" + + "\x1dDeleteSandboxTemplateResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\xae\x01\n" + + "\x1cBeginRootfsTarStagingRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tfile_name\x18\x01 \x01(\tR\bfileName\x12\x1d\n" + + "\n" + + "size_bytes\x18\x02 \x01(\x04R\tsizeBytes\"\xdc\x01\n" + + "\x1dBeginRootfsTarStagingResponse\x12#\n" + + "\rstaging_token\x18\x01 \x01(\tR\fstagingToken\x12\x1f\n" + + "\vupload_path\x18\x02 \x01(\tR\n" + + "uploadPath\x12\x1b\n" + + "\tmax_bytes\x18\x03 \x01(\x04R\bmaxBytes\x12C\n" + + "\x0fexpiration_time\x18h \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x04\x10\x05R\rexpires_at_ms\"{\n" + + "\x11GetSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\xcd\x01\n" + + "\x14ListSandboxesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x12%\n" + + "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\"\xc7\x01\n" + + "\x1bListSandboxProvidersRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1b\n" + + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x83\x02\n" + + "\x1cAttachSandboxProviderRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\x12:\n" + + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\x83\x02\n" + + "\x1cDetachSandboxProviderRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\x12:\n" + + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\xc2\x01\n" + + "\x14DeleteSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rallow_missing\x18\x03 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"\x9b\x01\n" + + "\x12StopSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\x9c\x01\n" + + "\x13StartSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\xd5\x01\n" + + "\x0fSandboxResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12Q\n" + + "\fservice_urls\x18\x02 \x03(\v2..openshell.v1.SandboxResponse.ServiceUrlsEntryR\vserviceUrls\x1a>\n" + + "\x10ServiceUrlsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" + + "\x15ListSandboxesResponse\x123\n" + + "\tsandboxes\x18\x01 \x03(\v2\x15.openshell.v1.SandboxR\tsandboxes\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x86\x01\n" + + "\x1cListSandboxProvidersResponse\x12>\n" + + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xad\x01\n" + + "\x1dAttachSandboxProviderResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + + "\battached\x18\x02 \x01(\bR\battached\x12?\n" + + "\areceipt\x18\x03 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\"\xad\x01\n" + + "\x1dDetachSandboxProviderResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + + "\bdetached\x18\x02 \x01(\bR\bdetached\x12?\n" + + "\areceipt\x18\x03 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\"\xd8\x02\n" + + "\x17ProviderDesiredIdentity\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x18\n" + + "\asandbox\x18\x02 \x01(\tR\asandbox\x12)\n" + + "\x10attachment_epoch\x18\x03 \x01(\tR\x0fattachmentEpoch\x12\x1f\n" + + "\vprovider_id\x18\x04 \x01(\tR\n" + + "providerId\x12:\n" + + "\x19provider_resource_version\x18\x05 \x01(\x04R\x17providerResourceVersion\x122\n" + + "\x15provider_env_revision\x18\x06 \x01(\x04R\x13providerEnvRevision\x12'\n" + + "\x0fconfig_revision\x18\a \x01(\x04R\x0econfigRevision\x12\x1f\n" + + "\vpolicy_hash\x18\b \x01(\tR\n" + + "policyHash\"\xfa\x01\n" + + "\x16ConfigSnapshotRevision\x12L\n" + + "\x0esandbox_config\x18\x01 \x01(\v2#.openshell.v1.SandboxConfigRevisionH\x00R\rsandboxConfig\x123\n" + + "\x14provider_environment\x18\x02 \x01(\x04H\x00R\x13providerEnvironment\x12P\n" + + "\x0fprovider_target\x18\x03 \x01(\v2%.openshell.v1.ProviderDesiredIdentityH\x00R\x0eproviderTargetB\v\n" + + "\tcomponent\"\x91\x02\n" + + "\x15SandboxConfigRevision\x12'\n" + + "\x0fconfig_revision\x18\x01 \x01(\x04R\x0econfigRevision\x12%\n" + + "\x0epolicy_version\x18\x02 \x01(\rR\rpolicyVersion\x12G\n" + + "\rpolicy_source\x18\x03 \x01(\x0e2\".openshell.sandbox.v1.PolicySourceR\fpolicySource\x122\n" + + "\x15global_policy_version\x18\x04 \x01(\rR\x13globalPolicyVersion\x12+\n" + + "\x11settings_revision\x18\x05 \x01(\x04R\x10settingsRevision\"\x8c\x05\n" + + "\x15ConfigUpdateOperation\x12!\n" + + "\foperation_id\x18\x01 \x01(\tR\voperationId\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12;\n" + + "\tcomponent\x18\x03 \x01(\x0e2\x1d.openshell.v1.ConfigComponentR\tcomponent\x12M\n" + + "\x0ftarget_revision\x18\x04 \x01(\v2$.openshell.v1.ConfigSnapshotRevisionR\x0etargetRevision\x12>\n" + + "\x05state\x18\x05 \x01(\x0e2(.openshell.v1.ConfigUpdateOperationStateR\x05state\x12:\n" + + "\aoutcome\x18\x06 \x01(\x0e2 .openshell.v1.ConfigApplyOutcomeR\aoutcome\x12'\n" + + "\x0fsanitized_error\x18\a \x01(\tR\x0esanitizedError\x12=\n" + + "\fcreated_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12=\n" + + "\fupdated_time\x18m \x01(\v2\x1a.google.protobuf.TimestampR\vupdatedTime\x12A\n" + + "\x0ecompleted_time\x18n \x01(\v2\x1a.google.protobuf.TimestampR\rcompletedTimeJ\x04\b\b\x10\tJ\x04\b\t\x10\n" + + "J\x04\b\n" + + "\x10\vR\rcreated_at_msR\rupdated_at_msR\x0fcompleted_at_ms\"\xe6\x02\n" + + "\x17ProviderMutationReceipt\x12\x1d\n" + + "\n" + + "receipt_id\x18\x01 \x01(\tR\treceiptId\x12\x1f\n" + + "\vmutation_id\x18\x02 \x01(\tR\n" + + "mutationId\x12\x1a\n" + + "\bprovider\x18\x03 \x01(\tR\bprovider\x12\x1c\n" + + "\tworkspace\x18\x04 \x01(\tR\tworkspace\x126\n" + + "\x04kind\x18\x05 \x01(\x0e2\".openshell.v1.ProviderMutationKindR\x04kind\x12?\n" + + "\adesired\x18\x06 \x01(\v2%.openshell.v1.ProviderDesiredIdentityR\adesired\x12A\n" + + "\x0epersisted_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\rpersistedTimeJ\x04\b\a\x10\bR\x0fpersisted_at_ms\"\x8d\x04\n" + + "\x1cProviderReadinessObservation\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1a\n" + + "\bsequence\x18\x02 \x01(\x04R\bsequence\x12)\n" + + "\x10attachment_epoch\x18\x03 \x01(\tR\x0fattachmentEpoch\x122\n" + + "\x15provider_env_revision\x18\x04 \x01(\x04R\x13providerEnvRevision\x12'\n" + + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x12\x1f\n" + + "\vpolicy_hash\x18\x06 \x01(\tR\n" + + "policyHash\x123\n" + + "\x15credentials_installed\x18\a \x01(\bR\x14credentialsInstalled\x12#\n" + + "\rpolicy_active\x18\b \x01(\bR\fpolicyActive\x12@\n" + + "\x1claunch_environment_installed\x18\t \x01(\bR\x1alaunchEnvironmentInstalled\x12.\n" + + "\x13process_instance_id\x18\n" + + " \x01(\tR\x11processInstanceId\x12=\n" + + "\x06reason\x18\v \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\"\xc1\x04\n" + + "\x17ProviderReadinessStatus\x12?\n" + + "\areceipt\x18\x01 \x01(\v2%.openshell.v1.ProviderMutationReceiptR\areceipt\x12:\n" + + "\x05state\x18\x02 \x01(\x0e2$.openshell.v1.ProviderReadinessStateR\x05state\x12=\n" + + "\x06reason\x18\x03 \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x06reason\x12F\n" + + "\bobserved\x18\x04 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\bobserved\x12.\n" + + "\x13network_instance_id\x18\x05 \x01(\tR\x11networkInstanceId\x12?\n" + + "\robserved_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\fobservedTime\x12A\n" + + "\x0eevaluated_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\revaluatedTime\x12A\n" + + "\toperation\x18\b \x01(\v2#.openshell.v1.ConfigUpdateOperationR\toperationJ\x04\b\x06\x10\aJ\x04\b\a\x10\bR\x0eobserved_at_msR\x0fevaluated_at_ms\"\xca\x01\n" + + "\x1fGetSandboxProviderStatusRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\x12\x1d\n" + + "\n" + + "receipt_id\x18\x03 \x01(\tR\treceiptId\"a\n" + + " GetSandboxProviderStatusResponse\x12=\n" + + "\x06status\x18\x01 \x01(\v2%.openshell.v1.ProviderReadinessStatusR\x06status\"\x8d\x01\n" + + "\x1eReportProviderReadinessRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12L\n" + + "\vobservation\x18\x02 \x01(\v2*.openshell.v1.ProviderReadinessObservationR\vobservation\"\x94\x02\n" + + "\x1fReportProviderReadinessResponse\x12+\n" + + "\x11accepted_sequence\x18\x01 \x01(\x04R\x10acceptedSequence\x12B\n" + + "\x0freport_interval\x18f \x01(\v2\x19.google.protobuf.DurationR\x0ereportInterval\x12B\n" + + "\x0fobservation_ttl\x18g \x01(\v2\x19.google.protobuf.DurationR\x0eobservationTtlJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04R\x17report_interval_secondsR\x17observation_ttl_seconds\"~\n" + + "\x15DeleteSandboxResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcome\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x03 \x01(\tR\tsandboxIdJ\x04\b\x01\x10\x02R\adeleted\"\x87\x01\n" + + "\x17CreateSshSessionRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\xce\x02\n" + + "\x18CreateSshSessionResponse\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + + "\x05token\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12!\n" + + "\fgateway_host\x18\x03 \x01(\tR\vgatewayHost\x12!\n" + + "\fgateway_port\x18\x04 \x01(\rR\vgatewayPort\x12%\n" + + "\x0egateway_scheme\x18\x05 \x01(\tR\rgatewayScheme\x120\n" + + "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12C\n" + + "\x0fexpiration_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\b\x10\tR\rexpires_at_ms\"\xc7\x02\n" + + "\x14ExposeServiceRequest\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + + "\vtarget_port\x18\x03 \x01(\rR\n" + + "targetPort\x12\x16\n" + + "\x06domain\x18\x04 \x01(\bR\x06domain\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x06 \x01(\tR\trequestId\x12U\n" + + "\x12authorization_mode\x18\a \x01(\x0e2&.openshell.v1.ServiceAuthorizationModeR\x11authorizationMode\"\x95\x01\n" + + "\x11GetServiceRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\xbf\x01\n" + + "\x13ListServicesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\x81\x01\n" + + "\x14ListServicesResponse\x12A\n" + + "\bservices\x18\x01 \x03(\v2%.openshell.v1.ServiceEndpointResponseR\bservices\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xdc\x01\n" + + "\x14DeleteServiceRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12#\n" + + "\rallow_missing\x18\x04 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"_\n" + + "\x15DeleteServiceResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\xae\x02\n" + + "\x0fServiceEndpoint\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12\x18\n" + + "\asandbox\x18\x03 \x01(\tR\asandbox\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1f\n" + + "\vtarget_port\x18\x05 \x01(\rR\n" + + "targetPort\x12\x16\n" + + "\x06domain\x18\x06 \x01(\bR\x06domain\x12U\n" + + "\x12authorization_mode\x18\a \x01(\x0e2&.openshell.v1.ServiceAuthorizationModeR\x11authorizationMode\"f\n" + + "\x17ServiceEndpointResponse\x129\n" + + "\bendpoint\x18\x01 \x01(\v2\x1d.openshell.v1.ServiceEndpointR\bendpoint\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\"Z\n" + + "\x17RevokeSshSessionRequest\x12\x1a\n" + + "\x05token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12#\n" + + "\rallow_missing\x18\x02 \x01(\bR\fallowMissing\"b\n" + + "\x18RevokeSshSessionResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\arevoked\"\xbf\x04\n" + + "\x12ExecSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\f \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + + "\acommand\x18\x02 \x03(\tR\acommand\x12\x18\n" + + "\aworkdir\x18\x03 \x01(\tR\aworkdir\x12S\n" + + "\venvironment\x18\x04 \x03(\v21.openshell.v1.ExecSandboxRequest.EnvironmentEntryR\venvironment\x12F\n" + + "\x11execution_timeout\x18i \x01(\v2\x19.google.protobuf.DurationR\x10executionTimeout\x12\x14\n" + + "\x05stdin\x18\x06 \x01(\fR\x05stdin\x12\x10\n" + + "\x03tty\x18\a \x01(\bR\x03tty\x12\x12\n" + + "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + + "\x04rows\x18\t \x01(\rR\x04rows\x12$\n" + + "\x0eno_login_shell\x18\n" + + " \x01(\bR\fnoLoginShell\x12\x1d\n" + + "\n" + + "request_id\x18\v \x01(\tR\trequestId\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x05\x10\x06R\x0ftimeout_seconds\"'\n" + + "\x11ExecSandboxStdout\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\"'\n" + + "\x11ExecSandboxStderr\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\".\n" + + "\x0fExecSandboxExit\x12\x1b\n" + + "\texit_code\x18\x01 \x01(\x05R\bexitCode\"\xc8\x01\n" + + "\x10ExecSandboxEvent\x129\n" + + "\x06stdout\x18\x01 \x01(\v2\x1f.openshell.v1.ExecSandboxStdoutH\x00R\x06stdout\x129\n" + + "\x06stderr\x18\x02 \x01(\v2\x1f.openshell.v1.ExecSandboxStderrH\x00R\x06stderr\x123\n" + + "\x04exit\x18\x03 \x01(\v2\x1d.openshell.v1.ExecSandboxExitH\x00R\x04exitB\t\n" + + "\apayload\"\x8c\x02\n" + + "\x0eTcpForwardInit\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1c\n" + + "\tworkspace\x18\x03 \x01(\tR\tworkspace\x12\x1d\n" + + "\n" + + "service_id\x18\x04 \x01(\tR\tserviceId\x120\n" + + "\x03ssh\x18\x05 \x01(\v2\x1c.openshell.v1.SshRelayTargetH\x00R\x03ssh\x120\n" + + "\x03tcp\x18\x06 \x01(\v2\x1c.openshell.v1.TcpRelayTargetH\x00R\x03tcp\x125\n" + + "\x13authorization_token\x18\a \x01(\tB\x04\x88\xb5\x18\x01R\x12authorizationTokenB\b\n" + + "\x06target\"f\n" + + "\x0fTcpForwardFrame\x122\n" + + "\x04init\x18\x01 \x01(\v2\x1c.openshell.v1.TcpForwardInitH\x00R\x04init\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04dataB\t\n" + + "\apayload\"\xb0\x01\n" + + "\x10ExecSandboxInput\x128\n" + + "\x05start\x18\x01 \x01(\v2 .openshell.v1.ExecSandboxRequestH\x00R\x05start\x12\x16\n" + + "\x05stdin\x18\x02 \x01(\fH\x00R\x05stdin\x12?\n" + + "\x06resize\x18\x03 \x01(\v2%.openshell.v1.ExecSandboxWindowResizeH\x00R\x06resizeB\t\n" + + "\apayload\"A\n" + + "\x17ExecSandboxWindowResize\x12\x12\n" + + "\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" + + "\x04rows\x18\x02 \x01(\rR\x04rows\"\xfb\x01\n" + + "\n" + + "SshSession\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12\x1a\n" + + "\x05token\x18\x03 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18h \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12\x18\n" + + "\arevoked\x18\x05 \x01(\bR\arevokedJ\x04\b\x04\x10\x05R\rexpires_at_ms\"\xa1\x04\n" + + "\x13WatchSandboxRequest\x12R\n" + + "\x0fworkspace_scope\x18\v \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12#\n" + + "\rfollow_status\x18\x02 \x01(\bR\ffollowStatus\x12\x1f\n" + + "\vfollow_logs\x18\x03 \x01(\bR\n" + + "followLogs\x12#\n" + + "\rfollow_events\x18\x04 \x01(\bR\ffollowEvents\x12$\n" + + "\x0elog_tail_lines\x18\x05 \x01(\rR\flogTailLines\x12\x1d\n" + + "\n" + + "event_tail\x18\x06 \x01(\rR\teventTail\x12(\n" + + "\x10stop_on_terminal\x18\a \x01(\bR\x0estopOnTerminal\x129\n" + + "\n" + + "since_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\tsinceTime\x12\x1f\n" + + "\vlog_sources\x18\t \x03(\tR\n" + + "logSources\x12\"\n" + + "\rlog_min_level\x18\n" + + " \x01(\tR\vlogMinLevel\x12.\n" + + "\x13resume_after_cursor\x18\f \x01(\tR\x11resumeAfterCursorJ\x04\b\b\x10\tR\flog_since_ms\"\xe4\x02\n" + + "\x12SandboxStreamEvent\x121\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxH\x00R\asandbox\x120\n" + + "\x03log\x18\x02 \x01(\v2\x1c.openshell.v1.SandboxLogLineH\x00R\x03log\x123\n" + + "\x05event\x18\x03 \x01(\v2\x1b.openshell.v1.PlatformEventH\x00R\x05event\x12>\n" + + "\awarning\x18\x04 \x01(\v2\".openshell.v1.SandboxStreamWarningH\x00R\awarning\x12Q\n" + + "\x13draft_policy_update\x18\x05 \x01(\v2\x1f.openshell.v1.DraftPolicyUpdateH\x00R\x11draftPolicyUpdate\x12\x16\n" + + "\x06cursor\x18\x06 \x01(\tR\x06cursorB\t\n" + + "\apayload\"\xdb\x02\n" + + "\x0eSandboxLogLine\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x129\n" + + "\n" + + "event_time\x18f \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x14\n" + + "\x05level\x18\x03 \x01(\tR\x05level\x12\x16\n" + + "\x06target\x18\x04 \x01(\tR\x06target\x12\x18\n" + + "\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" + + "\x06source\x18\x06 \x01(\tR\x06source\x12@\n" + + "\x06fields\x18\a \x03(\v2(.openshell.v1.SandboxLogLine.FieldsEntryR\x06fields\x1a9\n" + + "\vFieldsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x02\x10\x03R\ftimestamp_ms\"0\n" + + "\x14SandboxStreamWarning\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\"\xc8\x01\n" + + "\x15CreateProviderRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12<\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12\x1d\n" + + "\n" + + "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\"\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\"\xcd\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" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x12%\n" + + "\x0elabel_selector\x18\x04 \x01(\tR\rlabelSelector\"\xa0\x04\n" + + "\x15UpdateProviderRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12<\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12\x82\x01\n" + + "\x1bcredential_expiration_times\x18f \x03(\v2B.openshell.v1.UpdateProviderRequest.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12G\n" + + " clear_credential_expiration_keys\x18g \x03(\tR\x1dclearCredentialExpirationKeys\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01J\x04\b\x02\x10\x03R\x18credential_expires_at_ms\"\xc3\x01\n" + + "\x15DeleteProviderRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rallow_missing\x18\x04 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\xc1\x01\n" + + "\x10ProviderResponse\x12<\n" + + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\x12N\n" + + "\x0ftarget_receipts\x18\x02 \x03(\v2%.openshell.v1.ProviderMutationReceiptR\x0etargetReceipts\x12\x1f\n" + + "\vmutation_id\x18\x03 \x01(\tR\n" + + "mutationId\"\x7f\n" + + "\x15ListProvidersResponse\x12>\n" + + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xad\x01\n" + + "\x1bListProviderProfilesRequest\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" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\"\x7f\n" + + "\x19GetProviderProfileRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"l\n" + + "\x19ProviderProfileImportItem\x127\n" + + "\aprofile\x18\x01 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\"\x9e\x01\n" + + "\x19ProviderProfileDiagnostic\x12\x16\n" + + "\x06source\x18\x01 \x01(\tR\x06source\x12\x1d\n" + + "\n" + + "profile_id\x18\x02 \x01(\tR\tprofileId\x12\x14\n" + + "\x05field\x18\x03 \x01(\tR\x05field\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12\x1a\n" + + "\bseverity\x18\x05 \x01(\tR\bseverity\"\x9e\x01\n" + + ",ProviderCredentialTokenGrantAudienceOverride\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x1a\n" + + "\baudience\x18\x04 \x01(\tR\baudience\x12\x16\n" + + "\x06scopes\x18\x05 \x03(\tR\x06scopes\"\x90\x01\n" + + "(ProviderCredentialTokenGrantSubjectToken\x12\x16\n" + + "\x06source\x18\x01 \x01(\tR\x06source\x12\x1e\n" + + "\n" + + "credential\x18\x02 \x01(\tR\n" + + "credential\x12,\n" + + "\x12subject_token_type\x18\x03 \x01(\tR\x10subjectTokenType\"\xf3\x04\n" + + "\x1cProviderCredentialTokenGrant\x12%\n" + + "\x0etoken_endpoint\x18\x01 \x01(\tR\rtokenEndpoint\x12\x1a\n" + + "\baudience\x18\x02 \x01(\tR\baudience\x12*\n" + + "\x11jwt_svid_audience\x18\x06 \x01(\tR\x0fjwtSvidAudience\x12\x16\n" + + "\x06scopes\x18\x03 \x03(\tR\x06scopes\x126\n" + + "\tcache_ttl\x18h \x01(\v2\x19.google.protobuf.DurationR\bcacheTtl\x12i\n" + + "\x12audience_overrides\x18\x05 \x03(\v2:.openshell.v1.ProviderCredentialTokenGrantAudienceOverrideR\x11audienceOverrides\x122\n" + + "\x15client_assertion_type\x18\a \x01(\tR\x13clientAssertionType\x12M\n" + + "\n" + + "grant_type\x18\b \x01(\x0e2..openshell.v1.ProviderCredentialTokenGrantTypeR\tgrantType\x12[\n" + + "\rsubject_token\x18\t \x01(\v26.openshell.v1.ProviderCredentialTokenGrantSubjectTokenR\fsubjectToken\x120\n" + + "\x14requested_token_type\x18\n" + + " \x01(\tR\x12requestedTokenTypeJ\x04\b\x04\x10\x05R\x11cache_ttl_seconds\"\xfb\x03\n" + + "\x19ProviderProfileCredential\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + + "\benv_vars\x18\x03 \x03(\tR\aenvVars\x12\x1a\n" + + "\brequired\x18\x04 \x01(\bR\brequired\x12\x1d\n" + + "\n" + + "auth_style\x18\x05 \x01(\tR\tauthStyle\x12\x1f\n" + + "\vheader_name\x18\x06 \x01(\tR\n" + + "headerName\x12\x1f\n" + + "\vquery_param\x18\a \x01(\tR\n" + + "queryParam\x12A\n" + + "\arefresh\x18\b \x01(\v2'.openshell.v1.ProviderCredentialRefreshR\arefresh\x12#\n" + + "\rpath_template\x18\t \x01(\tR\fpathTemplate\x12K\n" + + "\vtoken_grant\x18\n" + + " \x01(\v2*.openshell.v1.ProviderCredentialTokenGrantR\n" + + "tokenGrant\x12,\n" + + "\x12token_grant_owners\x18\v \x03(\tR\x10tokenGrantOwners\x12-\n" + + "\x12stable_placeholder\x18\f \x01(\bR\x11stablePlaceholder\"\x8d\x01\n" + + "!ProviderCredentialRefreshMaterial\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + + "\brequired\x18\x03 \x01(\bR\brequired\x12\x16\n" + + "\x06secret\x18\x04 \x01(\bR\x06secret\"Y\n" + + "\x1fProviderCredentialRefreshOutput\x12\x16\n" + + "\x06output\x18\x01 \x01(\tR\x06output\x12\x1e\n" + + "\n" + + "credential\x18\x02 \x01(\tR\n" + + "credential\"\x82\x04\n" + + "\x19ProviderCredentialRefresh\x12K\n" + + "\bstrategy\x18\x01 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12\x1b\n" + + "\ttoken_url\x18\x02 \x01(\tR\btokenUrl\x12\x16\n" + + "\x06scopes\x18\x03 \x03(\tR\x06scopes\x12@\n" + + "\x0erefresh_before\x18h \x01(\v2\x19.google.protobuf.DurationR\rrefreshBefore\x12<\n" + + "\fmax_lifetime\x18i \x01(\v2\x19.google.protobuf.DurationR\vmaxLifetime\x12K\n" + + "\bmaterial\x18\x06 \x03(\v2/.openshell.v1.ProviderCredentialRefreshMaterialR\bmaterial\x12\\\n" + + "\x12additional_outputs\x18\a \x03(\v2-.openshell.v1.ProviderCredentialRefreshOutputR\x11additionalOutputsJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x16refresh_before_secondsR\x14max_lifetime_seconds\"\xbc\x06\n" + + "\x1fProviderCredentialRefreshStatus\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x1f\n" + + "\vprovider_id\x18\x02 \x01(\tR\n" + + "providerId\x12%\n" + + "\x0ecredential_key\x18\x03 \x01(\tR\rcredentialKey\x12K\n" + + "\bstrategy\x18\x04 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12C\n" + + "\x0fexpiration_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12F\n" + + "\x11next_refresh_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\x0fnextRefreshTime\x12F\n" + + "\x11last_refresh_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\x0flastRefreshTime\x12\x1d\n" + + "\n" + + "last_error\x18\t \x01(\tR\tlastError\x12^\n" + + "\x0frecovery_action\x18\n" + + " \x01(\x0e25.openshell.v1.ProviderCredentialRefreshRecoveryActionR\x0erecoveryAction\x12!\n" + + "\ffailure_code\x18\v \x01(\tR\vfailureCode\x124\n" + + "\x16provider_error_subtype\x18\f \x01(\tR\x14providerErrorSubtype\x12B\n" + + "\x0flast_error_time\x18q \x01(\v2\x1a.google.protobuf.TimestampR\rlastErrorTimeJ\x04\b\x06\x10\aJ\x04\b\a\x10\bJ\x04\b\b\x10\tJ\x04\b\r\x10\x0eR\rexpires_at_msR\x12next_refresh_at_msR\x12last_refresh_at_msR\x10last_error_at_ms\"<\n" + + "\x18ProviderProfileDiscovery\x12 \n" + + "\vcredentials\x18\x01 \x03(\tR\vcredentials\"\xb8\x01\n" + + "\x1fGetProviderRefreshStatusRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\"s\n" + + " GetProviderRefreshStatusResponse\x12O\n" + + "\vcredentials\x18\x01 \x03(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\vcredentials\"\xcc\x04\n" + + "\x1fConfigureProviderRefreshRequest\x12R\n" + + "\x0fworkspace_scope\x18\a \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12K\n" + + "\bstrategy\x18\x03 \x01(\x0e2/.openshell.v1.ProviderCredentialRefreshStrategyR\bstrategy\x12]\n" + + "\bmaterial\x18\x04 \x03(\v2;.openshell.v1.ConfigureProviderRefreshRequest.MaterialEntryB\x04\x88\xb5\x18\x01R\bmaterial\x120\n" + + "\x14secret_material_keys\x18\x05 \x03(\tR\x12secretMaterialKeys\x12C\n" + + "\x0fexpiration_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\x12\x1d\n" + + "\n" + + "request_id\x18\b \x01(\tR\trequestId\x1a;\n" + + "\rMaterialEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x06\x10\aR\rexpires_at_ms\"i\n" + + " ConfigureProviderRefreshResponse\x12E\n" + + "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\xd7\x01\n" + + "\x1fRotateProviderCredentialRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"i\n" + + " RotateProviderCredentialResponse\x12E\n" + + "\x06status\x18\x01 \x01(\v2-.openshell.v1.ProviderCredentialRefreshStatusR\x06status\"\xf9\x01\n" + + "\x1cDeleteProviderRefreshRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12#\n" + + "\rallow_missing\x18\x05 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x06 \x01(\tR\trequestId\"g\n" + + "\x1dDeleteProviderRefreshResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\x91\x06\n" + + "\x0fProviderProfile\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12A\n" + + "\bcategory\x18\x04 \x01(\x0e2%.openshell.v1.ProviderProfileCategoryR\bcategory\x12I\n" + + "\vcredentials\x18\x05 \x03(\v2'.openshell.v1.ProviderProfileCredentialR\vcredentials\x12C\n" + + "\tendpoints\x18\x06 \x03(\v2%.openshell.sandbox.v1.NetworkEndpointR\tendpoints\x12?\n" + + "\bbinaries\x18\a \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\x12+\n" + + "\x11inference_capable\x18\b \x01(\bR\x10inferenceCapable\x12D\n" + + "\tdiscovery\x18\t \x01(\v2&.openshell.v1.ProviderProfileDiscoveryR\tdiscovery\x12)\n" + + "\x10resource_version\x18\n" + + " \x01(\x04R\x0fresourceVersion\x12P\n" + + "\vannotations\x18\v \x03(\v2..openshell.v1.ProviderProfile.AnnotationsEntryR\vannotations\x12\x16\n" + + "\x06source\x18\f \x01(\tR\x06source\x12\x14\n" + + "\x05scope\x18\r \x01(\tR\x05scope\x127\n" + + "\x05files\x18\x0e \x03(\v2!.openshell.v1.ProviderProfileFileR\x05files\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\\\n" + + "\x13ProviderProfileFile\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\x12\x17\n" + + "\aenv_var\x18\x03 \x01(\tR\x06envVar\"R\n" + + "\x17ProviderProfileResponse\x127\n" + + "\aprofile\x18\x01 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\"\x81\x01\n" + + "\x1cListProviderProfilesResponse\x129\n" + + "\bprofiles\x18\x01 \x03(\v2\x1d.openshell.v1.ProviderProfileR\bprofiles\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xd7\x01\n" + + "\x1dImportProviderProfilesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12C\n" + + "\bprofiles\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileImportItemR\bprofiles\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\xc2\x01\n" + + "\x1eImportProviderProfilesResponse\x12I\n" + + "\vdiagnostics\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileDiagnosticR\vdiagnostics\x129\n" + + "\bprofiles\x18\x02 \x03(\v2\x1d.openshell.v1.ProviderProfileR\bprofiles\x12\x1a\n" + + "\bimported\x18\x03 \x01(\bR\bimported\"\xa1\x02\n" + + "\x1dUpdateProviderProfilesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12A\n" + + "\aprofile\x18\x01 \x01(\v2'.openshell.v1.ProviderProfileImportItemR\aprofile\x12:\n" + + "\x19expected_resource_version\x18\x02 \x01(\x04R\x17expectedResourceVersion\x12\x0e\n" + + "\x02id\x18\x03 \x01(\tR\x02id\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\xbe\x01\n" + + "\x1eUpdateProviderProfilesResponse\x12I\n" + + "\vdiagnostics\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileDiagnosticR\vdiagnostics\x127\n" + + "\aprofile\x18\x02 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\x12\x18\n" + + "\aupdated\x18\x03 \x01(\bR\aupdated\"\xb6\x01\n" + + "\x1bLintProviderProfilesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12C\n" + + "\bprofiles\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileImportItemR\bprofiles\"\xbf\x01\n" + + "\x1cLintProviderProfilesResponse\x12I\n" + + "\vdiagnostics\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileDiagnosticR\vdiagnostics\x12\x14\n" + + "\x05valid\x18\x02 \x01(\bR\x05valid\x12>\n" + + "\x1bsupports_stable_placeholder\x18\x03 \x01(\bR\x19supportsStablePlaceholder\"`\n" + + "\x16DeleteProviderResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\xc6\x01\n" + + "\x1cDeleteProviderProfileRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12#\n" + + "\rallow_missing\x18\x03 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"g\n" + + "\x1dDeleteProviderProfileResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\xf4\x01\n" + + "$GetSandboxProviderEnvironmentRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12M\n" + + "#supports_static_credential_bindings\x18\x02 \x01(\bR supportsStaticCredentialBindings\x12^\n" + + ",supports_stable_placeholder_environment_keys\x18\x03 \x01(\bR(supportsStablePlaceholderEnvironmentKeys\"]\n" + + "\x1fStaticCredentialEndpointBinding\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\"\xd5\x01\n" + + "\x17StaticCredentialBinding\x12K\n" + + "\tendpoints\x18\x01 \x03(\v2-.openshell.v1.StaticCredentialEndpointBindingR\tendpoints\x12/\n" + + "\x13credential_identity\x18\x02 \x01(\tR\x12credentialIdentity\x12<\n" + + "\x1aworkload_credential_handle\x18\x03 \x01(\tR\x18workloadCredentialHandle\"\xc9\f\n" + + "%GetSandboxProviderEnvironmentResponse\x12l\n" + + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryB\x04\x88\xb5\x18\x01R\venvironment\x122\n" + + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x12\x92\x01\n" + + "\x1bcredential_expiration_times\x18g \x03(\v2R.openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpirationTimesEntryR\x19credentialExpirationTimes\x12|\n" + + "\x13dynamic_credentials\x18\x04 \x03(\v2K.openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntryR\x12dynamicCredentials\x12\x8f\x01\n" + + "\x1astatic_credential_bindings\x18\x05 \x03(\v2Q.openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntryR\x18staticCredentialBindings\x12=\n" + + "\x1bnon_secret_environment_keys\x18\x06 \x03(\tR\x18nonSecretEnvironmentKeys\x12:\n" + + "\x19provider_attachment_epoch\x18\a \x01(\tR\x17providerAttachmentEpoch\x12\x1f\n" + + "\vpolicy_hash\x18\b \x01(\tR\n" + + "policyHash\x12P\n" + + "\x10readiness_reason\x18\t \x01(\x0e2%.openshell.v1.ProviderReadinessReasonR\x0freadinessReason\x12T\n" + + "\x05files\x18\n" + + " \x03(\v2>.openshell.v1.GetSandboxProviderEnvironmentResponse.FilesEntryR\x05files\x12M\n" + + "#stable_placeholder_environment_keys\x18\v \x03(\tR stablePlaceholderEnvironmentKeys\x12^\n" + + ",supports_stable_placeholder_environment_keys\x18\f \x01(\bR(supportsStablePlaceholderEnvironmentKeys\x1a>\n" + + "\x10EnvironmentEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + + "\x1eCredentialExpirationTimesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x05value:\x028\x01\x1an\n" + + "\x17DynamicCredentialsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12=\n" + + "\x05value\x18\x02 \x01(\v2'.openshell.v1.ProviderProfileCredentialR\x05value:\x028\x01\x1ar\n" + + "\x1dStaticCredentialBindingsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12;\n" + + "\x05value\x18\x02 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x05value:\x028\x01\x1a8\n" + + "\n" + + "FilesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x03\x10\x04R\x18credential_expires_at_ms\"\xbd\x01\n" + + "#ExchangeProviderSubjectTokenRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x03 \x01(\tR\rcredentialKey\x124\n" + + "\x13supervisor_jwt_svid\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\x11supervisorJwtSvid\"\xc0\x01\n" + + "$ExchangeProviderSubjectTokenResponse\x12'\n" + + "\faccess_token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12>\n" + + "\rexpires_after\x18f \x01(\v2\x19.google.protobuf.DurationR\fexpiresAfter\x12\x1d\n" + + "\n" + + "token_type\x18\x03 \x01(\tR\ttokenTypeJ\x04\b\x02\x10\x03R\n" + + "expires_in\"\xa9\x05\n" + + "\x13UpdateConfigRequest\x12R\n" + + "\x0fworkspace_scope\x18\n" + + " \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12;\n" + + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + + "\vsetting_key\x18\x03 \x01(\tR\n" + + "settingKey\x12G\n" + + "\rsetting_value\x18\x04 \x01(\v2\".openshell.sandbox.v1.SettingValueR\fsettingValue\x12%\n" + + "\x0edelete_setting\x18\x05 \x01(\bR\rdeleteSetting\x12\x16\n" + + "\x06global\x18\x06 \x01(\bR\x06global\x12M\n" + + "\x10merge_operations\x18\a \x03(\v2\".openshell.v1.PolicyMergeOperationR\x0fmergeOperations\x12:\n" + + "\x19expected_resource_version\x18\b \x01(\x04R\x17expectedResourceVersion\x12T\n" + + "\vannotations\x18\t \x03(\v22.openshell.v1.UpdateConfigRequest.AnnotationsEntryR\vannotations\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\v \x01(\tR\trequestId\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc7\x03\n" + + "\x14PolicyMergeOperation\x129\n" + + "\badd_rule\x18\x01 \x01(\v2\x1c.openshell.v1.AddNetworkRuleH\x00R\aaddRule\x12N\n" + + "\x0fremove_endpoint\x18\x02 \x01(\v2#.openshell.v1.RemoveNetworkEndpointH\x00R\x0eremoveEndpoint\x12B\n" + + "\vremove_rule\x18\x03 \x01(\v2\x1f.openshell.v1.RemoveNetworkRuleH\x00R\n" + + "removeRule\x12B\n" + + "\x0eadd_deny_rules\x18\x04 \x01(\v2\x1a.openshell.v1.AddDenyRulesH\x00R\faddDenyRules\x12E\n" + + "\x0fadd_allow_rules\x18\x05 \x01(\v2\x1b.openshell.v1.AddAllowRulesH\x00R\raddAllowRules\x12H\n" + + "\rremove_binary\x18\x06 \x01(\v2!.openshell.v1.RemoveNetworkBinaryH\x00R\fremoveBinaryB\v\n" + + "\toperation\"j\n" + + "\x0eAddNetworkRule\x12\x1b\n" + + "\trule_name\x18\x01 \x01(\tR\bruleName\x12;\n" + + "\x04rule\x18\x02 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\x04rule\"\\\n" + + "\x15RemoveNetworkEndpoint\x12\x1b\n" + + "\trule_name\x18\x01 \x01(\tR\bruleName\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04port\"0\n" + + "\x11RemoveNetworkRule\x12\x1b\n" + + "\trule_name\x18\x01 \x01(\tR\bruleName\"\xd7\x01\n" + + "\fL7RuleTarget\x12\x1b\n" + + "\trule_name\x18\x01 \x01(\tR\bruleName\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x14\n" + + "\x05ports\x18\x03 \x03(\rR\x05ports\x12\x17\n" + + "\x04path\x18\x04 \x01(\tH\x00R\x04path\x88\x01\x01\x12?\n" + + "\bbinaries\x18\x05 \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\x12\x1d\n" + + "\n" + + "any_binary\x18\x06 \x01(\bR\tanyBinaryB\a\n" + + "\x05_path\"\x9b\x01\n" + + "\fAddDenyRules\x12?\n" + + "\n" + + "deny_rules\x18\x03 \x03(\v2 .openshell.sandbox.v1.L7DenyRuleR\tdenyRules\x122\n" + + "\x06target\x18\x04 \x01(\v2\x1a.openshell.v1.L7RuleTargetR\x06targetJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x04hostR\x04port\"\x8f\x01\n" + + "\rAddAllowRules\x122\n" + + "\x05rules\x18\x03 \x03(\v2\x1c.openshell.sandbox.v1.L7RuleR\x05rules\x122\n" + + "\x06target\x18\x04 \x01(\v2\x1a.openshell.v1.L7RuleTargetR\x06targetJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x04hostR\x04port\"S\n" + + "\x13RemoveNetworkBinary\x12\x1b\n" + + "\trule_name\x18\x01 \x01(\tR\bruleName\x12\x1f\n" + + "\vbinary_path\x18\x02 \x01(\tR\n" + + "binaryPath\"\xaf\x02\n" + + "\x14UpdateConfigResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\x12+\n" + + "\x11settings_revision\x18\x03 \x01(\x04R\x10settingsRevision\x12\x18\n" + + "\adeleted\x18\x04 \x01(\bR\adeleted\x12U\n" + + "\vannotations\x18\x05 \x03(\v23.openshell.v1.UpdateConfigResponse.AnnotationsEntryR\vannotations\x1a>\n" + + "\x10AnnotationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbf\x01\n" + + "\x1dGetSandboxPolicyStatusRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\aversion\x18\x02 \x01(\rR\aversion\x12\x16\n" + + "\x06global\x18\x03 \x01(\bR\x06global\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\x88\x01\n" + + "\x1eGetSandboxPolicyStatusResponse\x12?\n" + + "\brevision\x18\x01 \x01(\v2#.openshell.v1.SandboxPolicyRevisionR\brevision\x12%\n" + + "\x0eactive_version\x18\x02 \x01(\rR\ractiveVersion\"\xde\x01\n" + + "\x1aListSandboxPoliciesRequest\x12R\n" + + "\x0fworkspace_scope\x18\x05 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\x12\x16\n" + + "\x06global\x18\x04 \x01(\bR\x06global\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\x88\x01\n" + + "\x1bListSandboxPoliciesResponse\x12A\n" + + "\trevisions\x18\x01 \x03(\v2#.openshell.v1.SandboxPolicyRevisionR\trevisions\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa7\x01\n" + + "\x19ReportPolicyStatusRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x18\n" + + "\aversion\x18\x02 \x01(\rR\aversion\x122\n" + + "\x06status\x18\x03 \x01(\x0e2\x1a.openshell.v1.PolicyStatusR\x06status\x12\x1d\n" + + "\n" + + "load_error\x18\x04 \x01(\tR\tloadError\"\x1c\n" + + "\x1aReportPolicyStatusResponse\"\xbc\x02\n" + + "\x1dSandboxConfigurationAdmission\x12\x1f\n" + + "\vinstance_id\x18\x01 \x01(\tR\n" + + "instanceId\x12?\n" + + "\x05state\x18\x02 \x01(\x0e2).openshell.v1.ConfigurationAdmissionStateR\x05state\x12%\n" + + "\x0epolicy_version\x18\x03 \x01(\rR\rpolicyVersion\x12\x1f\n" + + "\vpolicy_hash\x18\x04 \x01(\tR\n" + + "policyHash\x12'\n" + + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x122\n" + + "\x15provider_env_revision\x18\x06 \x01(\x04R\x13providerEnvRevision\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\"\xbf\x01\n" + + "!ReportSandboxConfigurationRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12I\n" + + "\tadmission\x18\x02 \x01(\v2+.openshell.v1.SandboxConfigurationAdmissionR\tadmission\x120\n" + + "\x14expected_instance_id\x18\x03 \x01(\tR\x12expectedInstanceId\"$\n" + + "\"ReportSandboxConfigurationResponse\"\x9b\x04\n" + + "\x15SandboxPolicyRevision\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\x122\n" + + "\x06status\x18\x03 \x01(\x0e2\x1a.openshell.v1.PolicyStatusR\x06status\x12\x1d\n" + + "\n" + + "load_error\x18\x04 \x01(\tR\tloadError\x12=\n" + + "\fcreated_time\x18i \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12;\n" + + "\vloaded_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "loadedTime\x12;\n" + + "\x06policy\x18\a \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12S\n" + + "\n" + + "provenance\x18\b \x03(\v23.openshell.v1.SandboxPolicyRevision.ProvenanceEntryR\n" + + "provenance\x1a=\n" + + "\x0fProvenanceEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x05\x10\x06J\x04\b\x06\x10\aR\rcreated_at_msR\floaded_at_ms\"\x9d\x02\n" + + "\x15GetSandboxLogsRequest\x12R\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x14\n" + + "\x05lines\x18\x02 \x01(\rR\x05lines\x129\n" + + "\n" + + "since_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\tsinceTime\x12\x18\n" + + "\asources\x18\x04 \x03(\tR\asources\x12\x1b\n" + + "\tmin_level\x18\x05 \x01(\tR\bminLevelJ\x04\b\x03\x10\x04R\bsince_ms\"i\n" + + "\x16PushSandboxLogsRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x120\n" + + "\x04logs\x18\x02 \x03(\v2\x1c.openshell.v1.SandboxLogLineR\x04logs\"\x19\n" + + "\x17PushSandboxLogsResponse\"m\n" + + "\x16GetSandboxLogsResponse\x120\n" + + "\x04logs\x18\x01 \x03(\v2\x1c.openshell.v1.SandboxLogLineR\x04logs\x12!\n" + + "\fbuffer_total\x18\x02 \x01(\rR\vbufferTotal\"\xa2\x02\n" + + "\x11SupervisorMessage\x125\n" + + "\x05hello\x18\x01 \x01(\v2\x1d.openshell.v1.SupervisorHelloH\x00R\x05hello\x12A\n" + + "\theartbeat\x18\x02 \x01(\v2!.openshell.v1.SupervisorHeartbeatH\x00R\theartbeat\x12K\n" + + "\x11relay_open_result\x18\x03 \x01(\v2\x1d.openshell.v1.RelayOpenResultH\x00R\x0frelayOpenResult\x12;\n" + + "\vrelay_close\x18\x04 \x01(\v2\x18.openshell.v1.RelayCloseH\x00R\n" + + "relayCloseB\t\n" + + "\apayload\"\xb6\x03\n" + + "\x0eGatewayMessage\x12J\n" + + "\x10session_accepted\x18\x01 \x01(\v2\x1d.openshell.v1.SessionAcceptedH\x00R\x0fsessionAccepted\x12J\n" + + "\x10session_rejected\x18\x02 \x01(\v2\x1d.openshell.v1.SessionRejectedH\x00R\x0fsessionRejected\x12>\n" + + "\theartbeat\x18\x03 \x01(\v2\x1e.openshell.v1.GatewayHeartbeatH\x00R\theartbeat\x128\n" + + "\n" + + "relay_open\x18\x04 \x01(\v2\x17.openshell.v1.RelayOpenH\x00R\trelayOpen\x12;\n" + + "\vrelay_close\x18\x05 \x01(\v2\x18.openshell.v1.RelayCloseH\x00R\n" + + "relayClose\x12J\n" + + "\x10session_redirect\x18\x06 \x01(\v2\x1d.openshell.v1.SessionRedirectH\x00R\x0fsessionRedirectB\t\n" + + "\apayload\"\x98\x02\n" + + "\x0fSupervisorHello\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + + "\vinstance_id\x18\x02 \x01(\tR\n" + + "instanceId\x12)\n" + + "\x10connection_epoch\x18\x03 \x01(\x04R\x0fconnectionEpoch\x12>\n" + + "\x1bsupports_provider_readiness\x18\x04 \x01(\bR\x19supportsProviderReadiness\x12\x1e\n" + + "\n" + + "redirected\x18\x05 \x01(\bR\n" + + "redirected\x12:\n" + + "\x19supports_session_redirect\x18\x06 \x01(\bR\x17supportsSessionRedirect\"\x99\x01\n" + + "\x0fSessionAccepted\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12H\n" + + "\x12heartbeat_interval\x18f \x01(\v2\x19.google.protobuf.DurationR\x11heartbeatIntervalJ\x04\b\x02\x10\x03R\x17heartbeat_interval_secs\")\n" + + "\x0fSessionRejected\x12\x16\n" + + "\x06reason\x18\x01 \x01(\tR\x06reason\"`\n" + + "\x0fSessionRedirect\x12#\n" + + "\rpeer_endpoint\x18\x01 \x01(\tR\fpeerEndpoint\x12(\n" + + "\x10owner_replica_id\x18\x02 \x01(\tR\x0eownerReplicaId\"\x15\n" + + "\x13SupervisorHeartbeat\"\x12\n" + + "\x10GatewayHeartbeat\"{\n" + + "\x1cReportMainProcessExitRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + + "\vinstance_id\x18\x02 \x01(\tR\n" + + "instanceId\x12\x1b\n" + + "\texit_code\x18\x03 \x01(\x05R\bexitCode\"\x1f\n" + + "\x1dReportMainProcessExitResponse\"`\n" + + "\x1eFinalizeMainProcessExitRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + + "\vinstance_id\x18\x02 \x01(\tR\n" + + "instanceId\"!\n" + + "\x1fFinalizeMainProcessExitResponse\"\xb7\x01\n" + + "\tRelayOpen\x12\x1d\n" + + "\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\x120\n" + + "\x03ssh\x18\x02 \x01(\v2\x1c.openshell.v1.SshRelayTargetH\x00R\x03ssh\x120\n" + + "\x03tcp\x18\x03 \x01(\v2\x1c.openshell.v1.TcpRelayTargetH\x00R\x03tcp\x12\x1d\n" + + "\n" + + "service_id\x18\x05 \x01(\tR\tserviceIdB\b\n" + + "\x06target\"\x10\n" + + "\x0eSshRelayTarget\"8\n" + + "\x0eTcpRelayTarget\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\"*\n" + + "\tRelayInit\x12\x1d\n" + + "\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\"\\\n" + + "\n" + + "RelayFrame\x12-\n" + + "\x04init\x18\x01 \x01(\v2\x17.openshell.v1.RelayInitH\x00R\x04init\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04dataB\t\n" + + "\apayload\"\x98\x01\n" + + "\rPeerRelayInit\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x126\n" + + "\n" + + "relay_open\x18\x02 \x01(\v2\x17.openshell.v1.RelayOpenR\trelayOpen\x120\n" + + "\x14requester_replica_id\x18\x03 \x01(\tR\x12requesterReplicaId\"d\n" + + "\x0ePeerRelayFrame\x121\n" + + "\x04init\x18\x01 \x01(\v2\x1b.openshell.v1.PeerRelayInitH\x00R\x04init\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04dataB\t\n" + + "\apayload\"`\n" + + "\x0fRelayOpenResult\x12\x1d\n" + + "\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"C\n" + + "\n" + + "RelayClose\x12\x1d\n" + + "\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"o\n" + + "\x0fL7RequestSample\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x1a\n" + + "\bdecision\x18\x03 \x01(\tR\bdecision\x12\x14\n" + + "\x05count\x18\x04 \x01(\rR\x05count\"\xce\x05\n" + + "\rDenialSummary\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04port\x12\x16\n" + + "\x06binary\x18\x04 \x01(\tR\x06binary\x12\x1c\n" + + "\tancestors\x18\x05 \x03(\tR\tancestors\x12\x1f\n" + + "\vdeny_reason\x18\x06 \x01(\tR\n" + + "denyReason\x12B\n" + + "\x0ffirst_seen_time\x18k \x01(\v2\x1a.google.protobuf.TimestampR\rfirstSeenTime\x12@\n" + + "\x0elast_seen_time\x18l \x01(\v2\x1a.google.protobuf.TimestampR\flastSeenTime\x12\x14\n" + + "\x05count\x18\t \x01(\rR\x05count\x12)\n" + + "\x10suppressed_count\x18\n" + + " \x01(\rR\x0fsuppressedCount\x12\x1f\n" + + "\vtotal_count\x18\v \x01(\rR\n" + + "totalCount\x12'\n" + + "\x0fsample_cmdlines\x18\f \x03(\tR\x0esampleCmdlines\x12#\n" + + "\rbinary_sha256\x18\r \x01(\tR\fbinarySha256\x12\x1e\n" + + "\n" + + "persistent\x18\x0e \x01(\bR\n" + + "persistent\x12!\n" + + "\fdenial_stage\x18\x0f \x01(\tR\vdenialStage\x12K\n" + + "\x12l7_request_samples\x18\x10 \x03(\v2\x1d.openshell.v1.L7RequestSampleR\x10l7RequestSamples\x120\n" + + "\x14l7_inspection_active\x18\x11 \x01(\bR\x12l7InspectionActiveJ\x04\b\a\x10\bJ\x04\b\b\x10\tR\rfirst_seen_msR\flast_seen_ms\"T\n" + + "\x10DenialGroupCount\x12\x1d\n" + + "\n" + + "deny_group\x18\x01 \x01(\tR\tdenyGroup\x12!\n" + + "\fdenied_count\x18\x02 \x01(\rR\vdeniedCount\"\xc8\x01\n" + + "\x16NetworkActivitySummary\x124\n" + + "\x16network_activity_count\x18\x01 \x01(\rR\x14networkActivityCount\x12.\n" + + "\x13denied_action_count\x18\x02 \x01(\rR\x11deniedActionCount\x12H\n" + + "\x10denials_by_group\x18\x03 \x03(\v2\x1e.openshell.v1.DenialGroupCountR\x0edenialsByGroup\"\xf9\t\n" + + "\vPolicyChunk\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1b\n" + + "\trule_name\x18\x03 \x01(\tR\bruleName\x12L\n" + + "\rproposed_rule\x18\x04 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x1c\n" + + "\trationale\x18\x05 \x01(\tR\trationale\x12%\n" + + "\x0esecurity_notes\x18\x06 \x01(\tR\rsecurityNotes\x12\x1e\n" + + "\n" + + "confidence\x18\a \x01(\x02R\n" + + "confidence\x12,\n" + + "\x12denial_summary_ids\x18\b \x03(\tR\x10denialSummaryIds\x12=\n" + + "\fcreated_time\x18m \x01(\v2\x1a.google.protobuf.TimestampR\vcreatedTime\x12=\n" + + "\fdecided_time\x18n \x01(\v2\x1a.google.protobuf.TimestampR\vdecidedTime\x12\x14\n" + + "\x05stage\x18\v \x01(\tR\x05stage\x12.\n" + + "\x13supersedes_chunk_id\x18\f \x01(\tR\x11supersedesChunkId\x12\x1b\n" + + "\thit_count\x18\r \x01(\x05R\bhitCount\x12B\n" + + "\x0ffirst_seen_time\x18r \x01(\v2\x1a.google.protobuf.TimestampR\rfirstSeenTime\x12@\n" + + "\x0elast_seen_time\x18s \x01(\v2\x1a.google.protobuf.TimestampR\flastSeenTime\x12\x16\n" + + "\x06binary\x18\x10 \x01(\tR\x06binary\x12+\n" + + "\x11validation_result\x18\x11 \x01(\tR\x10validationResult\x12)\n" + + "\x10rejection_reason\x18\x12 \x01(\tR\x0frejectionReason\x12+\n" + + "\x11application_error\x18\x13 \x01(\tR\x10applicationError\x12!\n" + + "\freview_token\x18\x14 \x01(\tR\vreviewToken\x12A\n" + + "\x1dcurrent_effective_policy_hash\x18\x15 \x01(\tR\x1acurrentEffectivePolicyHash\x12E\n" + + "\x1fcandidate_effective_policy_hash\x18\x16 \x01(\tR\x1ccandidateEffectivePolicyHash\x12]\n" + + "\x18current_effective_policy\x18\x17 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x16currentEffectivePolicy\x12a\n" + + "\x1acandidate_effective_policy\x18\x18 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x18candidateEffectivePolicyJ\x04\b\t\x10\n" + + "J\x04\b\n" + + "\x10\vJ\x04\b\x0e\x10\x0fJ\x04\b\x0f\x10\x10R\rcreated_at_msR\rdecided_at_msR\rfirst_seen_msR\flast_seen_ms\"\x96\x01\n" + + "\x11DraftPolicyUpdate\x12#\n" + + "\rdraft_version\x18\x01 \x01(\x04R\fdraftVersion\x12\x1d\n" + + "\n" + + "new_chunks\x18\x02 \x01(\rR\tnewChunks\x12#\n" + + "\rtotal_pending\x18\x03 \x01(\rR\ftotalPending\x12\x18\n" + + "\asummary\x18\x04 \x01(\tR\asummary\"\x8d\x03\n" + + "\x1bSubmitPolicyAnalysisRequest\x12R\n" + + "\x0fworkspace_scope\x18\x06 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x129\n" + + "\tsummaries\x18\x01 \x03(\v2\x1b.openshell.v1.DenialSummaryR\tsummaries\x12B\n" + + "\x0fproposed_chunks\x18\x02 \x03(\v2\x19.openshell.v1.PolicyChunkR\x0eproposedChunks\x12#\n" + + "\ranalysis_mode\x18\x03 \x01(\tR\fanalysisMode\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12b\n" + + "\x1anetwork_activity_summaries\x18\x05 \x03(\v2$.openshell.v1.NetworkActivitySummaryR\x18networkActivitySummaries\"\xcb\x01\n" + + "\x1cSubmitPolicyAnalysisResponse\x12'\n" + + "\x0faccepted_chunks\x18\x01 \x01(\rR\x0eacceptedChunks\x12'\n" + + "\x0frejected_chunks\x18\x02 \x01(\rR\x0erejectedChunks\x12+\n" + + "\x11rejection_reasons\x18\x03 \x03(\tR\x10rejectionReasons\x12,\n" + + "\x12accepted_chunk_ids\x18\x04 \x03(\tR\x10acceptedChunkIds\"\xaa\x01\n" + + "\x15GetDraftPolicyRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12#\n" + + "\rstatus_filter\x18\x02 \x01(\tR\fstatusFilter\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\xfe\x01\n" + + "\x16GetDraftPolicyResponse\x121\n" + + "\x06chunks\x18\x01 \x03(\v2\x19.openshell.v1.PolicyChunkR\x06chunks\x12'\n" + + "\x0frolling_summary\x18\x02 \x01(\tR\x0erollingSummary\x12#\n" + + "\rdraft_version\x18\x03 \x01(\x04R\fdraftVersion\x12H\n" + + "\x12last_analyzed_time\x18h \x01(\v2\x1a.google.protobuf.TimestampR\x10lastAnalyzedTimeJ\x04\b\x04\x10\x05R\x13last_analyzed_at_ms\"\xe5\x01\n" + + "\x18ApproveDraftChunkRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x19\n" + + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12!\n" + + "\freview_token\x18\x04 \x01(\tR\vreviewToken\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"c\n" + + "\x19ApproveDraftChunkResponse\x12%\n" + + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\"\xd9\x01\n" + + "\x17RejectDraftChunkRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x19\n" + + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\x1a\n" + + "\x18RejectDraftChunkResponse\"R\n" + + "\x12DraftChunkApproval\x12\x19\n" + + "\bchunk_id\x18\x01 \x01(\tR\achunkId\x12!\n" + + "\freview_token\x18\x02 \x01(\tR\vreviewToken\"\xa5\x02\n" + + "\x1cApproveAllDraftChunksRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x128\n" + + "\x18include_security_flagged\x18\x02 \x01(\bR\x16includeSecurityFlagged\x12>\n" + + "\tapprovals\x18\x03 \x03(\v2 .openshell.v1.DraftChunkApprovalR\tapprovals\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\xb7\x01\n" + + "\x1dApproveAllDraftChunksResponse\x12%\n" + + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\x12'\n" + + "\x0fchunks_approved\x18\x03 \x01(\rR\x0echunksApproved\x12%\n" + + "\x0echunks_skipped\x18\x04 \x01(\rR\rchunksSkipped\"\x8d\x02\n" + + "\x15EditDraftChunkRequest\x12R\n" + + "\x0fworkspace_scope\x18\x04 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x19\n" + + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12L\n" + + "\rproposed_rule\x18\x03 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x05 \x01(\tR\trequestId\"\x18\n" + + "\x16EditDraftChunkResponse\"\xbf\x01\n" + + "\x15UndoDraftChunkRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x19\n" + + "\bchunk_id\x18\x02 \x01(\tR\achunkId\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"`\n" + + "\x16UndoDraftChunkResponse\x12%\n" + + "\x0epolicy_version\x18\x01 \x01(\rR\rpolicyVersion\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\"\xa6\x01\n" + + "\x17ClearDraftChunksRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"A\n" + + "\x18ClearDraftChunksResponse\x12%\n" + + "\x0echunks_cleared\x18\x01 \x01(\rR\rchunksCleared\"\x86\x01\n" + + "\x16GetDraftHistoryRequest\x12R\n" + + "\x0fworkspace_scope\x18\x02 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\"\xbe\x01\n" + + "\x11DraftHistoryEntry\x129\n" + + "\n" + + "event_time\x18e \x01(\v2\x1a.google.protobuf.TimestampR\teventTime\x12\x1d\n" + + "\n" + + "event_type\x18\x02 \x01(\tR\teventType\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x19\n" + + "\bchunk_id\x18\x04 \x01(\tR\achunkIdJ\x04\b\x01\x10\x02R\ftimestamp_ms\"T\n" + + "\x17GetDraftHistoryResponse\x129\n" + + "\aentries\x18\x01 \x03(\v2\x1f.openshell.v1.DraftHistoryEntryR\aentries\"\xd0\x01\n" + + "\x16CreateWorkspaceRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12H\n" + + "\x06labels\x18\x02 \x03(\v20.openshell.v1.CreateWorkspaceRequest.LabelsEntryR\x06labels\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Z\n" + + "\x17CreateWorkspaceResponse\x12?\n" + + "\tworkspace\x18\x01 \x01(\v2!.openshell.datamodel.v1.WorkspaceR\tworkspace\")\n" + + "\x13GetWorkspaceRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"W\n" + + "\x14GetWorkspaceResponse\x12?\n" + + "\tworkspace\x18\x01 \x01(\v2!.openshell.datamodel.v1.WorkspaceR\tworkspace\"z\n" + + "\x15ListWorkspacesRequest\x12\x1b\n" + + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\x12%\n" + + "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\"\x83\x01\n" + + "\x16ListWorkspacesResponse\x12A\n" + + "\n" + + "workspaces\x18\x01 \x03(\v2!.openshell.datamodel.v1.WorkspaceR\n" + + "workspaces\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"p\n" + + "\x16DeleteWorkspaceRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rallow_missing\x18\x02 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"a\n" + + "\x17DeleteWorkspaceResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\adeleted\"\xaf\x01\n" + + "\x0fWorkspaceMember\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12+\n" + + "\x11principal_subject\x18\x02 \x01(\tR\x10principalSubject\x12/\n" + + "\x04role\x18\x03 \x01(\x0e2\x1b.openshell.v1.WorkspaceRoleR\x04role\"\xec\x01\n" + + "\x19AddWorkspaceMemberRequest\x12R\n" + + "\x0fworkspace_scope\x18\x01 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12+\n" + + "\x11principal_subject\x18\x02 \x01(\tR\x10principalSubject\x12/\n" + + "\x04role\x18\x03 \x01(\x0e2\x1b.openshell.v1.WorkspaceRoleR\x04role\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"S\n" + + "\x1aAddWorkspaceMemberResponse\x125\n" + + "\x06member\x18\x01 \x01(\v2\x1d.openshell.v1.WorkspaceMemberR\x06member\"\xe3\x01\n" + + "\x1cRemoveWorkspaceMemberRequest\x12R\n" + + "\x0fworkspace_scope\x18\x01 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12+\n" + + "\x11principal_subject\x18\x02 \x01(\tR\x10principalSubject\x12#\n" + + "\rallow_missing\x18\x03 \x01(\bR\fallowMissing\x12\x1d\n" + + "\n" + + "request_id\x18\x04 \x01(\tR\trequestId\"g\n" + + "\x1dRemoveWorkspaceMemberResponse\x127\n" + + "\aoutcome\x18\x02 \x01(\x0e2\x1d.openshell.v1.DeletionOutcomeR\aoutcomeJ\x04\b\x01\x10\x02R\aremoved\"\xad\x01\n" + + "\x1bListWorkspaceMembersRequest\x12R\n" + + "\x0fworkspace_scope\x18\x01 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x7f\n" + + "\x1cListWorkspaceMembersResponse\x127\n" + + "\amembers\x18\x01 \x03(\v2\x1d.openshell.v1.WorkspaceMemberR\amembers\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xb5\x01\n" + + "\x1aExtensionServiceCredential\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1a\n" + + "\x05token\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12C\n" + + "\x0fexpiration_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x03\x10\x04R\rexpires_at_ms\"l\n" + + "\x13EndpointObservation\x12\x1f\n" + + "\vendpoint_id\x18\x01 \x01(\tR\n" + + "endpointId\x124\n" + + "\x06result\x18\x02 \x01(\x0e2\x1c.openshell.v1.EndpointResultR\x06result\"\xe9\x02\n" + + "\x1bReportEndpointStatusRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + + "\vpolicy_hash\x18\x02 \x01(\tR\n" + + "policyHash\x122\n" + + "\x15provider_env_revision\x18\x03 \x01(\x04R\x13providerEnvRevision\x12E\n" + + "\fobservations\x18\x04 \x03(\v2!.openshell.v1.EndpointObservationR\fobservations\x122\n" + + "\x15observed_endpoint_ids\x18\x05 \x03(\tR\x13observedEndpointIds\x122\n" + + "\x15supervisor_session_id\x18\x06 \x01(\tR\x13supervisorSessionId\x12'\n" + + "\x0freport_sequence\x18\a \x01(\x04R\x0ereportSequence\"\x1e\n" + + "\x1cReportEndpointStatusResponse\"\x90\x02\n" + + "\x0eEndpointStatus\x12\x1f\n" + + "\vendpoint_id\x18\x01 \x01(\tR\n" + + "endpointId\x12\x12\n" + + "\x04host\x18\x02 \x01(\tR\x04host\x12\x14\n" + + "\x05ports\x18\x03 \x03(\rR\x05ports\x12\x12\n" + + "\x04path\x18\x04 \x01(\tR\x04path\x12=\n" + + "\vlast_result\x18\x05 \x01(\x0e2\x1c.openshell.v1.EndpointResultR\n" + + "lastResult\x12H\n" + + "\x12last_reported_time\x18j \x01(\v2\x1a.google.protobuf.TimestampR\x10lastReportedTimeJ\x04\b\x06\x10\aR\x10last_reported_at\"\xd5\a\n" + + "\x13SandboxProvisioning\x12\x1d\n" + + "\n" + + "attempt_id\x18\x01 \x01(\tR\tattemptId\x126\n" + + "\x17configuration_change_id\x18\x02 \x01(\tR\x15configurationChangeId\x12V\n" + + "\x19configuration_change_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x17configurationChangeTime\x12L\n" + + "\x14first_rejection_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x12firstRejectionTime\x126\n" + + "\bdeadline\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x12=\n" + + "\ftimeout_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vtimeoutTime\x12P\n" + + "\x16cleanup_completed_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\x14cleanupCompletedTime\x12#\n" + + "\rcleanup_error\x18\b \x01(\tR\fcleanupError\x12H\n" + + "\x12cleanup_retry_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x10cleanupRetryTime\x120\n" + + "\x14attachment_change_id\x18\n" + + " \x01(\tR\x12attachmentChangeId\x12P\n" + + "\x16attachment_change_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x14attachmentChangeTime\x12M\n" + + "\x14preparation_deadline\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\x13preparationDeadline\x12L\n" + + "\x14admission_start_time\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\x12admissionStartTime\x128\n" + + "\x18driver_operation_pending\x18\x0e \x01(\bR\x16driverOperationPending\x12.\n" + + "\x13driver_operation_id\x18\x0f \x01(\tR\x11driverOperationId\"\xaa\x01\n" + + "\x16SandboxServiceExposure\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x1f\n" + + "\vtarget_port\x18\x02 \x01(\rR\n" + + "targetPort\x12U\n" + + "\x12authorization_mode\x18\x03 \x01(\x0e2&.openshell.v1.ServiceAuthorizationModeR\x11authorizationMode*\xca\x01\n" + + "\rExtensionKind\x12\x1e\n" + + "\x1aEXTENSION_KIND_UNSPECIFIED\x10\x00\x12!\n" + + "\x1dEXTENSION_KIND_COMPUTE_DRIVER\x10\x01\x12$\n" + + " EXTENSION_KIND_CREDENTIAL_DRIVER\x10\x02\x12&\n" + + "\"EXTENSION_KIND_GATEWAY_INTERCEPTOR\x10\x03\x12(\n" + + "$EXTENSION_KIND_SUPERVISOR_MIDDLEWARE\x10\x04*\xc6\x02\n" + + "\fSandboxPhase\x12\x1d\n" + + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aSANDBOX_PHASE_PROVISIONING\x10\x01\x12\x17\n" + + "\x13SANDBOX_PHASE_READY\x10\x02\x12\x17\n" + + "\x13SANDBOX_PHASE_ERROR\x10\x03\x12\x1a\n" + + "\x16SANDBOX_PHASE_DELETING\x10\x04\x12\x19\n" + + "\x15SANDBOX_PHASE_UNKNOWN\x10\x05\x12\x1a\n" + + "\x16SANDBOX_PHASE_STOPPING\x10\x06\x12\x19\n" + + "\x15SANDBOX_PHASE_STOPPED\x10\a\x12\x1a\n" + + "\x16SANDBOX_PHASE_STARTING\x10\b\x12\x1b\n" + + "\x17SANDBOX_PHASE_COMPLETED\x10\t\"\x04\b\n" + + "\x10\n" + + "*\x18SANDBOX_PHASE_RESTARTING*\xcb\x01\n" + + "\x14ProviderMutationKind\x12&\n" + + "\"PROVIDER_MUTATION_KIND_UNSPECIFIED\x10\x00\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_ATTACH\x10\x01\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_DETACH\x10\x02\x12!\n" + + "\x1dPROVIDER_MUTATION_KIND_UPDATE\x10\x03\x12\"\n" + + "\x1ePROVIDER_MUTATION_KIND_OBSERVE\x10\x04*\xcf\x02\n" + + "\x16ProviderReadinessState\x12(\n" + + "$PROVIDER_READINESS_STATE_UNSPECIFIED\x10\x00\x12&\n" + + "\"PROVIDER_READINESS_STATE_PERSISTED\x10\x01\x12$\n" + + " PROVIDER_READINESS_STATE_PENDING\x10\x02\x12\"\n" + + "\x1ePROVIDER_READINESS_STATE_READY\x10\x03\x12%\n" + + "!PROVIDER_READINESS_STATE_WITHHELD\x10\x04\x12$\n" + + " PROVIDER_READINESS_STATE_REVOKED\x10\x05\x12#\n" + + "\x1fPROVIDER_READINESS_STATE_FAILED\x10\x06\x12'\n" + + "#PROVIDER_READINESS_STATE_SUPERSEDED\x10\a*\xda\x06\n" + + "\x17ProviderReadinessReason\x12)\n" + + "%PROVIDER_READINESS_REASON_UNSPECIFIED\x10\x00\x124\n" + + "0PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR\x10\x01\x125\n" + + "1PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS\x10\x02\x120\n" + + ",PROVIDER_READINESS_REASON_WAITING_FOR_POLICY\x10\x03\x121\n" + + "-PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS\x10\x04\x124\n" + + "0PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR\x10\x05\x122\n" + + ".PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD\x10\x06\x127\n" + + "3PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED\x10\a\x126\n" + + "2PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED\x10\b\x124\n" + + "0PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED\x10\t\x125\n" + + "1PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED\x10\n" + + "\x126\n" + + "2PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED\x10\v\x123\n" + + "/PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED\x10\f\x120\n" + + ",PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED\x10\r\x12*\n" + + "&PROVIDER_READINESS_REASON_LOCAL_POLICY\x10\x0e\x12/\n" + + "+PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH\x10\x0f*\x83\x01\n" + + "\x0fConfigComponent\x12 \n" + + "\x1cCONFIG_COMPONENT_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fCONFIG_COMPONENT_SANDBOX_CONFIG\x10\x01\x12)\n" + + "%CONFIG_COMPONENT_PROVIDER_ENVIRONMENT\x10\x02*\x8d\x03\n" + + "\x12ConfigApplyOutcome\x12$\n" + + " CONFIG_APPLY_OUTCOME_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cCONFIG_APPLY_OUTCOME_APPLIED\x10\x01\x12*\n" + + "&CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE\x10\x02\x12&\n" + + "\"CONFIG_APPLY_OUTCOME_IGNORED_STALE\x10\x03\x120\n" + + ",CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE\x10\x04\x12!\n" + + "\x1dCONFIG_APPLY_OUTCOME_DEGRADED\x10\x05\x128\n" + + "4CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD\x10\x06\x12&\n" + + "\"CONFIG_APPLY_OUTCOME_FAILED_CLOSED\x10\a\x12$\n" + + " CONFIG_APPLY_OUTCOME_UNSUPPORTED\x10\b*\xd2\x02\n" + + "\x1aConfigUpdateOperationState\x12-\n" + + ")CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED\x10\x00\x12)\n" + + "%CONFIG_UPDATE_OPERATION_STATE_PENDING\x10\x01\x12)\n" + + "%CONFIG_UPDATE_OPERATION_STATE_APPLIED\x10\x02\x12*\n" + + "&CONFIG_UPDATE_OPERATION_STATE_INACTIVE\x10\x03\x12(\n" + + "$CONFIG_UPDATE_OPERATION_STATE_FAILED\x10\x04\x12,\n" + + "(CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED\x10\x05\x12+\n" + + "'CONFIG_UPDATE_OPERATION_STATE_CANCELLED\x10\x06*\xce\x01\n" + + " ProviderCredentialTokenGrantType\x124\n" + + "0PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED\x10\x00\x12;\n" + + "7PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS\x10\x01\x127\n" + + "3PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE\x10\x02*\xc3\x03\n" + + "!ProviderCredentialRefreshStrategy\x124\n" + + "0PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED\x10\x00\x12/\n" + + "+PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC\x10\x01\x121\n" + + "-PROVIDER_CREDENTIAL_REFRESH_STRATEGY_EXTERNAL\x10\x02\x12=\n" + + "9PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN\x10\x03\x12B\n" + + ">PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_CLIENT_CREDENTIALS\x10\x04\x12C\n" + + "?PROVIDER_CREDENTIAL_REFRESH_STRATEGY_GOOGLE_SERVICE_ACCOUNT_JWT\x10\x05\x12<\n" + + "8PROVIDER_CREDENTIAL_REFRESH_STRATEGY_AWS_STS_ASSUME_ROLE\x10\x06*\xdb\x02\n" + + "\x17ProviderProfileCategory\x12)\n" + + "%PROVIDER_PROFILE_CATEGORY_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fPROVIDER_PROFILE_CATEGORY_OTHER\x10\x01\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_INFERENCE\x10\x02\x12#\n" + + "\x1fPROVIDER_PROFILE_CATEGORY_AGENT\x10\x03\x12,\n" + + "(PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL\x10\x04\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_MESSAGING\x10\x05\x12\"\n" + + "\x1ePROVIDER_PROFILE_CATEGORY_DATA\x10\x06\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_KNOWLEDGE\x10\a*\xcf\x01\n" + + "\x1bConfigurationAdmissionState\x12-\n" + + ")CONFIGURATION_ADMISSION_STATE_UNSPECIFIED\x10\x00\x12)\n" + + "%CONFIGURATION_ADMISSION_STATE_PENDING\x10\x01\x12*\n" + + "&CONFIGURATION_ADMISSION_STATE_ACCEPTED\x10\x02\x12*\n" + + "&CONFIGURATION_ADMISSION_STATE_REJECTED\x10\x03*\x9a\x01\n" + + "\fPolicyStatus\x12\x1d\n" + + "\x19POLICY_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15POLICY_STATUS_PENDING\x10\x01\x12\x18\n" + + "\x14POLICY_STATUS_LOADED\x10\x02\x12\x18\n" + + "\x14POLICY_STATUS_FAILED\x10\x03\x12\x1c\n" + + "\x18POLICY_STATUS_SUPERSEDED\x10\x04*\x86\x01\n" + + "\rServiceStatus\x12\x1e\n" + + "\x1aSERVICE_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16SERVICE_STATUS_HEALTHY\x10\x01\x12\x1b\n" + + "\x17SERVICE_STATUS_DEGRADED\x10\x02\x12\x1c\n" + + "\x18SERVICE_STATUS_UNHEALTHY\x10\x03*b\n" + + "\rWorkspaceRole\x12\x1e\n" + + "\x1aWORKSPACE_ROLE_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13WORKSPACE_ROLE_USER\x10\x01\x12\x18\n" + + "\x14WORKSPACE_ROLE_ADMIN\x10\x02*\xda\x02\n" + + "'ProviderCredentialRefreshRecoveryAction\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED\x10\x00\x125\n" + + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x04*\xaa\x01\n" + + "\x14SandboxRestartPolicy\x12&\n" + + "\"SANDBOX_RESTART_POLICY_UNSPECIFIED\x10\x00\x12 \n" + + "\x1cSANDBOX_RESTART_POLICY_NEVER\x10\x01\x12%\n" + + "!SANDBOX_RESTART_POLICY_ON_FAILURE\x10\x02\x12!\n" + + "\x1dSANDBOX_RESTART_POLICY_ALWAYS\x10\x03*\x97\x01\n" + + "\x0fDeletionOutcome\x12 \n" + + "\x1cDELETION_OUTCOME_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aDELETION_OUTCOME_COMPLETED\x10\x01\x12\x1d\n" + + "\x19DELETION_OUTCOME_ACCEPTED\x10\x02\x12#\n" + + "\x1fDELETION_OUTCOME_ALREADY_ABSENT\x10\x03*\xc3\x02\n" + + "\x0eEndpointResult\x12\x1f\n" + + "\x1bENDPOINT_RESULT_UNSPECIFIED\x10\x00\x12(\n" + + "$ENDPOINT_RESULT_NO_OBSERVED_EXCHANGE\x10\x01\x12*\n" + + "&ENDPOINT_RESULT_HTTP_RESPONSE_RECEIVED\x10\x02\x12!\n" + + "\x1dENDPOINT_RESULT_POLICY_DENIED\x10\x03\x12*\n" + + "&ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE\x10\x04\x12\x1e\n" + + "\x1aENDPOINT_RESULT_TLS_FAILED\x10\x05\x12$\n" + + " ENDPOINT_RESULT_TRANSPORT_FAILED\x10\x06\x12%\n" + + "!ENDPOINT_RESULT_UPSTREAM_REJECTED\x10\a*\x9f\x01\n" + + "\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\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" + + "\x0eGetCurrentUser\x12#.openshell.v1.GetCurrentUserRequest\x1a$.openshell.v1.GetCurrentUserResponse\"\f\x82\xb5\x18\b\n" + + "\x06bearer\x12\x86\x01\n" + + "\x0eGetGatewayInfo\x12#.openshell.v1.GetGatewayInfoRequest\x1a$.openshell.v1.GetGatewayInfoResponse\")\x82\xb5\x18%\n" + + "\x06bearer\x1a\x0eplatform_admin\"\vconfig:read\x12u\n" + + "\rCreateSandbox\x12\".openshell.v1.CreateSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x93\x01\n" + + "\x15BeginRootfsTarStaging\x12*.openshell.v1.BeginRootfsTarStagingRequest\x1a+.openshell.v1.BeginRootfsTarStagingResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12n\n" + + "\n" + + "GetSandbox\x12\x1f.openshell.v1.GetSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12z\n" + + "\rListSandboxes\x12\".openshell.v1.ListSandboxesRequest\x1a#.openshell.v1.ListSandboxesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8e\x01\n" + + "\x15CreateSandboxTemplate\x12*.openshell.v1.CreateSandboxTemplateRequest\x1a%.openshell.v1.SandboxTemplateResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x05admin\"\rsandbox:write\x12\x86\x01\n" + + "\x12GetSandboxTemplate\x12'.openshell.v1.GetSandboxTemplateRequest\x1a%.openshell.v1.SandboxTemplateResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8f\x01\n" + + "\x14ListSandboxTemplates\x12).openshell.v1.ListSandboxTemplatesRequest\x1a*.openshell.v1.ListSandboxTemplatesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x94\x01\n" + + "\x15DeleteSandboxTemplate\x12*.openshell.v1.DeleteSandboxTemplateRequest\x1a+.openshell.v1.DeleteSandboxTemplateResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x05admin\"\rsandbox:write\x12\x8f\x01\n" + + "\x14ListSandboxProviders\x12).openshell.v1.ListSandboxProvidersRequest\x1a*.openshell.v1.ListSandboxProvidersResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x93\x01\n" + + "\x15AttachSandboxProvider\x12*.openshell.v1.AttachSandboxProviderRequest\x1a+.openshell.v1.AttachSandboxProviderResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x93\x01\n" + + "\x15DetachSandboxProvider\x12*.openshell.v1.DetachSandboxProviderRequest\x1a+.openshell.v1.DetachSandboxProviderResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x9b\x01\n" + + "\x18GetSandboxProviderStatus\x12-.openshell.v1.GetSandboxProviderStatusRequest\x1a..openshell.v1.GetSandboxProviderStatusResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12{\n" + + "\rDeleteSandbox\x12\".openshell.v1.DeleteSandboxRequest\x1a#.openshell.v1.DeleteSandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12q\n" + + "\vStopSandbox\x12 .openshell.v1.StopSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12s\n" + + "\fStartSandbox\x12!.openshell.v1.StartSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x84\x01\n" + + "\x10CreateSshSession\x12%.openshell.v1.CreateSshSessionRequest\x1a&.openshell.v1.CreateSshSessionResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12}\n" + + "\rExposeService\x12\".openshell.v1.ExposeServiceRequest\x1a%.openshell.v1.ServiceEndpointResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12v\n" + + "\n" + + "GetService\x12\x1f.openshell.v1.GetServiceRequest\x1a%.openshell.v1.ServiceEndpointResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12w\n" + + "\fListServices\x12!.openshell.v1.ListServicesRequest\x1a\".openshell.v1.ListServicesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12{\n" + + "\rDeleteService\x12\".openshell.v1.DeleteServiceRequest\x1a#.openshell.v1.DeleteServiceResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\x84\x01\n" + + "\x10RevokeSshSession\x12%.openshell.v1.RevokeSshSessionRequest\x1a&.openshell.v1.RevokeSshSessionResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12t\n" + + "\vExecSandbox\x12 .openshell.v1.ExecSandboxRequest\x1a\x1e.openshell.v1.ExecSandboxEvent\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write0\x01\x12q\n" + + "\n" + + "ForwardTcp\x12\x1d.openshell.v1.TcpForwardFrame\x1a\x1d.openshell.v1.TcpForwardFrame\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write(\x010\x01\x12\x7f\n" + + "\x16ExecSandboxInteractive\x12\x1e.openshell.v1.ExecSandboxInput\x1a\x1e.openshell.v1.ExecSandboxEvent\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write(\x010\x01\x12z\n" + + "\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\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" + + "\x06bearer\x12\x04user\"\rprovider:read\x12\x87\x01\n" + + "\x12GetProviderProfile\x12'.openshell.v1.GetProviderProfileRequest\x1a%.openshell.v1.ProviderProfileResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rprovider:read\x12\x98\x01\n" + + "\x16ImportProviderProfiles\x12+.openshell.v1.ImportProviderProfilesRequest\x1a,.openshell.v1.ImportProviderProfilesResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x98\x01\n" + + "\x16UpdateProviderProfiles\x12+.openshell.v1.UpdateProviderProfilesRequest\x1a,.openshell.v1.UpdateProviderProfilesResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x90\x01\n" + + "\x14LintProviderProfiles\x12).openshell.v1.LintProviderProfilesRequest\x1a*.openshell.v1.LintProviderProfilesResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rprovider:read\x12z\n" + + "\x0eUpdateProvider\x12#.openshell.v1.UpdateProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x9c\x01\n" + + "\x18GetProviderRefreshStatus\x12-.openshell.v1.GetProviderRefreshStatusRequest\x1a..openshell.v1.GetProviderRefreshStatusResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rprovider:read\x12\x9e\x01\n" + + "\x18ConfigureProviderRefresh\x12-.openshell.v1.ConfigureProviderRefreshRequest\x1a..openshell.v1.ConfigureProviderRefreshResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x9e\x01\n" + + "\x18RotateProviderCredential\x12-.openshell.v1.RotateProviderCredentialRequest\x1a..openshell.v1.RotateProviderCredentialResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x95\x01\n" + + "\x15DeleteProviderRefresh\x12*.openshell.v1.DeleteProviderRefreshRequest\x1a+.openshell.v1.DeleteProviderRefreshResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x80\x01\n" + + "\x0eDeleteProvider\x12#.openshell.v1.DeleteProviderRequest\x1a$.openshell.v1.DeleteProviderResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x95\x01\n" + + "\x15DeleteProviderProfile\x12*.openshell.v1.DeleteProviderProfileRequest\x1a+.openshell.v1.DeleteProviderProfileResponse\"#\x82\xb5\x18\x1f\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x90\x01\n" + + "\x10GetSandboxConfig\x12-.openshell.sandbox.v1.GetSandboxConfigRequest\x1a..openshell.sandbox.v1.GetSandboxConfigResponse\"\x1d\x82\xb5\x18\x19\n" + + "\x04dual\x12\x04user\"\vconfig:read\x12\x8c\x01\n" + + "\x10GetGatewayConfig\x12-.openshell.sandbox.v1.GetGatewayConfigRequest\x1a..openshell.sandbox.v1.GetGatewayConfigResponse\"\x19\x82\xb5\x18\x15\n" + + "\x06bearer\"\vconfig:read\x12v\n" + + "\fUpdateConfig\x12!.openshell.v1.UpdateConfigRequest\x1a\".openshell.v1.UpdateConfigResponse\"\x1f\x82\xb5\x18\x1b\n" + + "\x04dual\x12\x05admin\"\fconfig:write\x12\x95\x01\n" + + "\x16GetSandboxPolicyStatus\x12+.openshell.v1.GetSandboxPolicyStatusRequest\x1a,.openshell.v1.GetSandboxPolicyStatusResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\x8c\x01\n" + + "\x13ListSandboxPolicies\x12(.openshell.v1.ListSandboxPoliciesRequest\x1a).openshell.v1.ListSandboxPoliciesResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12v\n" + + "\x12ReportPolicyStatus\x12'.openshell.v1.ReportPolicyStatusRequest\x1a(.openshell.v1.ReportPolicyStatusResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12|\n" + + "\x14ReportEndpointStatus\x12).openshell.v1.ReportEndpointStatusRequest\x1a*.openshell.v1.ReportEndpointStatusResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x85\x01\n" + + "\x17ReportProviderReadiness\x12,.openshell.v1.ReportProviderReadinessRequest\x1a-.openshell.v1.ReportProviderReadinessResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x8e\x01\n" + + "\x1aReportSandboxConfiguration\x12/.openshell.v1.ReportSandboxConfigurationRequest\x1a0.openshell.v1.ReportSandboxConfigurationResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x97\x01\n" + + "\x1dGetSandboxProviderEnvironment\x122.openshell.v1.GetSandboxProviderEnvironmentRequest\x1a3.openshell.v1.GetSandboxProviderEnvironmentResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x94\x01\n" + + "\x1cExchangeProviderSubjectToken\x121.openshell.v1.ExchangeProviderSubjectTokenRequest\x1a2.openshell.v1.ExchangeProviderSubjectTokenResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12}\n" + + "\x0eGetSandboxLogs\x12#.openshell.v1.GetSandboxLogsRequest\x1a$.openshell.v1.GetSandboxLogsResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12o\n" + + "\x0fPushSandboxLogs\x12$.openshell.v1.PushSandboxLogsRequest\x1a%.openshell.v1.PushSandboxLogsResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox(\x01\x12e\n" + + "\x11ConnectSupervisor\x12\x1f.openshell.v1.SupervisorMessage\x1a\x1c.openshell.v1.GatewayMessage\"\r\x82\xb5\x18\t\n" + + "\asandbox(\x010\x01\x12\x7f\n" + + "\x15ReportMainProcessExit\x12*.openshell.v1.ReportMainProcessExitRequest\x1a+.openshell.v1.ReportMainProcessExitResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x85\x01\n" + + "\x17FinalizeMainProcessExit\x12,.openshell.v1.FinalizeMainProcessExitRequest\x1a-.openshell.v1.FinalizeMainProcessExitResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12T\n" + + "\vRelayStream\x12\x18.openshell.v1.RelayFrame\x1a\x18.openshell.v1.RelayFrame\"\r\x82\xb5\x18\t\n" + + "\asandbox(\x010\x01\x12W\n" + + "\tPeerRelay\x12\x1c.openshell.v1.PeerRelayFrame\x1a\x1c.openshell.v1.PeerRelayFrame\"\n" + + "\x82\xb5\x18\x06\n" + + "\x04peer(\x010\x01\x12\x86\x01\n" + + "\x1bPeerReportProviderReadiness\x12,.openshell.v1.ReportProviderReadinessRequest\x1a-.openshell.v1.ReportProviderReadinessResponse\"\n" + + "\x82\xb5\x18\x06\n" + + "\x04peer\x12}\n" + + "\x18PeerReportEndpointStatus\x12).openshell.v1.ReportEndpointStatusRequest\x1a*.openshell.v1.ReportEndpointStatusResponse\"\n" + + "\x82\xb5\x18\x06\n" + + "\x04peer\x12\x89\x01\n" + + "\x1cPeerGetSandboxProviderStatus\x12-.openshell.v1.GetSandboxProviderStatusRequest\x1a..openshell.v1.GetSandboxProviderStatusResponse\"\n" + + "\x82\xb5\x18\x06\n" + + "\x04peer\x12w\n" + + "\fWatchSandbox\x12!.openshell.v1.WatchSandboxRequest\x1a .openshell.v1.SandboxStreamEvent\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read0\x01\x12|\n" + + "\x14SubmitPolicyAnalysis\x12).openshell.v1.SubmitPolicyAnalysisRequest\x1a*.openshell.v1.SubmitPolicyAnalysisResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12z\n" + + "\x0eGetDraftPolicy\x12#.openshell.v1.GetDraftPolicyRequest\x1a$.openshell.v1.GetDraftPolicyResponse\"\x1d\x82\xb5\x18\x19\n" + + "\x04dual\x12\x04user\"\vconfig:read\x12\x87\x01\n" + + "\x11ApproveDraftChunk\x12&.openshell.v1.ApproveDraftChunkRequest\x1a'.openshell.v1.ApproveDraftChunkResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12\x84\x01\n" + + "\x10RejectDraftChunk\x12%.openshell.v1.RejectDraftChunkRequest\x1a&.openshell.v1.RejectDraftChunkResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12\x93\x01\n" + + "\x15ApproveAllDraftChunks\x12*.openshell.v1.ApproveAllDraftChunksRequest\x1a+.openshell.v1.ApproveAllDraftChunksResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12~\n" + + "\x0eEditDraftChunk\x12#.openshell.v1.EditDraftChunkRequest\x1a$.openshell.v1.EditDraftChunkResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12~\n" + + "\x0eUndoDraftChunk\x12#.openshell.v1.UndoDraftChunkRequest\x1a$.openshell.v1.UndoDraftChunkResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12\x84\x01\n" + + "\x10ClearDraftChunks\x12%.openshell.v1.ClearDraftChunksRequest\x1a&.openshell.v1.ClearDraftChunksResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x05admin\"\fconfig:write\x12\x7f\n" + + "\x0fGetDraftHistory\x12$.openshell.v1.GetDraftHistoryRequest\x1a%.openshell.v1.GetDraftHistoryResponse\"\x1f\x82\xb5\x18\x1b\n" + + "\x06bearer\x12\x04user\"\vconfig:read\x12s\n" + + "\x11IssueSandboxToken\x12&.openshell.v1.IssueSandboxTokenRequest\x1a'.openshell.v1.IssueSandboxTokenResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12y\n" + + "\x13RefreshSandboxToken\x12(.openshell.v1.RefreshSandboxTokenRequest\x1a).openshell.v1.RefreshSandboxTokenResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x8d\x01\n" + + "\x0fCreateWorkspace\x12$.openshell.v1.CreateWorkspaceRequest\x1a%.openshell.v1.CreateWorkspaceResponse\"-\x82\xb5\x18)\n" + + "\x06bearer\x1a\x0eplatform_admin\"\x0fworkspace:write\x12y\n" + + "\fGetWorkspace\x12!.openshell.v1.GetWorkspaceRequest\x1a\".openshell.v1.GetWorkspaceResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x04user\"\x0eworkspace:read\x12\x7f\n" + + "\x0eListWorkspaces\x12#.openshell.v1.ListWorkspacesRequest\x1a$.openshell.v1.ListWorkspacesResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x04user\"\x0eworkspace:read\x12\x8d\x01\n" + + "\x0fDeleteWorkspace\x12$.openshell.v1.DeleteWorkspaceRequest\x1a%.openshell.v1.DeleteWorkspaceResponse\"-\x82\xb5\x18)\n" + + "\x06bearer\x1a\x0eplatform_admin\"\x0fworkspace:write\x12\x8d\x01\n" + + "\x12AddWorkspaceMember\x12'.openshell.v1.AddWorkspaceMemberRequest\x1a(.openshell.v1.AddWorkspaceMemberResponse\"$\x82\xb5\x18 \n" + + "\x06bearer\x12\x05admin\"\x0fworkspace:write\x12\x96\x01\n" + + "\x15RemoveWorkspaceMember\x12*.openshell.v1.RemoveWorkspaceMemberRequest\x1a+.openshell.v1.RemoveWorkspaceMemberResponse\"$\x82\xb5\x18 \n" + + "\x06bearer\x12\x05admin\"\x0fworkspace:write\x12\x91\x01\n" + + "\x14ListWorkspaceMembers\x12).openshell.v1.ListWorkspaceMembersRequest\x1a*.openshell.v1.ListWorkspaceMembersResponse\"\"\x82\xb5\x18\x1e\n" + + "\x06bearer\x12\x04user\"\x0eworkspace:readB\xc4\x01\n" + + "\x10com.openshell.v1B\x0eOpenshellProtoP\x01ZOgithub.com/agentnameservice/ans-openshell/gen/openshell/openshellv1;openshellv1\xa2\x02\x03OXX\xaa\x02\fOpenshell.V1\xca\x02\fOpenshell\\V1\xe2\x02\x18Openshell\\V1\\GPBMetadata\xea\x02\rOpenshell::V1b\x06proto3" + +var ( + file_openshell_proto_rawDescOnce sync.Once + file_openshell_proto_rawDescData []byte +) + +func file_openshell_proto_rawDescGZIP() []byte { + file_openshell_proto_rawDescOnce.Do(func() { + file_openshell_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc))) + }) + return file_openshell_proto_rawDescData +} + +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 20) +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 + (ProviderMutationKind)(0), // 2: openshell.v1.ProviderMutationKind + (ProviderReadinessState)(0), // 3: openshell.v1.ProviderReadinessState + (ProviderReadinessReason)(0), // 4: openshell.v1.ProviderReadinessReason + (ConfigComponent)(0), // 5: openshell.v1.ConfigComponent + (ConfigApplyOutcome)(0), // 6: openshell.v1.ConfigApplyOutcome + (ConfigUpdateOperationState)(0), // 7: openshell.v1.ConfigUpdateOperationState + (ProviderCredentialTokenGrantType)(0), // 8: openshell.v1.ProviderCredentialTokenGrantType + (ProviderCredentialRefreshStrategy)(0), // 9: openshell.v1.ProviderCredentialRefreshStrategy + (ProviderProfileCategory)(0), // 10: openshell.v1.ProviderProfileCategory + (ConfigurationAdmissionState)(0), // 11: openshell.v1.ConfigurationAdmissionState + (PolicyStatus)(0), // 12: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 13: openshell.v1.ServiceStatus + (WorkspaceRole)(0), // 14: openshell.v1.WorkspaceRole + (ProviderCredentialRefreshRecoveryAction)(0), // 15: openshell.v1.ProviderCredentialRefreshRecoveryAction + (SandboxRestartPolicy)(0), // 16: openshell.v1.SandboxRestartPolicy + (DeletionOutcome)(0), // 17: openshell.v1.DeletionOutcome + (EndpointResult)(0), // 18: openshell.v1.EndpointResult + (ServiceAuthorizationMode)(0), // 19: openshell.v1.ServiceAuthorizationMode + (*IssueSandboxTokenRequest)(nil), // 20: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 21: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 22: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 23: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 24: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 25: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 26: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 27: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 28: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 29: openshell.v1.GetGatewayInfoResponse + (*NegotiatedExtensionInfo)(nil), // 30: openshell.v1.NegotiatedExtensionInfo + (*ComputeDriverInfo)(nil), // 31: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 32: openshell.v1.ComputeDriverCapabilities + (*ResourceCapabilities)(nil), // 33: openshell.v1.ResourceCapabilities + (*CpuResourceCapabilities)(nil), // 34: openshell.v1.CpuResourceCapabilities + (*MemoryResourceCapabilities)(nil), // 35: openshell.v1.MemoryResourceCapabilities + (*GpuResourceCapabilities)(nil), // 36: openshell.v1.GpuResourceCapabilities + (*Sandbox)(nil), // 37: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 38: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 39: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 40: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 41: openshell.v1.SandboxTemplate + (*SandboxWorkloadTemplate)(nil), // 42: openshell.v1.SandboxWorkloadTemplate + (*SandboxWorkloadTemplateSpec)(nil), // 43: openshell.v1.SandboxWorkloadTemplateSpec + (*SandboxWorkloadConfig)(nil), // 44: openshell.v1.SandboxWorkloadConfig + (*SandboxResources)(nil), // 45: openshell.v1.SandboxResources + (*SandboxServiceLevel)(nil), // 46: openshell.v1.SandboxServiceLevel + (*SandboxStartup)(nil), // 47: openshell.v1.SandboxStartup + (*SandboxWorkloadTemplateProvenance)(nil), // 48: openshell.v1.SandboxWorkloadTemplateProvenance + (*SandboxStatus)(nil), // 49: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 50: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 51: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 52: openshell.v1.CreateSandboxRequest + (*CreateSandboxTemplateRequest)(nil), // 53: openshell.v1.CreateSandboxTemplateRequest + (*GetSandboxTemplateRequest)(nil), // 54: openshell.v1.GetSandboxTemplateRequest + (*ListSandboxTemplatesRequest)(nil), // 55: openshell.v1.ListSandboxTemplatesRequest + (*DeleteSandboxTemplateRequest)(nil), // 56: openshell.v1.DeleteSandboxTemplateRequest + (*SandboxTemplateResponse)(nil), // 57: openshell.v1.SandboxTemplateResponse + (*ListSandboxTemplatesResponse)(nil), // 58: openshell.v1.ListSandboxTemplatesResponse + (*DeleteSandboxTemplateResponse)(nil), // 59: openshell.v1.DeleteSandboxTemplateResponse + (*BeginRootfsTarStagingRequest)(nil), // 60: openshell.v1.BeginRootfsTarStagingRequest + (*BeginRootfsTarStagingResponse)(nil), // 61: openshell.v1.BeginRootfsTarStagingResponse + (*GetSandboxRequest)(nil), // 62: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 63: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 64: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 65: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 66: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 67: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 68: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 69: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 70: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 71: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 72: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 73: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 74: openshell.v1.DetachSandboxProviderResponse + (*ProviderDesiredIdentity)(nil), // 75: openshell.v1.ProviderDesiredIdentity + (*ConfigSnapshotRevision)(nil), // 76: openshell.v1.ConfigSnapshotRevision + (*SandboxConfigRevision)(nil), // 77: openshell.v1.SandboxConfigRevision + (*ConfigUpdateOperation)(nil), // 78: openshell.v1.ConfigUpdateOperation + (*ProviderMutationReceipt)(nil), // 79: openshell.v1.ProviderMutationReceipt + (*ProviderReadinessObservation)(nil), // 80: openshell.v1.ProviderReadinessObservation + (*ProviderReadinessStatus)(nil), // 81: openshell.v1.ProviderReadinessStatus + (*GetSandboxProviderStatusRequest)(nil), // 82: openshell.v1.GetSandboxProviderStatusRequest + (*GetSandboxProviderStatusResponse)(nil), // 83: openshell.v1.GetSandboxProviderStatusResponse + (*ReportProviderReadinessRequest)(nil), // 84: openshell.v1.ReportProviderReadinessRequest + (*ReportProviderReadinessResponse)(nil), // 85: openshell.v1.ReportProviderReadinessResponse + (*DeleteSandboxResponse)(nil), // 86: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 87: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 88: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 89: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 90: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 91: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 92: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 93: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 94: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 95: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 96: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 97: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 98: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 99: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 100: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 101: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 102: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 103: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 104: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 105: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 106: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 107: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 108: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 109: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 110: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 111: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 112: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 113: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 114: openshell.v1.GetProviderRequest + (*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{ + 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 + 30, // 7: openshell.v1.GetGatewayInfoResponse.extensions:type_name -> openshell.v1.NegotiatedExtensionInfo + 0, // 8: openshell.v1.NegotiatedExtensionInfo.kind:type_name -> openshell.v1.ExtensionKind + 32, // 9: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 33, // 10: openshell.v1.ComputeDriverCapabilities.resource_capabilities:type_name -> openshell.v1.ResourceCapabilities + 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 + 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 + 256, // 18: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 41, // 19: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 264, // 73: openshell.v1.SandboxResponse.service_urls:type_name -> openshell.v1.SandboxResponse.ServiceUrlsEntry + 37, // 74: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 285, // 114: openshell.v1.DeleteServiceRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 17, // 115: openshell.v1.DeleteServiceResponse.outcome:type_name -> openshell.v1.DeletionOutcome + 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 + 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 + 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 + 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 + 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() } +func file_openshell_proto_init() { + if File_openshell_proto != nil { + return + } + file_openshell_proto_msgTypes[20].OneofWrappers = []any{} + file_openshell_proto_msgTypes[21].OneofWrappers = []any{} + file_openshell_proto_msgTypes[29].OneofWrappers = []any{} + file_openshell_proto_msgTypes[56].OneofWrappers = []any{ + (*ConfigSnapshotRevision_SandboxConfig)(nil), + (*ConfigSnapshotRevision_ProviderEnvironment)(nil), + (*ConfigSnapshotRevision_ProviderTarget)(nil), + } + file_openshell_proto_msgTypes[83].OneofWrappers = []any{ + (*ExecSandboxEvent_Stdout)(nil), + (*ExecSandboxEvent_Stderr)(nil), + (*ExecSandboxEvent_Exit)(nil), + } + file_openshell_proto_msgTypes[84].OneofWrappers = []any{ + (*TcpForwardInit_Ssh)(nil), + (*TcpForwardInit_Tcp)(nil), + } + file_openshell_proto_msgTypes[85].OneofWrappers = []any{ + (*TcpForwardFrame_Init)(nil), + (*TcpForwardFrame_Data)(nil), + } + file_openshell_proto_msgTypes[86].OneofWrappers = []any{ + (*ExecSandboxInput_Start)(nil), + (*ExecSandboxInput_Stdin)(nil), + (*ExecSandboxInput_Resize)(nil), + } + file_openshell_proto_msgTypes[90].OneofWrappers = []any{ + (*SandboxStreamEvent_Sandbox)(nil), + (*SandboxStreamEvent_Log)(nil), + (*SandboxStreamEvent_Event)(nil), + (*SandboxStreamEvent_Warning)(nil), + (*SandboxStreamEvent_DraftPolicyUpdate)(nil), + } + file_openshell_proto_msgTypes[144].OneofWrappers = []any{ + (*PolicyMergeOperation_AddRule)(nil), + (*PolicyMergeOperation_RemoveEndpoint)(nil), + (*PolicyMergeOperation_RemoveRule)(nil), + (*PolicyMergeOperation_AddDenyRules)(nil), + (*PolicyMergeOperation_AddAllowRules)(nil), + (*PolicyMergeOperation_RemoveBinary)(nil), + } + 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[168].OneofWrappers = []any{ + (*GatewayMessage_SessionAccepted)(nil), + (*GatewayMessage_SessionRejected)(nil), + (*GatewayMessage_Heartbeat)(nil), + (*GatewayMessage_RelayOpen)(nil), + (*GatewayMessage_RelayClose)(nil), + (*GatewayMessage_SessionRedirect)(nil), + } + file_openshell_proto_msgTypes[179].OneofWrappers = []any{ + (*RelayOpen_Ssh)(nil), + (*RelayOpen_Tcp)(nil), + } + file_openshell_proto_msgTypes[183].OneofWrappers = []any{ + (*RelayFrame_Init)(nil), + (*RelayFrame_Data)(nil), + } + file_openshell_proto_msgTypes[185].OneofWrappers = []any{ + (*PeerRelayFrame_Init)(nil), + (*PeerRelayFrame_Data)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), + NumEnums: 20, + NumMessages: 260, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_openshell_proto_goTypes, + DependencyIndexes: file_openshell_proto_depIdxs, + EnumInfos: file_openshell_proto_enumTypes, + MessageInfos: file_openshell_proto_msgTypes, + }.Build() + File_openshell_proto = out.File + file_openshell_proto_goTypes = nil + file_openshell_proto_depIdxs = nil +} diff --git a/gen/openshell/openshellv1/openshell_grpc.pb.go b/gen/openshell/openshellv1/openshell_grpc.pb.go new file mode 100644 index 0000000..f2f754c --- /dev/null +++ b/gen/openshell/openshellv1/openshell_grpc.pb.go @@ -0,0 +1,3499 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: openshell.proto + +package openshellv1 + +import ( + context "context" + sandboxv1 "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + OpenShell_Health_FullMethodName = "/openshell.v1.OpenShell/Health" + OpenShell_GetCurrentUser_FullMethodName = "/openshell.v1.OpenShell/GetCurrentUser" + OpenShell_GetGatewayInfo_FullMethodName = "/openshell.v1.OpenShell/GetGatewayInfo" + OpenShell_CreateSandbox_FullMethodName = "/openshell.v1.OpenShell/CreateSandbox" + OpenShell_BeginRootfsTarStaging_FullMethodName = "/openshell.v1.OpenShell/BeginRootfsTarStaging" + OpenShell_GetSandbox_FullMethodName = "/openshell.v1.OpenShell/GetSandbox" + OpenShell_ListSandboxes_FullMethodName = "/openshell.v1.OpenShell/ListSandboxes" + OpenShell_CreateSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/CreateSandboxTemplate" + OpenShell_GetSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/GetSandboxTemplate" + OpenShell_ListSandboxTemplates_FullMethodName = "/openshell.v1.OpenShell/ListSandboxTemplates" + OpenShell_DeleteSandboxTemplate_FullMethodName = "/openshell.v1.OpenShell/DeleteSandboxTemplate" + OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" + OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" + OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" + OpenShell_GetSandboxProviderStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderStatus" + OpenShell_DeleteSandbox_FullMethodName = "/openshell.v1.OpenShell/DeleteSandbox" + OpenShell_StopSandbox_FullMethodName = "/openshell.v1.OpenShell/StopSandbox" + OpenShell_StartSandbox_FullMethodName = "/openshell.v1.OpenShell/StartSandbox" + OpenShell_CreateSshSession_FullMethodName = "/openshell.v1.OpenShell/CreateSshSession" + OpenShell_ExposeService_FullMethodName = "/openshell.v1.OpenShell/ExposeService" + OpenShell_GetService_FullMethodName = "/openshell.v1.OpenShell/GetService" + OpenShell_ListServices_FullMethodName = "/openshell.v1.OpenShell/ListServices" + OpenShell_DeleteService_FullMethodName = "/openshell.v1.OpenShell/DeleteService" + OpenShell_RevokeSshSession_FullMethodName = "/openshell.v1.OpenShell/RevokeSshSession" + OpenShell_ExecSandbox_FullMethodName = "/openshell.v1.OpenShell/ExecSandbox" + OpenShell_ForwardTcp_FullMethodName = "/openshell.v1.OpenShell/ForwardTcp" + 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" + OpenShell_ImportProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ImportProviderProfiles" + OpenShell_UpdateProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/UpdateProviderProfiles" + OpenShell_LintProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/LintProviderProfiles" + OpenShell_UpdateProvider_FullMethodName = "/openshell.v1.OpenShell/UpdateProvider" + OpenShell_GetProviderRefreshStatus_FullMethodName = "/openshell.v1.OpenShell/GetProviderRefreshStatus" + OpenShell_ConfigureProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/ConfigureProviderRefresh" + OpenShell_RotateProviderCredential_FullMethodName = "/openshell.v1.OpenShell/RotateProviderCredential" + OpenShell_DeleteProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderRefresh" + OpenShell_DeleteProvider_FullMethodName = "/openshell.v1.OpenShell/DeleteProvider" + OpenShell_DeleteProviderProfile_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderProfile" + OpenShell_GetSandboxConfig_FullMethodName = "/openshell.v1.OpenShell/GetSandboxConfig" + OpenShell_GetGatewayConfig_FullMethodName = "/openshell.v1.OpenShell/GetGatewayConfig" + OpenShell_UpdateConfig_FullMethodName = "/openshell.v1.OpenShell/UpdateConfig" + OpenShell_GetSandboxPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxPolicyStatus" + OpenShell_ListSandboxPolicies_FullMethodName = "/openshell.v1.OpenShell/ListSandboxPolicies" + OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" + OpenShell_ReportEndpointStatus_FullMethodName = "/openshell.v1.OpenShell/ReportEndpointStatus" + OpenShell_ReportProviderReadiness_FullMethodName = "/openshell.v1.OpenShell/ReportProviderReadiness" + OpenShell_ReportSandboxConfiguration_FullMethodName = "/openshell.v1.OpenShell/ReportSandboxConfiguration" + OpenShell_GetSandboxProviderEnvironment_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" + OpenShell_ExchangeProviderSubjectToken_FullMethodName = "/openshell.v1.OpenShell/ExchangeProviderSubjectToken" + OpenShell_GetSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/GetSandboxLogs" + OpenShell_PushSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/PushSandboxLogs" + OpenShell_ConnectSupervisor_FullMethodName = "/openshell.v1.OpenShell/ConnectSupervisor" + OpenShell_ReportMainProcessExit_FullMethodName = "/openshell.v1.OpenShell/ReportMainProcessExit" + OpenShell_FinalizeMainProcessExit_FullMethodName = "/openshell.v1.OpenShell/FinalizeMainProcessExit" + OpenShell_RelayStream_FullMethodName = "/openshell.v1.OpenShell/RelayStream" + OpenShell_PeerRelay_FullMethodName = "/openshell.v1.OpenShell/PeerRelay" + OpenShell_PeerReportProviderReadiness_FullMethodName = "/openshell.v1.OpenShell/PeerReportProviderReadiness" + OpenShell_PeerReportEndpointStatus_FullMethodName = "/openshell.v1.OpenShell/PeerReportEndpointStatus" + OpenShell_PeerGetSandboxProviderStatus_FullMethodName = "/openshell.v1.OpenShell/PeerGetSandboxProviderStatus" + OpenShell_WatchSandbox_FullMethodName = "/openshell.v1.OpenShell/WatchSandbox" + OpenShell_SubmitPolicyAnalysis_FullMethodName = "/openshell.v1.OpenShell/SubmitPolicyAnalysis" + OpenShell_GetDraftPolicy_FullMethodName = "/openshell.v1.OpenShell/GetDraftPolicy" + OpenShell_ApproveDraftChunk_FullMethodName = "/openshell.v1.OpenShell/ApproveDraftChunk" + OpenShell_RejectDraftChunk_FullMethodName = "/openshell.v1.OpenShell/RejectDraftChunk" + OpenShell_ApproveAllDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ApproveAllDraftChunks" + OpenShell_EditDraftChunk_FullMethodName = "/openshell.v1.OpenShell/EditDraftChunk" + OpenShell_UndoDraftChunk_FullMethodName = "/openshell.v1.OpenShell/UndoDraftChunk" + OpenShell_ClearDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ClearDraftChunks" + OpenShell_GetDraftHistory_FullMethodName = "/openshell.v1.OpenShell/GetDraftHistory" + OpenShell_IssueSandboxToken_FullMethodName = "/openshell.v1.OpenShell/IssueSandboxToken" + OpenShell_RefreshSandboxToken_FullMethodName = "/openshell.v1.OpenShell/RefreshSandboxToken" + OpenShell_CreateWorkspace_FullMethodName = "/openshell.v1.OpenShell/CreateWorkspace" + OpenShell_GetWorkspace_FullMethodName = "/openshell.v1.OpenShell/GetWorkspace" + OpenShell_ListWorkspaces_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaces" + OpenShell_DeleteWorkspace_FullMethodName = "/openshell.v1.OpenShell/DeleteWorkspace" + OpenShell_AddWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/AddWorkspaceMember" + OpenShell_RemoveWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/RemoveWorkspaceMember" + OpenShell_ListWorkspaceMembers_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaceMembers" +) + +// OpenShellClient is the client API for OpenShell service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// OpenShell service provides sandbox, provider, and runtime management capabilities. +// +// Conventions: +// - This file owns the public API resource model exposed to OpenShell clients. +// - `Sandbox`, `SandboxSpec`, `SandboxStatus`, and `SandboxPhase` are gateway-owned +// public types. Internal compute drivers must not import or return them directly. +// - The gateway translates internal compute-driver observations into these public +// resource messages before persisting or returning them to clients. +type OpenShellClient interface { + // Check the health of the service. + Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) + // Return the authenticated caller identity established by the gateway. + GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) + // Fetch elevated live gateway runtime metadata. + GetGatewayInfo(ctx context.Context, in *GetGatewayInfoRequest, opts ...grpc.CallOption) (*GetGatewayInfoResponse, error) + // Create a new sandbox. + CreateSandbox(ctx context.Context, in *CreateSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) + // Allocate a gateway-owned staging slot for a local rootfs tar archive. + // + // The gateway creates a request-scoped directory inside the compute driver's + // staging root and returns an opaque single-use token plus the absolute path + // the client must write the archive to. The token is then passed as + // `template.driver_config..rootfs_tar_staging_token` on + // CreateSandbox; callers never name a filesystem path themselves. Only a + // client sharing the gateway's filesystem can complete the upload. + BeginRootfsTarStaging(ctx context.Context, in *BeginRootfsTarStagingRequest, opts ...grpc.CallOption) (*BeginRootfsTarStagingResponse, error) + // Fetch a sandbox by name. + GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) + // List sandboxes. + ListSandboxes(ctx context.Context, in *ListSandboxesRequest, opts ...grpc.CallOption) (*ListSandboxesResponse, error) + // Create a reusable sandbox workload template. + CreateSandboxTemplate(ctx context.Context, in *CreateSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) + // Fetch a reusable sandbox workload template by name. + GetSandboxTemplate(ctx context.Context, in *GetSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) + // List reusable sandbox workload templates. + ListSandboxTemplates(ctx context.Context, in *ListSandboxTemplatesRequest, opts ...grpc.CallOption) (*ListSandboxTemplatesResponse, error) + // Delete a reusable sandbox workload template by name. + DeleteSandboxTemplate(ctx context.Context, in *DeleteSandboxTemplateRequest, opts ...grpc.CallOption) (*DeleteSandboxTemplateResponse, error) + // List provider records attached to a sandbox. + ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) + // Attach a provider record to an existing sandbox. + AttachSandboxProvider(ctx context.Context, in *AttachSandboxProviderRequest, opts ...grpc.CallOption) (*AttachSandboxProviderResponse, error) + // Detach a provider record from an existing sandbox. + DetachSandboxProvider(ctx context.Context, in *DetachSandboxProviderRequest, opts ...grpc.CallOption) (*DetachSandboxProviderResponse, error) + // Inspect the installed authority for one sandbox provider mutation. + GetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) + // Delete a sandbox by name. + DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) + // Stop a sandbox while retaining its persistent state. + StopSandbox(ctx context.Context, in *StopSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) + // Start a previously stopped sandbox. + StartSandbox(ctx context.Context, in *StartSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) + // Create a short-lived SSH session for a sandbox. + CreateSshSession(ctx context.Context, in *CreateSshSessionRequest, opts ...grpc.CallOption) (*CreateSshSessionResponse, error) + // Create or update a sandbox HTTP service endpoint for local routing. + ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) + // Fetch one sandbox HTTP service endpoint. + GetService(ctx context.Context, in *GetServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) + // List sandbox HTTP service endpoints. + ListServices(ctx context.Context, in *ListServicesRequest, opts ...grpc.CallOption) (*ListServicesResponse, error) + // Delete one sandbox HTTP service endpoint. + DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceResponse, error) + // Revoke a previously issued SSH session. + RevokeSshSession(ctx context.Context, in *RevokeSshSessionRequest, opts ...grpc.CallOption) (*RevokeSshSessionResponse, error) + // Execute a command in a ready sandbox and stream output. + ExecSandbox(ctx context.Context, in *ExecSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSandboxEvent], error) + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + ForwardTcp(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame], error) + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + ExecSandboxInteractive(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent], error) + // Create a provider. + 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. + ListProviderProfiles(ctx context.Context, in *ListProviderProfilesRequest, opts ...grpc.CallOption) (*ListProviderProfilesResponse, error) + // Fetch one provider type profile by id. + GetProviderProfile(ctx context.Context, in *GetProviderProfileRequest, opts ...grpc.CallOption) (*ProviderProfileResponse, error) + // Import custom provider type profiles. + ImportProviderProfiles(ctx context.Context, in *ImportProviderProfilesRequest, opts ...grpc.CallOption) (*ImportProviderProfilesResponse, error) + // Update an existing custom provider type profile. + UpdateProviderProfiles(ctx context.Context, in *UpdateProviderProfilesRequest, opts ...grpc.CallOption) (*UpdateProviderProfilesResponse, error) + // Validate provider type profiles without registering them. + LintProviderProfiles(ctx context.Context, in *LintProviderProfilesRequest, opts ...grpc.CallOption) (*LintProviderProfilesResponse, error) + // Update an existing provider by name. + UpdateProvider(ctx context.Context, in *UpdateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) + // Fetch refresh status for one provider or provider credential. + GetProviderRefreshStatus(ctx context.Context, in *GetProviderRefreshStatusRequest, opts ...grpc.CallOption) (*GetProviderRefreshStatusResponse, error) + // Configure gateway-owned refresh material for one provider credential. + ConfigureProviderRefresh(ctx context.Context, in *ConfigureProviderRefreshRequest, opts ...grpc.CallOption) (*ConfigureProviderRefreshResponse, error) + // Record a gateway-owned refresh request for one provider credential. + RotateProviderCredential(ctx context.Context, in *RotateProviderCredentialRequest, opts ...grpc.CallOption) (*RotateProviderCredentialResponse, error) + // Delete gateway-owned refresh configuration for one provider credential. + DeleteProviderRefresh(ctx context.Context, in *DeleteProviderRefreshRequest, opts ...grpc.CallOption) (*DeleteProviderRefreshResponse, error) + // Delete a provider by name. + DeleteProvider(ctx context.Context, in *DeleteProviderRequest, opts ...grpc.CallOption) (*DeleteProviderResponse, error) + // Delete a custom provider type profile by id. + DeleteProviderProfile(ctx context.Context, in *DeleteProviderProfileRequest, opts ...grpc.CallOption) (*DeleteProviderProfileResponse, error) + // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + GetSandboxConfig(ctx context.Context, in *sandboxv1.GetSandboxConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetSandboxConfigResponse, error) + // Get gateway-global settings (read-only runtime configuration; any + // authenticated user may read these without requiring Platform Admin). + // + // Scope-only (no role): scopes are granted by the IdP at token issuance, + // orthogonal to workspace membership. Deployments that enable scope + // enforcement configure the IdP to grant config:read (or openshell:all) + // to all sandbox users, so this does not block least-privilege flows. + GetGatewayConfig(ctx context.Context, in *sandboxv1.GetGatewayConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetGatewayConfigResponse, error) + // Update settings or policy at sandbox or global scope. + UpdateConfig(ctx context.Context, in *UpdateConfigRequest, opts ...grpc.CallOption) (*UpdateConfigResponse, error) + // Get the load status of a specific policy version. + GetSandboxPolicyStatus(ctx context.Context, in *GetSandboxPolicyStatusRequest, opts ...grpc.CallOption) (*GetSandboxPolicyStatusResponse, error) + // List policy history for a sandbox. + ListSandboxPolicies(ctx context.Context, in *ListSandboxPoliciesRequest, opts ...grpc.CallOption) (*ListSandboxPoliciesResponse, error) + // Report policy load result (called by sandbox after reload attempt). + ReportPolicyStatus(ctx context.Context, in *ReportPolicyStatusRequest, opts ...grpc.CallOption) (*ReportPolicyStatusResponse, error) + // Replace the gateway's observed tool server endpoint status for one sandbox. + ReportEndpointStatus(ctx context.Context, in *ReportEndpointStatusRequest, opts ...grpc.CallOption) (*ReportEndpointStatusResponse, error) + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + ReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) + // Register startup and acknowledge an exact validated runtime configuration. + ReportSandboxConfiguration(ctx context.Context, in *ReportSandboxConfigurationRequest, opts ...grpc.CallOption) (*ReportSandboxConfigurationResponse, error) + // Get provider environment for a sandbox (called by sandbox supervisor at startup). + GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + ExchangeProviderSubjectToken(ctx context.Context, in *ExchangeProviderSubjectTokenRequest, opts ...grpc.CallOption) (*ExchangeProviderSubjectTokenResponse, error) + // Fetch recent sandbox logs (one-shot). + GetSandboxLogs(ctx context.Context, in *GetSandboxLogsRequest, opts ...grpc.CallOption) (*GetSandboxLogsResponse, error) + // Push sandbox supervisor logs to the server (client-streaming). + PushSandboxLogs(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushSandboxLogsRequest, PushSandboxLogsResponse], error) + // Persistent supervisor-to-gateway session (bidirectional streaming). + // + // The supervisor opens this stream at startup and keeps it alive for the + // sandbox lifetime. The gateway uses it to coordinate relay channels for + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. + ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) + // Persist the canonical main process result before the supervisor exits. + ReportMainProcessExit(ctx context.Context, in *ReportMainProcessExitRequest, opts ...grpc.CallOption) (*ReportMainProcessExitResponse, error) + // Confirm that foreground terminal delivery completed naturally. + FinalizeMainProcessExit(ctx context.Context, in *FinalizeMainProcessExitRequest, opts ...grpc.CallOption) (*FinalizeMainProcessExitResponse, error) + // Raw byte relay between supervisor and gateway. + // + // The supervisor initiates this call after receiving a RelayOpen message + // on its ConnectSupervisor stream. The first RelayFrame carries a + // RelayInit with the channel_id to associate the new HTTP/2 stream with + // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. + // + // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — + // no new TLS handshake, no reverse HTTP CONNECT. + RelayStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[RelayFrame, RelayFrame], error) + // Internal gateway-to-gateway relay forwarding. + // + // A gateway replica that receives a user request for a sandbox whose + // supervisor session is owned by a different replica opens this stream to the + // owner. The first frame carries PeerRelayInit; subsequent frames carry raw + // bytes in either direction. This RPC is authenticated as a gateway peer, not + // as a user or sandbox supervisor. + PeerRelay(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PeerRelayFrame, PeerRelayFrame], error) + // Internal gateway-to-owner forwarding for supervisor-bound state. + PeerReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) + PeerReportEndpointStatus(ctx context.Context, in *ReportEndpointStatusRequest, opts ...grpc.CallOption) (*ReportEndpointStatusResponse, error) + PeerGetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) + // Watch a sandbox and stream updates. + // + // This stream can include: + // - Sandbox status snapshots (phase/status) + // - OpenShell server process logs correlated by sandbox_id + // - Platform events correlated to the sandbox + WatchSandbox(ctx context.Context, in *WatchSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SandboxStreamEvent], error) + // Submit denial analysis results from sandbox (summaries + proposed chunks). + SubmitPolicyAnalysis(ctx context.Context, in *SubmitPolicyAnalysisRequest, opts ...grpc.CallOption) (*SubmitPolicyAnalysisResponse, error) + // Get draft policy recommendations for a sandbox. + GetDraftPolicy(ctx context.Context, in *GetDraftPolicyRequest, opts ...grpc.CallOption) (*GetDraftPolicyResponse, error) + // Approve a single draft policy chunk (merges into active policy). + ApproveDraftChunk(ctx context.Context, in *ApproveDraftChunkRequest, opts ...grpc.CallOption) (*ApproveDraftChunkResponse, error) + // Reject a single draft policy chunk. + RejectDraftChunk(ctx context.Context, in *RejectDraftChunkRequest, opts ...grpc.CallOption) (*RejectDraftChunkResponse, error) + // Approve all pending draft chunks (skips security-flagged unless forced). + ApproveAllDraftChunks(ctx context.Context, in *ApproveAllDraftChunksRequest, opts ...grpc.CallOption) (*ApproveAllDraftChunksResponse, error) + // Edit a pending draft chunk in-place (e.g. narrow allowed_ips). + EditDraftChunk(ctx context.Context, in *EditDraftChunkRequest, opts ...grpc.CallOption) (*EditDraftChunkResponse, error) + // Reverse an approval (remove merged rule from active policy). + UndoDraftChunk(ctx context.Context, in *UndoDraftChunkRequest, opts ...grpc.CallOption) (*UndoDraftChunkResponse, error) + // Clear all pending draft chunks for a sandbox. + ClearDraftChunks(ctx context.Context, in *ClearDraftChunksRequest, opts ...grpc.CallOption) (*ClearDraftChunksResponse, error) + // Get decision history for a sandbox's draft policy. + GetDraftHistory(ctx context.Context, in *GetDraftHistoryRequest, opts ...grpc.CallOption) (*GetDraftHistoryResponse, error) + // Exchange a sandbox-bootstrap credential (e.g. a Kubernetes projected + // ServiceAccount token) for a gateway-minted JWT bound to the calling + // sandbox's UUID. Used by the Kubernetes driver path; singleplayer + // drivers receive the gateway JWT directly from the create-sandbox flow + // and never call this RPC. + IssueSandboxToken(ctx context.Context, in *IssueSandboxTokenRequest, opts ...grpc.CallOption) (*IssueSandboxTokenResponse, error) + // Renew the calling sandbox's gateway JWT. Older tokens remain valid + // until their own expiry; deployments should keep token TTLs short to + // bound replay exposure. The supervisor calls this from a background + // task at ~80% of the token's lifetime; the new token is cached in + // memory only — the on-disk bootstrap file is intentionally not + // rewritten. + RefreshSandboxToken(ctx context.Context, in *RefreshSandboxTokenRequest, opts ...grpc.CallOption) (*RefreshSandboxTokenResponse, error) + // Create a workspace. + CreateWorkspace(ctx context.Context, in *CreateWorkspaceRequest, opts ...grpc.CallOption) (*CreateWorkspaceResponse, error) + // Fetch a workspace by name. + GetWorkspace(ctx context.Context, in *GetWorkspaceRequest, opts ...grpc.CallOption) (*GetWorkspaceResponse, error) + // List workspaces. + ListWorkspaces(ctx context.Context, in *ListWorkspacesRequest, opts ...grpc.CallOption) (*ListWorkspacesResponse, error) + // Delete a workspace by name. + DeleteWorkspace(ctx context.Context, in *DeleteWorkspaceRequest, opts ...grpc.CallOption) (*DeleteWorkspaceResponse, error) + // Add a member to a workspace. + AddWorkspaceMember(ctx context.Context, in *AddWorkspaceMemberRequest, opts ...grpc.CallOption) (*AddWorkspaceMemberResponse, error) + // Remove a member from a workspace. + RemoveWorkspaceMember(ctx context.Context, in *RemoveWorkspaceMemberRequest, opts ...grpc.CallOption) (*RemoveWorkspaceMemberResponse, error) + // List members of a workspace. + ListWorkspaceMembers(ctx context.Context, in *ListWorkspaceMembersRequest, opts ...grpc.CallOption) (*ListWorkspaceMembersResponse, error) +} + +type openShellClient struct { + cc grpc.ClientConnInterface +} + +func NewOpenShellClient(cc grpc.ClientConnInterface) OpenShellClient { + return &openShellClient{cc} +} + +func (c *openShellClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HealthResponse) + err := c.cc.Invoke(ctx, OpenShell_Health_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetCurrentUserResponse) + err := c.cc.Invoke(ctx, OpenShell_GetCurrentUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetGatewayInfo(ctx context.Context, in *GetGatewayInfoRequest, opts ...grpc.CallOption) (*GetGatewayInfoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetGatewayInfoResponse) + err := c.cc.Invoke(ctx, OpenShell_GetGatewayInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) CreateSandbox(ctx context.Context, in *CreateSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) BeginRootfsTarStaging(ctx context.Context, in *BeginRootfsTarStagingRequest, opts ...grpc.CallOption) (*BeginRootfsTarStagingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BeginRootfsTarStagingResponse) + err := c.cc.Invoke(ctx, OpenShell_BeginRootfsTarStaging_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListSandboxes(ctx context.Context, in *ListSandboxesRequest, opts ...grpc.CallOption) (*ListSandboxesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) CreateSandboxTemplate(ctx context.Context, in *CreateSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxTemplate(ctx context.Context, in *GetSandboxTemplateRequest, opts ...grpc.CallOption) (*SandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListSandboxTemplates(ctx context.Context, in *ListSandboxTemplatesRequest, opts ...grpc.CallOption) (*ListSandboxTemplatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxTemplatesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxTemplates_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteSandboxTemplate(ctx context.Context, in *DeleteSandboxTemplateRequest, opts ...grpc.CallOption) (*DeleteSandboxTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteSandboxTemplateResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteSandboxTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxProvidersResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxProviders_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) AttachSandboxProvider(ctx context.Context, in *AttachSandboxProviderRequest, opts ...grpc.CallOption) (*AttachSandboxProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AttachSandboxProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_AttachSandboxProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DetachSandboxProvider(ctx context.Context, in *DetachSandboxProviderRequest, opts ...grpc.CallOption) (*DetachSandboxProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DetachSandboxProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_DetachSandboxProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxProviderStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxProviderStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteSandboxResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) StopSandbox(ctx context.Context, in *StopSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxResponse) + err := c.cc.Invoke(ctx, OpenShell_StopSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) StartSandbox(ctx context.Context, in *StartSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SandboxResponse) + err := c.cc.Invoke(ctx, OpenShell_StartSandbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) CreateSshSession(ctx context.Context, in *CreateSshSessionRequest, opts ...grpc.CallOption) (*CreateSshSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateSshSessionResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateSshSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServiceEndpointResponse) + err := c.cc.Invoke(ctx, OpenShell_ExposeService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetService(ctx context.Context, in *GetServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServiceEndpointResponse) + err := c.cc.Invoke(ctx, OpenShell_GetService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListServices(ctx context.Context, in *ListServicesRequest, opts ...grpc.CallOption) (*ListServicesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListServicesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListServices_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteServiceResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RevokeSshSession(ctx context.Context, in *RevokeSshSessionRequest, opts ...grpc.CallOption) (*RevokeSshSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RevokeSshSessionResponse) + err := c.cc.Invoke(ctx, OpenShell_RevokeSshSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ExecSandbox(ctx context.Context, in *ExecSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSandboxEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[0], OpenShell_ExecSandbox_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExecSandboxRequest, ExecSandboxEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxClient = grpc.ServerStreamingClient[ExecSandboxEvent] + +func (c *openShellClient) ForwardTcp(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[1], OpenShell_ForwardTcp_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[TcpForwardFrame, TcpForwardFrame]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ForwardTcpClient = grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame] + +func (c *openShellClient) ExecSandboxInteractive(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[2], OpenShell_ExecSandboxInteractive_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExecSandboxInput, ExecSandboxEvent]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxInteractiveClient = grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent] + +func (c *openShellClient) CreateProvider(ctx context.Context, in *CreateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetProvider(ctx context.Context, in *GetProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_GetProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + 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) + err := c.cc.Invoke(ctx, OpenShell_ListProviders_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListProviderProfiles(ctx context.Context, in *ListProviderProfilesRequest, opts ...grpc.CallOption) (*ListProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetProviderProfile(ctx context.Context, in *GetProviderProfileRequest, opts ...grpc.CallOption) (*ProviderProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderProfileResponse) + err := c.cc.Invoke(ctx, OpenShell_GetProviderProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ImportProviderProfiles(ctx context.Context, in *ImportProviderProfilesRequest, opts ...grpc.CallOption) (*ImportProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_ImportProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) UpdateProviderProfiles(ctx context.Context, in *UpdateProviderProfilesRequest, opts ...grpc.CallOption) (*UpdateProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_UpdateProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) LintProviderProfiles(ctx context.Context, in *LintProviderProfilesRequest, opts ...grpc.CallOption) (*LintProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LintProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_LintProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) UpdateProvider(ctx context.Context, in *UpdateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_UpdateProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetProviderRefreshStatus(ctx context.Context, in *GetProviderRefreshStatusRequest, opts ...grpc.CallOption) (*GetProviderRefreshStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProviderRefreshStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetProviderRefreshStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ConfigureProviderRefresh(ctx context.Context, in *ConfigureProviderRefreshRequest, opts ...grpc.CallOption) (*ConfigureProviderRefreshResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConfigureProviderRefreshResponse) + err := c.cc.Invoke(ctx, OpenShell_ConfigureProviderRefresh_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RotateProviderCredential(ctx context.Context, in *RotateProviderCredentialRequest, opts ...grpc.CallOption) (*RotateProviderCredentialResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RotateProviderCredentialResponse) + err := c.cc.Invoke(ctx, OpenShell_RotateProviderCredential_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteProviderRefresh(ctx context.Context, in *DeleteProviderRefreshRequest, opts ...grpc.CallOption) (*DeleteProviderRefreshResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteProviderRefreshResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteProviderRefresh_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteProvider(ctx context.Context, in *DeleteProviderRequest, opts ...grpc.CallOption) (*DeleteProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteProviderProfile(ctx context.Context, in *DeleteProviderProfileRequest, opts ...grpc.CallOption) (*DeleteProviderProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteProviderProfileResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteProviderProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxConfig(ctx context.Context, in *sandboxv1.GetSandboxConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetSandboxConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(sandboxv1.GetSandboxConfigResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetGatewayConfig(ctx context.Context, in *sandboxv1.GetGatewayConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetGatewayConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(sandboxv1.GetGatewayConfigResponse) + err := c.cc.Invoke(ctx, OpenShell_GetGatewayConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) UpdateConfig(ctx context.Context, in *UpdateConfigRequest, opts ...grpc.CallOption) (*UpdateConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateConfigResponse) + err := c.cc.Invoke(ctx, OpenShell_UpdateConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxPolicyStatus(ctx context.Context, in *GetSandboxPolicyStatusRequest, opts ...grpc.CallOption) (*GetSandboxPolicyStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxPolicyStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxPolicyStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListSandboxPolicies(ctx context.Context, in *ListSandboxPoliciesRequest, opts ...grpc.CallOption) (*ListSandboxPoliciesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxPoliciesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxPolicies_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ReportPolicyStatus(ctx context.Context, in *ReportPolicyStatusRequest, opts ...grpc.CallOption) (*ReportPolicyStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportPolicyStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportPolicyStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ReportEndpointStatus(ctx context.Context, in *ReportEndpointStatusRequest, opts ...grpc.CallOption) (*ReportEndpointStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportEndpointStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportEndpointStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportProviderReadinessResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportProviderReadiness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ReportSandboxConfiguration(ctx context.Context, in *ReportSandboxConfigurationRequest, opts ...grpc.CallOption) (*ReportSandboxConfigurationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportSandboxConfigurationResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportSandboxConfiguration_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxProviderEnvironmentResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxProviderEnvironment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ExchangeProviderSubjectToken(ctx context.Context, in *ExchangeProviderSubjectTokenRequest, opts ...grpc.CallOption) (*ExchangeProviderSubjectTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExchangeProviderSubjectTokenResponse) + err := c.cc.Invoke(ctx, OpenShell_ExchangeProviderSubjectToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetSandboxLogs(ctx context.Context, in *GetSandboxLogsRequest, opts ...grpc.CallOption) (*GetSandboxLogsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxLogsResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxLogs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) PushSandboxLogs(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushSandboxLogsRequest, PushSandboxLogsResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[3], OpenShell_PushSandboxLogs_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[PushSandboxLogsRequest, PushSandboxLogsResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_PushSandboxLogsClient = grpc.ClientStreamingClient[PushSandboxLogsRequest, PushSandboxLogsResponse] + +func (c *openShellClient) ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[4], OpenShell_ConnectSupervisor_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SupervisorMessage, GatewayMessage]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ConnectSupervisorClient = grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage] + +func (c *openShellClient) ReportMainProcessExit(ctx context.Context, in *ReportMainProcessExitRequest, opts ...grpc.CallOption) (*ReportMainProcessExitResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportMainProcessExitResponse) + err := c.cc.Invoke(ctx, OpenShell_ReportMainProcessExit_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) FinalizeMainProcessExit(ctx context.Context, in *FinalizeMainProcessExitRequest, opts ...grpc.CallOption) (*FinalizeMainProcessExitResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(FinalizeMainProcessExitResponse) + err := c.cc.Invoke(ctx, OpenShell_FinalizeMainProcessExit_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RelayStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[RelayFrame, RelayFrame], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[5], OpenShell_RelayStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[RelayFrame, RelayFrame]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_RelayStreamClient = grpc.BidiStreamingClient[RelayFrame, RelayFrame] + +func (c *openShellClient) PeerRelay(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PeerRelayFrame, PeerRelayFrame], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[6], OpenShell_PeerRelay_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[PeerRelayFrame, PeerRelayFrame]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_PeerRelayClient = grpc.BidiStreamingClient[PeerRelayFrame, PeerRelayFrame] + +func (c *openShellClient) PeerReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportProviderReadinessResponse) + err := c.cc.Invoke(ctx, OpenShell_PeerReportProviderReadiness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) PeerReportEndpointStatus(ctx context.Context, in *ReportEndpointStatusRequest, opts ...grpc.CallOption) (*ReportEndpointStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportEndpointStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_PeerReportEndpointStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) PeerGetSandboxProviderStatus(ctx context.Context, in *GetSandboxProviderStatusRequest, opts ...grpc.CallOption) (*GetSandboxProviderStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxProviderStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_PeerGetSandboxProviderStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) WatchSandbox(ctx context.Context, in *WatchSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SandboxStreamEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[7], OpenShell_WatchSandbox_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[WatchSandboxRequest, SandboxStreamEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_WatchSandboxClient = grpc.ServerStreamingClient[SandboxStreamEvent] + +func (c *openShellClient) SubmitPolicyAnalysis(ctx context.Context, in *SubmitPolicyAnalysisRequest, opts ...grpc.CallOption) (*SubmitPolicyAnalysisResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubmitPolicyAnalysisResponse) + err := c.cc.Invoke(ctx, OpenShell_SubmitPolicyAnalysis_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetDraftPolicy(ctx context.Context, in *GetDraftPolicyRequest, opts ...grpc.CallOption) (*GetDraftPolicyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDraftPolicyResponse) + err := c.cc.Invoke(ctx, OpenShell_GetDraftPolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ApproveDraftChunk(ctx context.Context, in *ApproveDraftChunkRequest, opts ...grpc.CallOption) (*ApproveDraftChunkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApproveDraftChunkResponse) + err := c.cc.Invoke(ctx, OpenShell_ApproveDraftChunk_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RejectDraftChunk(ctx context.Context, in *RejectDraftChunkRequest, opts ...grpc.CallOption) (*RejectDraftChunkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RejectDraftChunkResponse) + err := c.cc.Invoke(ctx, OpenShell_RejectDraftChunk_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ApproveAllDraftChunks(ctx context.Context, in *ApproveAllDraftChunksRequest, opts ...grpc.CallOption) (*ApproveAllDraftChunksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApproveAllDraftChunksResponse) + err := c.cc.Invoke(ctx, OpenShell_ApproveAllDraftChunks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) EditDraftChunk(ctx context.Context, in *EditDraftChunkRequest, opts ...grpc.CallOption) (*EditDraftChunkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EditDraftChunkResponse) + err := c.cc.Invoke(ctx, OpenShell_EditDraftChunk_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) UndoDraftChunk(ctx context.Context, in *UndoDraftChunkRequest, opts ...grpc.CallOption) (*UndoDraftChunkResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UndoDraftChunkResponse) + err := c.cc.Invoke(ctx, OpenShell_UndoDraftChunk_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ClearDraftChunks(ctx context.Context, in *ClearDraftChunksRequest, opts ...grpc.CallOption) (*ClearDraftChunksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClearDraftChunksResponse) + err := c.cc.Invoke(ctx, OpenShell_ClearDraftChunks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetDraftHistory(ctx context.Context, in *GetDraftHistoryRequest, opts ...grpc.CallOption) (*GetDraftHistoryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDraftHistoryResponse) + err := c.cc.Invoke(ctx, OpenShell_GetDraftHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) IssueSandboxToken(ctx context.Context, in *IssueSandboxTokenRequest, opts ...grpc.CallOption) (*IssueSandboxTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IssueSandboxTokenResponse) + err := c.cc.Invoke(ctx, OpenShell_IssueSandboxToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RefreshSandboxToken(ctx context.Context, in *RefreshSandboxTokenRequest, opts ...grpc.CallOption) (*RefreshSandboxTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RefreshSandboxTokenResponse) + err := c.cc.Invoke(ctx, OpenShell_RefreshSandboxToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) CreateWorkspace(ctx context.Context, in *CreateWorkspaceRequest, opts ...grpc.CallOption) (*CreateWorkspaceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateWorkspaceResponse) + err := c.cc.Invoke(ctx, OpenShell_CreateWorkspace_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetWorkspace(ctx context.Context, in *GetWorkspaceRequest, opts ...grpc.CallOption) (*GetWorkspaceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetWorkspaceResponse) + err := c.cc.Invoke(ctx, OpenShell_GetWorkspace_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListWorkspaces(ctx context.Context, in *ListWorkspacesRequest, opts ...grpc.CallOption) (*ListWorkspacesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListWorkspacesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListWorkspaces_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteWorkspace(ctx context.Context, in *DeleteWorkspaceRequest, opts ...grpc.CallOption) (*DeleteWorkspaceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteWorkspaceResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteWorkspace_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) AddWorkspaceMember(ctx context.Context, in *AddWorkspaceMemberRequest, opts ...grpc.CallOption) (*AddWorkspaceMemberResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddWorkspaceMemberResponse) + err := c.cc.Invoke(ctx, OpenShell_AddWorkspaceMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RemoveWorkspaceMember(ctx context.Context, in *RemoveWorkspaceMemberRequest, opts ...grpc.CallOption) (*RemoveWorkspaceMemberResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveWorkspaceMemberResponse) + err := c.cc.Invoke(ctx, OpenShell_RemoveWorkspaceMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListWorkspaceMembers(ctx context.Context, in *ListWorkspaceMembersRequest, opts ...grpc.CallOption) (*ListWorkspaceMembersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListWorkspaceMembersResponse) + err := c.cc.Invoke(ctx, OpenShell_ListWorkspaceMembers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// OpenShellServer is the server API for OpenShell service. +// All implementations must embed UnimplementedOpenShellServer +// for forward compatibility. +// +// OpenShell service provides sandbox, provider, and runtime management capabilities. +// +// Conventions: +// - This file owns the public API resource model exposed to OpenShell clients. +// - `Sandbox`, `SandboxSpec`, `SandboxStatus`, and `SandboxPhase` are gateway-owned +// public types. Internal compute drivers must not import or return them directly. +// - The gateway translates internal compute-driver observations into these public +// resource messages before persisting or returning them to clients. +type OpenShellServer interface { + // Check the health of the service. + Health(context.Context, *HealthRequest) (*HealthResponse, error) + // Return the authenticated caller identity established by the gateway. + GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) + // Fetch elevated live gateway runtime metadata. + GetGatewayInfo(context.Context, *GetGatewayInfoRequest) (*GetGatewayInfoResponse, error) + // Create a new sandbox. + CreateSandbox(context.Context, *CreateSandboxRequest) (*SandboxResponse, error) + // Allocate a gateway-owned staging slot for a local rootfs tar archive. + // + // The gateway creates a request-scoped directory inside the compute driver's + // staging root and returns an opaque single-use token plus the absolute path + // the client must write the archive to. The token is then passed as + // `template.driver_config..rootfs_tar_staging_token` on + // CreateSandbox; callers never name a filesystem path themselves. Only a + // client sharing the gateway's filesystem can complete the upload. + BeginRootfsTarStaging(context.Context, *BeginRootfsTarStagingRequest) (*BeginRootfsTarStagingResponse, error) + // Fetch a sandbox by name. + GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) + // List sandboxes. + ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) + // Create a reusable sandbox workload template. + CreateSandboxTemplate(context.Context, *CreateSandboxTemplateRequest) (*SandboxTemplateResponse, error) + // Fetch a reusable sandbox workload template by name. + GetSandboxTemplate(context.Context, *GetSandboxTemplateRequest) (*SandboxTemplateResponse, error) + // List reusable sandbox workload templates. + ListSandboxTemplates(context.Context, *ListSandboxTemplatesRequest) (*ListSandboxTemplatesResponse, error) + // Delete a reusable sandbox workload template by name. + DeleteSandboxTemplate(context.Context, *DeleteSandboxTemplateRequest) (*DeleteSandboxTemplateResponse, error) + // List provider records attached to a sandbox. + ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) + // Attach a provider record to an existing sandbox. + AttachSandboxProvider(context.Context, *AttachSandboxProviderRequest) (*AttachSandboxProviderResponse, error) + // Detach a provider record from an existing sandbox. + DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) + // Inspect the installed authority for one sandbox provider mutation. + GetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) + // Delete a sandbox by name. + DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) + // Stop a sandbox while retaining its persistent state. + StopSandbox(context.Context, *StopSandboxRequest) (*SandboxResponse, error) + // Start a previously stopped sandbox. + StartSandbox(context.Context, *StartSandboxRequest) (*SandboxResponse, error) + // Create a short-lived SSH session for a sandbox. + CreateSshSession(context.Context, *CreateSshSessionRequest) (*CreateSshSessionResponse, error) + // Create or update a sandbox HTTP service endpoint for local routing. + ExposeService(context.Context, *ExposeServiceRequest) (*ServiceEndpointResponse, error) + // Fetch one sandbox HTTP service endpoint. + GetService(context.Context, *GetServiceRequest) (*ServiceEndpointResponse, error) + // List sandbox HTTP service endpoints. + ListServices(context.Context, *ListServicesRequest) (*ListServicesResponse, error) + // Delete one sandbox HTTP service endpoint. + DeleteService(context.Context, *DeleteServiceRequest) (*DeleteServiceResponse, error) + // Revoke a previously issued SSH session. + RevokeSshSession(context.Context, *RevokeSshSessionRequest) (*RevokeSshSessionResponse, error) + // Execute a command in a ready sandbox and stream output. + ExecSandbox(*ExecSandboxRequest, grpc.ServerStreamingServer[ExecSandboxEvent]) error + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + ForwardTcp(grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame]) error + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + ExecSandboxInteractive(grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent]) error + // Create a provider. + 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. + ListProviderProfiles(context.Context, *ListProviderProfilesRequest) (*ListProviderProfilesResponse, error) + // Fetch one provider type profile by id. + GetProviderProfile(context.Context, *GetProviderProfileRequest) (*ProviderProfileResponse, error) + // Import custom provider type profiles. + ImportProviderProfiles(context.Context, *ImportProviderProfilesRequest) (*ImportProviderProfilesResponse, error) + // Update an existing custom provider type profile. + UpdateProviderProfiles(context.Context, *UpdateProviderProfilesRequest) (*UpdateProviderProfilesResponse, error) + // Validate provider type profiles without registering them. + LintProviderProfiles(context.Context, *LintProviderProfilesRequest) (*LintProviderProfilesResponse, error) + // Update an existing provider by name. + UpdateProvider(context.Context, *UpdateProviderRequest) (*ProviderResponse, error) + // Fetch refresh status for one provider or provider credential. + GetProviderRefreshStatus(context.Context, *GetProviderRefreshStatusRequest) (*GetProviderRefreshStatusResponse, error) + // Configure gateway-owned refresh material for one provider credential. + ConfigureProviderRefresh(context.Context, *ConfigureProviderRefreshRequest) (*ConfigureProviderRefreshResponse, error) + // Record a gateway-owned refresh request for one provider credential. + RotateProviderCredential(context.Context, *RotateProviderCredentialRequest) (*RotateProviderCredentialResponse, error) + // Delete gateway-owned refresh configuration for one provider credential. + DeleteProviderRefresh(context.Context, *DeleteProviderRefreshRequest) (*DeleteProviderRefreshResponse, error) + // Delete a provider by name. + DeleteProvider(context.Context, *DeleteProviderRequest) (*DeleteProviderResponse, error) + // Delete a custom provider type profile by id. + DeleteProviderProfile(context.Context, *DeleteProviderProfileRequest) (*DeleteProviderProfileResponse, error) + // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + GetSandboxConfig(context.Context, *sandboxv1.GetSandboxConfigRequest) (*sandboxv1.GetSandboxConfigResponse, error) + // Get gateway-global settings (read-only runtime configuration; any + // authenticated user may read these without requiring Platform Admin). + // + // Scope-only (no role): scopes are granted by the IdP at token issuance, + // orthogonal to workspace membership. Deployments that enable scope + // enforcement configure the IdP to grant config:read (or openshell:all) + // to all sandbox users, so this does not block least-privilege flows. + GetGatewayConfig(context.Context, *sandboxv1.GetGatewayConfigRequest) (*sandboxv1.GetGatewayConfigResponse, error) + // Update settings or policy at sandbox or global scope. + UpdateConfig(context.Context, *UpdateConfigRequest) (*UpdateConfigResponse, error) + // Get the load status of a specific policy version. + GetSandboxPolicyStatus(context.Context, *GetSandboxPolicyStatusRequest) (*GetSandboxPolicyStatusResponse, error) + // List policy history for a sandbox. + ListSandboxPolicies(context.Context, *ListSandboxPoliciesRequest) (*ListSandboxPoliciesResponse, error) + // Report policy load result (called by sandbox after reload attempt). + ReportPolicyStatus(context.Context, *ReportPolicyStatusRequest) (*ReportPolicyStatusResponse, error) + // Replace the gateway's observed tool server endpoint status for one sandbox. + ReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) + // Register startup and acknowledge an exact validated runtime configuration. + ReportSandboxConfiguration(context.Context, *ReportSandboxConfigurationRequest) (*ReportSandboxConfigurationResponse, error) + // Get provider environment for a sandbox (called by sandbox supervisor at startup). + GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + ExchangeProviderSubjectToken(context.Context, *ExchangeProviderSubjectTokenRequest) (*ExchangeProviderSubjectTokenResponse, error) + // Fetch recent sandbox logs (one-shot). + GetSandboxLogs(context.Context, *GetSandboxLogsRequest) (*GetSandboxLogsResponse, error) + // Push sandbox supervisor logs to the server (client-streaming). + PushSandboxLogs(grpc.ClientStreamingServer[PushSandboxLogsRequest, PushSandboxLogsResponse]) error + // Persistent supervisor-to-gateway session (bidirectional streaming). + // + // The supervisor opens this stream at startup and keeps it alive for the + // sandbox lifetime. The gateway uses it to coordinate relay channels for + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. + ConnectSupervisor(grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage]) error + // Persist the canonical main process result before the supervisor exits. + ReportMainProcessExit(context.Context, *ReportMainProcessExitRequest) (*ReportMainProcessExitResponse, error) + // Confirm that foreground terminal delivery completed naturally. + FinalizeMainProcessExit(context.Context, *FinalizeMainProcessExitRequest) (*FinalizeMainProcessExitResponse, error) + // Raw byte relay between supervisor and gateway. + // + // The supervisor initiates this call after receiving a RelayOpen message + // on its ConnectSupervisor stream. The first RelayFrame carries a + // RelayInit with the channel_id to associate the new HTTP/2 stream with + // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. + // + // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — + // no new TLS handshake, no reverse HTTP CONNECT. + RelayStream(grpc.BidiStreamingServer[RelayFrame, RelayFrame]) error + // Internal gateway-to-gateway relay forwarding. + // + // A gateway replica that receives a user request for a sandbox whose + // supervisor session is owned by a different replica opens this stream to the + // owner. The first frame carries PeerRelayInit; subsequent frames carry raw + // bytes in either direction. This RPC is authenticated as a gateway peer, not + // as a user or sandbox supervisor. + PeerRelay(grpc.BidiStreamingServer[PeerRelayFrame, PeerRelayFrame]) error + // Internal gateway-to-owner forwarding for supervisor-bound state. + PeerReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) + PeerReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) + PeerGetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) + // Watch a sandbox and stream updates. + // + // This stream can include: + // - Sandbox status snapshots (phase/status) + // - OpenShell server process logs correlated by sandbox_id + // - Platform events correlated to the sandbox + WatchSandbox(*WatchSandboxRequest, grpc.ServerStreamingServer[SandboxStreamEvent]) error + // Submit denial analysis results from sandbox (summaries + proposed chunks). + SubmitPolicyAnalysis(context.Context, *SubmitPolicyAnalysisRequest) (*SubmitPolicyAnalysisResponse, error) + // Get draft policy recommendations for a sandbox. + GetDraftPolicy(context.Context, *GetDraftPolicyRequest) (*GetDraftPolicyResponse, error) + // Approve a single draft policy chunk (merges into active policy). + ApproveDraftChunk(context.Context, *ApproveDraftChunkRequest) (*ApproveDraftChunkResponse, error) + // Reject a single draft policy chunk. + RejectDraftChunk(context.Context, *RejectDraftChunkRequest) (*RejectDraftChunkResponse, error) + // Approve all pending draft chunks (skips security-flagged unless forced). + ApproveAllDraftChunks(context.Context, *ApproveAllDraftChunksRequest) (*ApproveAllDraftChunksResponse, error) + // Edit a pending draft chunk in-place (e.g. narrow allowed_ips). + EditDraftChunk(context.Context, *EditDraftChunkRequest) (*EditDraftChunkResponse, error) + // Reverse an approval (remove merged rule from active policy). + UndoDraftChunk(context.Context, *UndoDraftChunkRequest) (*UndoDraftChunkResponse, error) + // Clear all pending draft chunks for a sandbox. + ClearDraftChunks(context.Context, *ClearDraftChunksRequest) (*ClearDraftChunksResponse, error) + // Get decision history for a sandbox's draft policy. + GetDraftHistory(context.Context, *GetDraftHistoryRequest) (*GetDraftHistoryResponse, error) + // Exchange a sandbox-bootstrap credential (e.g. a Kubernetes projected + // ServiceAccount token) for a gateway-minted JWT bound to the calling + // sandbox's UUID. Used by the Kubernetes driver path; singleplayer + // drivers receive the gateway JWT directly from the create-sandbox flow + // and never call this RPC. + IssueSandboxToken(context.Context, *IssueSandboxTokenRequest) (*IssueSandboxTokenResponse, error) + // Renew the calling sandbox's gateway JWT. Older tokens remain valid + // until their own expiry; deployments should keep token TTLs short to + // bound replay exposure. The supervisor calls this from a background + // task at ~80% of the token's lifetime; the new token is cached in + // memory only — the on-disk bootstrap file is intentionally not + // rewritten. + RefreshSandboxToken(context.Context, *RefreshSandboxTokenRequest) (*RefreshSandboxTokenResponse, error) + // Create a workspace. + CreateWorkspace(context.Context, *CreateWorkspaceRequest) (*CreateWorkspaceResponse, error) + // Fetch a workspace by name. + GetWorkspace(context.Context, *GetWorkspaceRequest) (*GetWorkspaceResponse, error) + // List workspaces. + ListWorkspaces(context.Context, *ListWorkspacesRequest) (*ListWorkspacesResponse, error) + // Delete a workspace by name. + DeleteWorkspace(context.Context, *DeleteWorkspaceRequest) (*DeleteWorkspaceResponse, error) + // Add a member to a workspace. + AddWorkspaceMember(context.Context, *AddWorkspaceMemberRequest) (*AddWorkspaceMemberResponse, error) + // Remove a member from a workspace. + RemoveWorkspaceMember(context.Context, *RemoveWorkspaceMemberRequest) (*RemoveWorkspaceMemberResponse, error) + // List members of a workspace. + ListWorkspaceMembers(context.Context, *ListWorkspaceMembersRequest) (*ListWorkspaceMembersResponse, error) + mustEmbedUnimplementedOpenShellServer() +} + +// UnimplementedOpenShellServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedOpenShellServer struct{} + +func (UnimplementedOpenShellServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Health not implemented") +} +func (UnimplementedOpenShellServer) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetCurrentUser not implemented") +} +func (UnimplementedOpenShellServer) GetGatewayInfo(context.Context, *GetGatewayInfoRequest) (*GetGatewayInfoResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetGatewayInfo not implemented") +} +func (UnimplementedOpenShellServer) CreateSandbox(context.Context, *CreateSandboxRequest) (*SandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSandbox not implemented") +} +func (UnimplementedOpenShellServer) BeginRootfsTarStaging(context.Context, *BeginRootfsTarStagingRequest) (*BeginRootfsTarStagingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BeginRootfsTarStaging not implemented") +} +func (UnimplementedOpenShellServer) GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandbox not implemented") +} +func (UnimplementedOpenShellServer) ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSandboxes not implemented") +} +func (UnimplementedOpenShellServer) CreateSandboxTemplate(context.Context, *CreateSandboxTemplateRequest) (*SandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSandboxTemplate not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxTemplate(context.Context, *GetSandboxTemplateRequest) (*SandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxTemplate not implemented") +} +func (UnimplementedOpenShellServer) ListSandboxTemplates(context.Context, *ListSandboxTemplatesRequest) (*ListSandboxTemplatesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSandboxTemplates not implemented") +} +func (UnimplementedOpenShellServer) DeleteSandboxTemplate(context.Context, *DeleteSandboxTemplateRequest) (*DeleteSandboxTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteSandboxTemplate not implemented") +} +func (UnimplementedOpenShellServer) ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSandboxProviders not implemented") +} +func (UnimplementedOpenShellServer) AttachSandboxProvider(context.Context, *AttachSandboxProviderRequest) (*AttachSandboxProviderResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AttachSandboxProvider not implemented") +} +func (UnimplementedOpenShellServer) DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DetachSandboxProvider not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderStatus not implemented") +} +func (UnimplementedOpenShellServer) DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteSandbox not implemented") +} +func (UnimplementedOpenShellServer) StopSandbox(context.Context, *StopSandboxRequest) (*SandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method StopSandbox not implemented") +} +func (UnimplementedOpenShellServer) StartSandbox(context.Context, *StartSandboxRequest) (*SandboxResponse, error) { + return nil, status.Error(codes.Unimplemented, "method StartSandbox not implemented") +} +func (UnimplementedOpenShellServer) CreateSshSession(context.Context, *CreateSshSessionRequest) (*CreateSshSessionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSshSession not implemented") +} +func (UnimplementedOpenShellServer) ExposeService(context.Context, *ExposeServiceRequest) (*ServiceEndpointResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExposeService not implemented") +} +func (UnimplementedOpenShellServer) GetService(context.Context, *GetServiceRequest) (*ServiceEndpointResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetService not implemented") +} +func (UnimplementedOpenShellServer) ListServices(context.Context, *ListServicesRequest) (*ListServicesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListServices not implemented") +} +func (UnimplementedOpenShellServer) DeleteService(context.Context, *DeleteServiceRequest) (*DeleteServiceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteService not implemented") +} +func (UnimplementedOpenShellServer) RevokeSshSession(context.Context, *RevokeSshSessionRequest) (*RevokeSshSessionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RevokeSshSession not implemented") +} +func (UnimplementedOpenShellServer) ExecSandbox(*ExecSandboxRequest, grpc.ServerStreamingServer[ExecSandboxEvent]) error { + return status.Error(codes.Unimplemented, "method ExecSandbox not implemented") +} +func (UnimplementedOpenShellServer) ForwardTcp(grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame]) error { + return status.Error(codes.Unimplemented, "method ForwardTcp not implemented") +} +func (UnimplementedOpenShellServer) ExecSandboxInteractive(grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent]) error { + return status.Error(codes.Unimplemented, "method ExecSandboxInteractive not implemented") +} +func (UnimplementedOpenShellServer) CreateProvider(context.Context, *CreateProviderRequest) (*ProviderResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateProvider not implemented") +} +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") +} +func (UnimplementedOpenShellServer) ListProviderProfiles(context.Context, *ListProviderProfilesRequest) (*ListProviderProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) GetProviderProfile(context.Context, *GetProviderProfileRequest) (*ProviderProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProviderProfile not implemented") +} +func (UnimplementedOpenShellServer) ImportProviderProfiles(context.Context, *ImportProviderProfilesRequest) (*ImportProviderProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ImportProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) UpdateProviderProfiles(context.Context, *UpdateProviderProfilesRequest) (*UpdateProviderProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) LintProviderProfiles(context.Context, *LintProviderProfilesRequest) (*LintProviderProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method LintProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) UpdateProvider(context.Context, *UpdateProviderRequest) (*ProviderResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateProvider not implemented") +} +func (UnimplementedOpenShellServer) GetProviderRefreshStatus(context.Context, *GetProviderRefreshStatusRequest) (*GetProviderRefreshStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProviderRefreshStatus not implemented") +} +func (UnimplementedOpenShellServer) ConfigureProviderRefresh(context.Context, *ConfigureProviderRefreshRequest) (*ConfigureProviderRefreshResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ConfigureProviderRefresh not implemented") +} +func (UnimplementedOpenShellServer) RotateProviderCredential(context.Context, *RotateProviderCredentialRequest) (*RotateProviderCredentialResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RotateProviderCredential not implemented") +} +func (UnimplementedOpenShellServer) DeleteProviderRefresh(context.Context, *DeleteProviderRefreshRequest) (*DeleteProviderRefreshResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteProviderRefresh not implemented") +} +func (UnimplementedOpenShellServer) DeleteProvider(context.Context, *DeleteProviderRequest) (*DeleteProviderResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteProvider not implemented") +} +func (UnimplementedOpenShellServer) DeleteProviderProfile(context.Context, *DeleteProviderProfileRequest) (*DeleteProviderProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteProviderProfile not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxConfig(context.Context, *sandboxv1.GetSandboxConfigRequest) (*sandboxv1.GetSandboxConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxConfig not implemented") +} +func (UnimplementedOpenShellServer) GetGatewayConfig(context.Context, *sandboxv1.GetGatewayConfigRequest) (*sandboxv1.GetGatewayConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetGatewayConfig not implemented") +} +func (UnimplementedOpenShellServer) UpdateConfig(context.Context, *UpdateConfigRequest) (*UpdateConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateConfig not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxPolicyStatus(context.Context, *GetSandboxPolicyStatusRequest) (*GetSandboxPolicyStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxPolicyStatus not implemented") +} +func (UnimplementedOpenShellServer) ListSandboxPolicies(context.Context, *ListSandboxPoliciesRequest) (*ListSandboxPoliciesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSandboxPolicies not implemented") +} +func (UnimplementedOpenShellServer) ReportPolicyStatus(context.Context, *ReportPolicyStatusRequest) (*ReportPolicyStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportPolicyStatus not implemented") +} +func (UnimplementedOpenShellServer) ReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportEndpointStatus not implemented") +} +func (UnimplementedOpenShellServer) ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportProviderReadiness not implemented") +} +func (UnimplementedOpenShellServer) ReportSandboxConfiguration(context.Context, *ReportSandboxConfigurationRequest) (*ReportSandboxConfigurationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportSandboxConfiguration not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderEnvironment not implemented") +} +func (UnimplementedOpenShellServer) ExchangeProviderSubjectToken(context.Context, *ExchangeProviderSubjectTokenRequest) (*ExchangeProviderSubjectTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExchangeProviderSubjectToken not implemented") +} +func (UnimplementedOpenShellServer) GetSandboxLogs(context.Context, *GetSandboxLogsRequest) (*GetSandboxLogsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxLogs not implemented") +} +func (UnimplementedOpenShellServer) PushSandboxLogs(grpc.ClientStreamingServer[PushSandboxLogsRequest, PushSandboxLogsResponse]) error { + return status.Error(codes.Unimplemented, "method PushSandboxLogs not implemented") +} +func (UnimplementedOpenShellServer) ConnectSupervisor(grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage]) error { + return status.Error(codes.Unimplemented, "method ConnectSupervisor not implemented") +} +func (UnimplementedOpenShellServer) ReportMainProcessExit(context.Context, *ReportMainProcessExitRequest) (*ReportMainProcessExitResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportMainProcessExit not implemented") +} +func (UnimplementedOpenShellServer) FinalizeMainProcessExit(context.Context, *FinalizeMainProcessExitRequest) (*FinalizeMainProcessExitResponse, error) { + return nil, status.Error(codes.Unimplemented, "method FinalizeMainProcessExit not implemented") +} +func (UnimplementedOpenShellServer) RelayStream(grpc.BidiStreamingServer[RelayFrame, RelayFrame]) error { + return status.Error(codes.Unimplemented, "method RelayStream not implemented") +} +func (UnimplementedOpenShellServer) PeerRelay(grpc.BidiStreamingServer[PeerRelayFrame, PeerRelayFrame]) error { + return status.Error(codes.Unimplemented, "method PeerRelay not implemented") +} +func (UnimplementedOpenShellServer) PeerReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method PeerReportProviderReadiness not implemented") +} +func (UnimplementedOpenShellServer) PeerReportEndpointStatus(context.Context, *ReportEndpointStatusRequest) (*ReportEndpointStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method PeerReportEndpointStatus not implemented") +} +func (UnimplementedOpenShellServer) PeerGetSandboxProviderStatus(context.Context, *GetSandboxProviderStatusRequest) (*GetSandboxProviderStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method PeerGetSandboxProviderStatus not implemented") +} +func (UnimplementedOpenShellServer) WatchSandbox(*WatchSandboxRequest, grpc.ServerStreamingServer[SandboxStreamEvent]) error { + return status.Error(codes.Unimplemented, "method WatchSandbox not implemented") +} +func (UnimplementedOpenShellServer) SubmitPolicyAnalysis(context.Context, *SubmitPolicyAnalysisRequest) (*SubmitPolicyAnalysisResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitPolicyAnalysis not implemented") +} +func (UnimplementedOpenShellServer) GetDraftPolicy(context.Context, *GetDraftPolicyRequest) (*GetDraftPolicyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetDraftPolicy not implemented") +} +func (UnimplementedOpenShellServer) ApproveDraftChunk(context.Context, *ApproveDraftChunkRequest) (*ApproveDraftChunkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApproveDraftChunk not implemented") +} +func (UnimplementedOpenShellServer) RejectDraftChunk(context.Context, *RejectDraftChunkRequest) (*RejectDraftChunkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RejectDraftChunk not implemented") +} +func (UnimplementedOpenShellServer) ApproveAllDraftChunks(context.Context, *ApproveAllDraftChunksRequest) (*ApproveAllDraftChunksResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApproveAllDraftChunks not implemented") +} +func (UnimplementedOpenShellServer) EditDraftChunk(context.Context, *EditDraftChunkRequest) (*EditDraftChunkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method EditDraftChunk not implemented") +} +func (UnimplementedOpenShellServer) UndoDraftChunk(context.Context, *UndoDraftChunkRequest) (*UndoDraftChunkResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UndoDraftChunk not implemented") +} +func (UnimplementedOpenShellServer) ClearDraftChunks(context.Context, *ClearDraftChunksRequest) (*ClearDraftChunksResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ClearDraftChunks not implemented") +} +func (UnimplementedOpenShellServer) GetDraftHistory(context.Context, *GetDraftHistoryRequest) (*GetDraftHistoryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetDraftHistory not implemented") +} +func (UnimplementedOpenShellServer) IssueSandboxToken(context.Context, *IssueSandboxTokenRequest) (*IssueSandboxTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IssueSandboxToken not implemented") +} +func (UnimplementedOpenShellServer) RefreshSandboxToken(context.Context, *RefreshSandboxTokenRequest) (*RefreshSandboxTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RefreshSandboxToken not implemented") +} +func (UnimplementedOpenShellServer) CreateWorkspace(context.Context, *CreateWorkspaceRequest) (*CreateWorkspaceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateWorkspace not implemented") +} +func (UnimplementedOpenShellServer) GetWorkspace(context.Context, *GetWorkspaceRequest) (*GetWorkspaceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetWorkspace not implemented") +} +func (UnimplementedOpenShellServer) ListWorkspaces(context.Context, *ListWorkspacesRequest) (*ListWorkspacesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListWorkspaces not implemented") +} +func (UnimplementedOpenShellServer) DeleteWorkspace(context.Context, *DeleteWorkspaceRequest) (*DeleteWorkspaceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteWorkspace not implemented") +} +func (UnimplementedOpenShellServer) AddWorkspaceMember(context.Context, *AddWorkspaceMemberRequest) (*AddWorkspaceMemberResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AddWorkspaceMember not implemented") +} +func (UnimplementedOpenShellServer) RemoveWorkspaceMember(context.Context, *RemoveWorkspaceMemberRequest) (*RemoveWorkspaceMemberResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveWorkspaceMember not implemented") +} +func (UnimplementedOpenShellServer) ListWorkspaceMembers(context.Context, *ListWorkspaceMembersRequest) (*ListWorkspaceMembersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListWorkspaceMembers not implemented") +} +func (UnimplementedOpenShellServer) mustEmbedUnimplementedOpenShellServer() {} +func (UnimplementedOpenShellServer) testEmbeddedByValue() {} + +// UnsafeOpenShellServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to OpenShellServer will +// result in compilation errors. +type UnsafeOpenShellServer interface { + mustEmbedUnimplementedOpenShellServer() +} + +func RegisterOpenShellServer(s grpc.ServiceRegistrar, srv OpenShellServer) { + // If the following call panics, it indicates UnimplementedOpenShellServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&OpenShell_ServiceDesc, srv) +} + +func _OpenShell_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).Health(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_Health_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).Health(ctx, req.(*HealthRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetCurrentUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCurrentUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetCurrentUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetCurrentUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetCurrentUser(ctx, req.(*GetCurrentUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetGatewayInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGatewayInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetGatewayInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetGatewayInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetGatewayInfo(ctx, req.(*GetGatewayInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_CreateSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateSandbox(ctx, req.(*CreateSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_BeginRootfsTarStaging_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BeginRootfsTarStagingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).BeginRootfsTarStaging(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_BeginRootfsTarStaging_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).BeginRootfsTarStaging(ctx, req.(*BeginRootfsTarStagingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandbox(ctx, req.(*GetSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListSandboxes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxes(ctx, req.(*ListSandboxesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_CreateSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateSandboxTemplate(ctx, req.(*CreateSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxTemplate(ctx, req.(*GetSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListSandboxTemplates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxTemplatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxTemplates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxTemplates_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxTemplates(ctx, req.(*ListSandboxTemplatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteSandboxTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteSandboxTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteSandboxTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteSandboxTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteSandboxTemplate(ctx, req.(*DeleteSandboxTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListSandboxProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxProvidersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxProviders_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxProviders(ctx, req.(*ListSandboxProvidersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_AttachSandboxProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AttachSandboxProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).AttachSandboxProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_AttachSandboxProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).AttachSandboxProvider(ctx, req.(*AttachSandboxProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DetachSandboxProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DetachSandboxProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DetachSandboxProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DetachSandboxProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DetachSandboxProvider(ctx, req.(*DetachSandboxProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxProviderStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxProviderStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxProviderStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxProviderStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxProviderStatus(ctx, req.(*GetSandboxProviderStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteSandbox(ctx, req.(*DeleteSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_StopSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StopSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).StopSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_StopSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).StopSandbox(ctx, req.(*StopSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_StartSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StartSandboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).StartSandbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_StartSandbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).StartSandbox(ctx, req.(*StartSandboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_CreateSshSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSshSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateSshSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateSshSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateSshSession(ctx, req.(*CreateSshSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ExposeService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExposeServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ExposeService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ExposeService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ExposeService(ctx, req.(*ExposeServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetService(ctx, req.(*GetServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListServicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListServices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListServices_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListServices(ctx, req.(*ListServicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteService(ctx, req.(*DeleteServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RevokeSshSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeSshSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RevokeSshSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RevokeSshSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RevokeSshSession(ctx, req.(*RevokeSshSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ExecSandbox_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ExecSandboxRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(OpenShellServer).ExecSandbox(m, &grpc.GenericServerStream[ExecSandboxRequest, ExecSandboxEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxServer = grpc.ServerStreamingServer[ExecSandboxEvent] + +func _OpenShell_ForwardTcp_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).ForwardTcp(&grpc.GenericServerStream[TcpForwardFrame, TcpForwardFrame]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ForwardTcpServer = grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame] + +func _OpenShell_ExecSandboxInteractive_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).ExecSandboxInteractive(&grpc.GenericServerStream[ExecSandboxInput, ExecSandboxEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxInteractiveServer = grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent] + +func _OpenShell_CreateProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateProvider(ctx, req.(*CreateProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetProvider(ctx, req.(*GetProviderRequest)) + } + 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 { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListProviders_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListProviders(ctx, req.(*ListProvidersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListProviderProfiles(ctx, req.(*ListProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetProviderProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetProviderProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetProviderProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetProviderProfile(ctx, req.(*GetProviderProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ImportProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ImportProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ImportProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ImportProviderProfiles(ctx, req.(*ImportProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_UpdateProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).UpdateProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_UpdateProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).UpdateProviderProfiles(ctx, req.(*UpdateProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_LintProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LintProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).LintProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_LintProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).LintProviderProfiles(ctx, req.(*LintProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_UpdateProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).UpdateProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_UpdateProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).UpdateProvider(ctx, req.(*UpdateProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetProviderRefreshStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderRefreshStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetProviderRefreshStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetProviderRefreshStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetProviderRefreshStatus(ctx, req.(*GetProviderRefreshStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ConfigureProviderRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigureProviderRefreshRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ConfigureProviderRefresh(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ConfigureProviderRefresh_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ConfigureProviderRefresh(ctx, req.(*ConfigureProviderRefreshRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RotateProviderCredential_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RotateProviderCredentialRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RotateProviderCredential(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RotateProviderCredential_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RotateProviderCredential(ctx, req.(*RotateProviderCredentialRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteProviderRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteProviderRefreshRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteProviderRefresh(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteProviderRefresh_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteProviderRefresh(ctx, req.(*DeleteProviderRefreshRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteProvider(ctx, req.(*DeleteProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteProviderProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteProviderProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteProviderProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteProviderProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteProviderProfile(ctx, req.(*DeleteProviderProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(sandboxv1.GetSandboxConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxConfig(ctx, req.(*sandboxv1.GetSandboxConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetGatewayConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(sandboxv1.GetGatewayConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetGatewayConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetGatewayConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetGatewayConfig(ctx, req.(*sandboxv1.GetGatewayConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_UpdateConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).UpdateConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_UpdateConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).UpdateConfig(ctx, req.(*UpdateConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxPolicyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxPolicyStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxPolicyStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxPolicyStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxPolicyStatus(ctx, req.(*GetSandboxPolicyStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListSandboxPolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxPoliciesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxPolicies(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxPolicies_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxPolicies(ctx, req.(*ListSandboxPoliciesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ReportPolicyStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportPolicyStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportPolicyStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportPolicyStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportPolicyStatus(ctx, req.(*ReportPolicyStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ReportEndpointStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportEndpointStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportEndpointStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportEndpointStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportEndpointStatus(ctx, req.(*ReportEndpointStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ReportProviderReadiness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportProviderReadinessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportProviderReadiness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportProviderReadiness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportProviderReadiness(ctx, req.(*ReportProviderReadinessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ReportSandboxConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportSandboxConfigurationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportSandboxConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportSandboxConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportSandboxConfiguration(ctx, req.(*ReportSandboxConfigurationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxProviderEnvironment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxProviderEnvironmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxProviderEnvironment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxProviderEnvironment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxProviderEnvironment(ctx, req.(*GetSandboxProviderEnvironmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ExchangeProviderSubjectToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangeProviderSubjectTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ExchangeProviderSubjectToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ExchangeProviderSubjectToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ExchangeProviderSubjectToken(ctx, req.(*ExchangeProviderSubjectTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetSandboxLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxLogs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxLogs(ctx, req.(*GetSandboxLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_PushSandboxLogs_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).PushSandboxLogs(&grpc.GenericServerStream[PushSandboxLogsRequest, PushSandboxLogsResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_PushSandboxLogsServer = grpc.ClientStreamingServer[PushSandboxLogsRequest, PushSandboxLogsResponse] + +func _OpenShell_ConnectSupervisor_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).ConnectSupervisor(&grpc.GenericServerStream[SupervisorMessage, GatewayMessage]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ConnectSupervisorServer = grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage] + +func _OpenShell_ReportMainProcessExit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportMainProcessExitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ReportMainProcessExit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ReportMainProcessExit_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ReportMainProcessExit(ctx, req.(*ReportMainProcessExitRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_FinalizeMainProcessExit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FinalizeMainProcessExitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).FinalizeMainProcessExit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_FinalizeMainProcessExit_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).FinalizeMainProcessExit(ctx, req.(*FinalizeMainProcessExitRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RelayStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).RelayStream(&grpc.GenericServerStream[RelayFrame, RelayFrame]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_RelayStreamServer = grpc.BidiStreamingServer[RelayFrame, RelayFrame] + +func _OpenShell_PeerRelay_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).PeerRelay(&grpc.GenericServerStream[PeerRelayFrame, PeerRelayFrame]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_PeerRelayServer = grpc.BidiStreamingServer[PeerRelayFrame, PeerRelayFrame] + +func _OpenShell_PeerReportProviderReadiness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportProviderReadinessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).PeerReportProviderReadiness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_PeerReportProviderReadiness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).PeerReportProviderReadiness(ctx, req.(*ReportProviderReadinessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_PeerReportEndpointStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportEndpointStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).PeerReportEndpointStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_PeerReportEndpointStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).PeerReportEndpointStatus(ctx, req.(*ReportEndpointStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_PeerGetSandboxProviderStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxProviderStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).PeerGetSandboxProviderStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_PeerGetSandboxProviderStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).PeerGetSandboxProviderStatus(ctx, req.(*GetSandboxProviderStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_WatchSandbox_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(WatchSandboxRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(OpenShellServer).WatchSandbox(m, &grpc.GenericServerStream[WatchSandboxRequest, SandboxStreamEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_WatchSandboxServer = grpc.ServerStreamingServer[SandboxStreamEvent] + +func _OpenShell_SubmitPolicyAnalysis_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubmitPolicyAnalysisRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).SubmitPolicyAnalysis(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_SubmitPolicyAnalysis_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).SubmitPolicyAnalysis(ctx, req.(*SubmitPolicyAnalysisRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetDraftPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDraftPolicyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetDraftPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetDraftPolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetDraftPolicy(ctx, req.(*GetDraftPolicyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ApproveDraftChunk_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApproveDraftChunkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ApproveDraftChunk(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ApproveDraftChunk_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ApproveDraftChunk(ctx, req.(*ApproveDraftChunkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RejectDraftChunk_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RejectDraftChunkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RejectDraftChunk(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RejectDraftChunk_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RejectDraftChunk(ctx, req.(*RejectDraftChunkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ApproveAllDraftChunks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApproveAllDraftChunksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ApproveAllDraftChunks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ApproveAllDraftChunks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ApproveAllDraftChunks(ctx, req.(*ApproveAllDraftChunksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_EditDraftChunk_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EditDraftChunkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).EditDraftChunk(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_EditDraftChunk_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).EditDraftChunk(ctx, req.(*EditDraftChunkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_UndoDraftChunk_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UndoDraftChunkRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).UndoDraftChunk(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_UndoDraftChunk_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).UndoDraftChunk(ctx, req.(*UndoDraftChunkRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ClearDraftChunks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClearDraftChunksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ClearDraftChunks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ClearDraftChunks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ClearDraftChunks(ctx, req.(*ClearDraftChunksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetDraftHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDraftHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetDraftHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetDraftHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetDraftHistory(ctx, req.(*GetDraftHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_IssueSandboxToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IssueSandboxTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).IssueSandboxToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_IssueSandboxToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).IssueSandboxToken(ctx, req.(*IssueSandboxTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RefreshSandboxToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RefreshSandboxTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RefreshSandboxToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RefreshSandboxToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RefreshSandboxToken(ctx, req.(*RefreshSandboxTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_CreateWorkspace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateWorkspaceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).CreateWorkspace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_CreateWorkspace_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).CreateWorkspace(ctx, req.(*CreateWorkspaceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetWorkspace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkspaceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetWorkspace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetWorkspace_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetWorkspace(ctx, req.(*GetWorkspaceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListWorkspaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWorkspacesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListWorkspaces(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListWorkspaces_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListWorkspaces(ctx, req.(*ListWorkspacesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteWorkspace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWorkspaceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteWorkspace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteWorkspace_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteWorkspace(ctx, req.(*DeleteWorkspaceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_AddWorkspaceMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddWorkspaceMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).AddWorkspaceMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_AddWorkspaceMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).AddWorkspaceMember(ctx, req.(*AddWorkspaceMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RemoveWorkspaceMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveWorkspaceMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RemoveWorkspaceMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RemoveWorkspaceMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RemoveWorkspaceMember(ctx, req.(*RemoveWorkspaceMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListWorkspaceMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWorkspaceMembersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListWorkspaceMembers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListWorkspaceMembers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListWorkspaceMembers(ctx, req.(*ListWorkspaceMembersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// OpenShell_ServiceDesc is the grpc.ServiceDesc for OpenShell service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var OpenShell_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "openshell.v1.OpenShell", + HandlerType: (*OpenShellServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Health", + Handler: _OpenShell_Health_Handler, + }, + { + MethodName: "GetCurrentUser", + Handler: _OpenShell_GetCurrentUser_Handler, + }, + { + MethodName: "GetGatewayInfo", + Handler: _OpenShell_GetGatewayInfo_Handler, + }, + { + MethodName: "CreateSandbox", + Handler: _OpenShell_CreateSandbox_Handler, + }, + { + MethodName: "BeginRootfsTarStaging", + Handler: _OpenShell_BeginRootfsTarStaging_Handler, + }, + { + MethodName: "GetSandbox", + Handler: _OpenShell_GetSandbox_Handler, + }, + { + MethodName: "ListSandboxes", + Handler: _OpenShell_ListSandboxes_Handler, + }, + { + MethodName: "CreateSandboxTemplate", + Handler: _OpenShell_CreateSandboxTemplate_Handler, + }, + { + MethodName: "GetSandboxTemplate", + Handler: _OpenShell_GetSandboxTemplate_Handler, + }, + { + MethodName: "ListSandboxTemplates", + Handler: _OpenShell_ListSandboxTemplates_Handler, + }, + { + MethodName: "DeleteSandboxTemplate", + Handler: _OpenShell_DeleteSandboxTemplate_Handler, + }, + { + MethodName: "ListSandboxProviders", + Handler: _OpenShell_ListSandboxProviders_Handler, + }, + { + MethodName: "AttachSandboxProvider", + Handler: _OpenShell_AttachSandboxProvider_Handler, + }, + { + MethodName: "DetachSandboxProvider", + Handler: _OpenShell_DetachSandboxProvider_Handler, + }, + { + MethodName: "GetSandboxProviderStatus", + Handler: _OpenShell_GetSandboxProviderStatus_Handler, + }, + { + MethodName: "DeleteSandbox", + Handler: _OpenShell_DeleteSandbox_Handler, + }, + { + MethodName: "StopSandbox", + Handler: _OpenShell_StopSandbox_Handler, + }, + { + MethodName: "StartSandbox", + Handler: _OpenShell_StartSandbox_Handler, + }, + { + MethodName: "CreateSshSession", + Handler: _OpenShell_CreateSshSession_Handler, + }, + { + MethodName: "ExposeService", + Handler: _OpenShell_ExposeService_Handler, + }, + { + MethodName: "GetService", + Handler: _OpenShell_GetService_Handler, + }, + { + MethodName: "ListServices", + Handler: _OpenShell_ListServices_Handler, + }, + { + MethodName: "DeleteService", + Handler: _OpenShell_DeleteService_Handler, + }, + { + MethodName: "RevokeSshSession", + Handler: _OpenShell_RevokeSshSession_Handler, + }, + { + MethodName: "CreateProvider", + Handler: _OpenShell_CreateProvider_Handler, + }, + { + MethodName: "GetProvider", + Handler: _OpenShell_GetProvider_Handler, + }, + { + MethodName: "GetProviderCredentials", + Handler: _OpenShell_GetProviderCredentials_Handler, + }, + { + MethodName: "ListProviders", + Handler: _OpenShell_ListProviders_Handler, + }, + { + MethodName: "ListProviderProfiles", + Handler: _OpenShell_ListProviderProfiles_Handler, + }, + { + MethodName: "GetProviderProfile", + Handler: _OpenShell_GetProviderProfile_Handler, + }, + { + MethodName: "ImportProviderProfiles", + Handler: _OpenShell_ImportProviderProfiles_Handler, + }, + { + MethodName: "UpdateProviderProfiles", + Handler: _OpenShell_UpdateProviderProfiles_Handler, + }, + { + MethodName: "LintProviderProfiles", + Handler: _OpenShell_LintProviderProfiles_Handler, + }, + { + MethodName: "UpdateProvider", + Handler: _OpenShell_UpdateProvider_Handler, + }, + { + MethodName: "GetProviderRefreshStatus", + Handler: _OpenShell_GetProviderRefreshStatus_Handler, + }, + { + MethodName: "ConfigureProviderRefresh", + Handler: _OpenShell_ConfigureProviderRefresh_Handler, + }, + { + MethodName: "RotateProviderCredential", + Handler: _OpenShell_RotateProviderCredential_Handler, + }, + { + MethodName: "DeleteProviderRefresh", + Handler: _OpenShell_DeleteProviderRefresh_Handler, + }, + { + MethodName: "DeleteProvider", + Handler: _OpenShell_DeleteProvider_Handler, + }, + { + MethodName: "DeleteProviderProfile", + Handler: _OpenShell_DeleteProviderProfile_Handler, + }, + { + MethodName: "GetSandboxConfig", + Handler: _OpenShell_GetSandboxConfig_Handler, + }, + { + MethodName: "GetGatewayConfig", + Handler: _OpenShell_GetGatewayConfig_Handler, + }, + { + MethodName: "UpdateConfig", + Handler: _OpenShell_UpdateConfig_Handler, + }, + { + MethodName: "GetSandboxPolicyStatus", + Handler: _OpenShell_GetSandboxPolicyStatus_Handler, + }, + { + MethodName: "ListSandboxPolicies", + Handler: _OpenShell_ListSandboxPolicies_Handler, + }, + { + MethodName: "ReportPolicyStatus", + Handler: _OpenShell_ReportPolicyStatus_Handler, + }, + { + MethodName: "ReportEndpointStatus", + Handler: _OpenShell_ReportEndpointStatus_Handler, + }, + { + MethodName: "ReportProviderReadiness", + Handler: _OpenShell_ReportProviderReadiness_Handler, + }, + { + MethodName: "ReportSandboxConfiguration", + Handler: _OpenShell_ReportSandboxConfiguration_Handler, + }, + { + MethodName: "GetSandboxProviderEnvironment", + Handler: _OpenShell_GetSandboxProviderEnvironment_Handler, + }, + { + MethodName: "ExchangeProviderSubjectToken", + Handler: _OpenShell_ExchangeProviderSubjectToken_Handler, + }, + { + MethodName: "GetSandboxLogs", + Handler: _OpenShell_GetSandboxLogs_Handler, + }, + { + MethodName: "ReportMainProcessExit", + Handler: _OpenShell_ReportMainProcessExit_Handler, + }, + { + MethodName: "FinalizeMainProcessExit", + Handler: _OpenShell_FinalizeMainProcessExit_Handler, + }, + { + MethodName: "PeerReportProviderReadiness", + Handler: _OpenShell_PeerReportProviderReadiness_Handler, + }, + { + MethodName: "PeerReportEndpointStatus", + Handler: _OpenShell_PeerReportEndpointStatus_Handler, + }, + { + MethodName: "PeerGetSandboxProviderStatus", + Handler: _OpenShell_PeerGetSandboxProviderStatus_Handler, + }, + { + MethodName: "SubmitPolicyAnalysis", + Handler: _OpenShell_SubmitPolicyAnalysis_Handler, + }, + { + MethodName: "GetDraftPolicy", + Handler: _OpenShell_GetDraftPolicy_Handler, + }, + { + MethodName: "ApproveDraftChunk", + Handler: _OpenShell_ApproveDraftChunk_Handler, + }, + { + MethodName: "RejectDraftChunk", + Handler: _OpenShell_RejectDraftChunk_Handler, + }, + { + MethodName: "ApproveAllDraftChunks", + Handler: _OpenShell_ApproveAllDraftChunks_Handler, + }, + { + MethodName: "EditDraftChunk", + Handler: _OpenShell_EditDraftChunk_Handler, + }, + { + MethodName: "UndoDraftChunk", + Handler: _OpenShell_UndoDraftChunk_Handler, + }, + { + MethodName: "ClearDraftChunks", + Handler: _OpenShell_ClearDraftChunks_Handler, + }, + { + MethodName: "GetDraftHistory", + Handler: _OpenShell_GetDraftHistory_Handler, + }, + { + MethodName: "IssueSandboxToken", + Handler: _OpenShell_IssueSandboxToken_Handler, + }, + { + MethodName: "RefreshSandboxToken", + Handler: _OpenShell_RefreshSandboxToken_Handler, + }, + { + MethodName: "CreateWorkspace", + Handler: _OpenShell_CreateWorkspace_Handler, + }, + { + MethodName: "GetWorkspace", + Handler: _OpenShell_GetWorkspace_Handler, + }, + { + MethodName: "ListWorkspaces", + Handler: _OpenShell_ListWorkspaces_Handler, + }, + { + MethodName: "DeleteWorkspace", + Handler: _OpenShell_DeleteWorkspace_Handler, + }, + { + MethodName: "AddWorkspaceMember", + Handler: _OpenShell_AddWorkspaceMember_Handler, + }, + { + MethodName: "RemoveWorkspaceMember", + Handler: _OpenShell_RemoveWorkspaceMember_Handler, + }, + { + MethodName: "ListWorkspaceMembers", + Handler: _OpenShell_ListWorkspaceMembers_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ExecSandbox", + Handler: _OpenShell_ExecSandbox_Handler, + ServerStreams: true, + }, + { + StreamName: "ForwardTcp", + Handler: _OpenShell_ForwardTcp_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "ExecSandboxInteractive", + Handler: _OpenShell_ExecSandboxInteractive_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "PushSandboxLogs", + Handler: _OpenShell_PushSandboxLogs_Handler, + ClientStreams: true, + }, + { + StreamName: "ConnectSupervisor", + Handler: _OpenShell_ConnectSupervisor_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "RelayStream", + Handler: _OpenShell_RelayStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "PeerRelay", + Handler: _OpenShell_PeerRelay_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "WatchSandbox", + Handler: _OpenShell_WatchSandbox_Handler, + ServerStreams: true, + }, + }, + Metadata: "openshell.proto", +} diff --git a/gen/openshell/optionsv1/options.pb.go b/gen/openshell/optionsv1/options.pb.go new file mode 100644 index 0000000..571d490 --- /dev/null +++ b/gen/openshell/optionsv1/options.pb.go @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: options.proto + +package optionsv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Per-method authorization rule. Consumed at runtime by the gateway's +// 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", "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 + // global_role. + WorkspaceRole string `protobuf:"bytes,2,opt,name=workspace_role,json=workspaceRole,proto3" json:"workspace_role,omitempty"` + // Global role required (checked by middleware via OIDC claims): + // "platform_admin". Mutually exclusive with workspace_role. + GlobalRole string `protobuf:"bytes,3,opt,name=global_role,json=globalRole,proto3" json:"global_role,omitempty"` + // Required OIDC scope on the bearer path (e.g. "sandbox:read"). + Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizationRule) Reset() { + *x = AuthorizationRule{} + mi := &file_options_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizationRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizationRule) ProtoMessage() {} + +func (x *AuthorizationRule) ProtoReflect() protoreflect.Message { + mi := &file_options_proto_msgTypes[0] + 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 AuthorizationRule.ProtoReflect.Descriptor instead. +func (*AuthorizationRule) Descriptor() ([]byte, []int) { + return file_options_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthorizationRule) GetAuthMode() string { + if x != nil { + return x.AuthMode + } + return "" +} + +func (x *AuthorizationRule) GetWorkspaceRole() string { + if x != nil { + return x.WorkspaceRole + } + return "" +} + +func (x *AuthorizationRule) GetGlobalRole() string { + if x != nil { + return x.GlobalRole + } + return "" +} + +func (x *AuthorizationRule) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +var file_options_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.MethodOptions)(nil), + ExtensionType: (*AuthorizationRule)(nil), + Field: 50000, + Name: "openshell.options.v1.authorization", + Tag: "bytes,50000,opt,name=authorization", + Filename: "options.proto", + }, + { + ExtendedType: (*descriptorpb.FieldOptions)(nil), + ExtensionType: (*bool)(nil), + Field: 50001, + Name: "openshell.options.v1.secret", + Tag: "varint,50001,opt,name=secret", + Filename: "options.proto", + }, +} + +// Extension fields to descriptorpb.MethodOptions. +var ( + // Authorization metadata for a gRPC method. + // + // optional openshell.options.v1.AuthorizationRule authorization = 50000; + E_Authorization = &file_options_proto_extTypes[0] +) + +// Extension fields to descriptorpb.FieldOptions. +var ( + // optional bool secret = 50001; + E_Secret = &file_options_proto_extTypes[1] +) + +var File_options_proto protoreflect.FileDescriptor + +const file_options_proto_rawDesc = "" + + "\n" + + "\roptions.proto\x12\x14openshell.options.v1\x1a google/protobuf/descriptor.proto\"\x8e\x01\n" + + "\x11AuthorizationRule\x12\x1b\n" + + "\tauth_mode\x18\x01 \x01(\tR\bauthMode\x12%\n" + + "\x0eworkspace_role\x18\x02 \x01(\tR\rworkspaceRole\x12\x1f\n" + + "\vglobal_role\x18\x03 \x01(\tR\n" + + "globalRole\x12\x14\n" + + "\x05scope\x18\x04 \x01(\tR\x05scope:o\n" + + "\rauthorization\x12\x1e.google.protobuf.MethodOptions\x18І\x03 \x01(\v2'.openshell.options.v1.AuthorizationRuleR\rauthorization:7\n" + + "\x06secret\x12\x1d.google.protobuf.FieldOptions\x18ц\x03 \x01(\bR\x06secretB\xe7\x01\n" + + "\x18com.openshell.options.v1B\fOptionsProtoP\x01ZKgithub.com/agentnameservice/ans-openshell/gen/openshell/optionsv1;optionsv1\xa2\x02\x03OOX\xaa\x02\x14Openshell.Options.V1\xca\x02\x14Openshell\\Options\\V1\xe2\x02 Openshell\\Options\\V1\\GPBMetadata\xea\x02\x16Openshell::Options::V1b\x06proto3" + +var ( + file_options_proto_rawDescOnce sync.Once + file_options_proto_rawDescData []byte +) + +func file_options_proto_rawDescGZIP() []byte { + file_options_proto_rawDescOnce.Do(func() { + file_options_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_options_proto_rawDesc), len(file_options_proto_rawDesc))) + }) + return file_options_proto_rawDescData +} + +var file_options_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_options_proto_goTypes = []any{ + (*AuthorizationRule)(nil), // 0: openshell.options.v1.AuthorizationRule + (*descriptorpb.MethodOptions)(nil), // 1: google.protobuf.MethodOptions + (*descriptorpb.FieldOptions)(nil), // 2: google.protobuf.FieldOptions +} +var file_options_proto_depIdxs = []int32{ + 1, // 0: openshell.options.v1.authorization:extendee -> google.protobuf.MethodOptions + 2, // 1: openshell.options.v1.secret:extendee -> google.protobuf.FieldOptions + 0, // 2: openshell.options.v1.authorization:type_name -> openshell.options.v1.AuthorizationRule + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 2, // [2:3] is the sub-list for extension type_name + 0, // [0:2] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_options_proto_init() } +func file_options_proto_init() { + if File_options_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_options_proto_rawDesc), len(file_options_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 2, + NumServices: 0, + }, + GoTypes: file_options_proto_goTypes, + DependencyIndexes: file_options_proto_depIdxs, + MessageInfos: file_options_proto_msgTypes, + ExtensionInfos: file_options_proto_extTypes, + }.Build() + File_options_proto = out.File + file_options_proto_goTypes = nil + file_options_proto_depIdxs = nil +} diff --git a/gen/openshell/sandboxv1/sandbox.pb.go b/gen/openshell/sandboxv1/sandbox.pb.go new file mode 100644 index 0000000..8dcf0f5 --- /dev/null +++ b/gen/openshell/sandboxv1/sandbox.pb.go @@ -0,0 +1,2628 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc (unknown) +// source: sandbox.proto + +package sandboxv1 + +import ( + datamodelv1 "github.com/agentnameservice/ans-openshell/gen/openshell/datamodelv1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// TLS handling for a network endpoint. +type NetworkTlsMode int32 + +const ( + // Auto-detect TLS and terminate it when inspection is configured. + NetworkTlsMode_NETWORK_TLS_MODE_UNSPECIFIED NetworkTlsMode = 0 + // Disable TLS detection and relay the connection without inspection. + NetworkTlsMode_NETWORK_TLS_MODE_SKIP NetworkTlsMode = 1 + // Rejected by policy validation; use NETWORK_TLS_MODE_UNSPECIFIED. + // + // Deprecated: Marked as deprecated in sandbox.proto. + NetworkTlsMode_NETWORK_TLS_MODE_TERMINATE NetworkTlsMode = 2 + // Rejected by policy validation; use NETWORK_TLS_MODE_UNSPECIFIED. + // + // Deprecated: Marked as deprecated in sandbox.proto. + NetworkTlsMode_NETWORK_TLS_MODE_PASSTHROUGH NetworkTlsMode = 3 +) + +// Enum value maps for NetworkTlsMode. +var ( + NetworkTlsMode_name = map[int32]string{ + 0: "NETWORK_TLS_MODE_UNSPECIFIED", + 1: "NETWORK_TLS_MODE_SKIP", + 2: "NETWORK_TLS_MODE_TERMINATE", + 3: "NETWORK_TLS_MODE_PASSTHROUGH", + } + NetworkTlsMode_value = map[string]int32{ + "NETWORK_TLS_MODE_UNSPECIFIED": 0, + "NETWORK_TLS_MODE_SKIP": 1, + "NETWORK_TLS_MODE_TERMINATE": 2, + "NETWORK_TLS_MODE_PASSTHROUGH": 3, + } +) + +func (x NetworkTlsMode) Enum() *NetworkTlsMode { + p := new(NetworkTlsMode) + *p = x + return p +} + +func (x NetworkTlsMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkTlsMode) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[0].Descriptor() +} + +func (NetworkTlsMode) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[0] +} + +func (x NetworkTlsMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkTlsMode.Descriptor instead. +func (NetworkTlsMode) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{0} +} + +// Enforcement behavior for inspected network traffic. +type NetworkEnforcementMode int32 + +const ( + // Preserve the existing default: log violations and allow traffic. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_UNSPECIFIED NetworkEnforcementMode = 0 + // Deny traffic that does not satisfy the endpoint policy. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_ENFORCE NetworkEnforcementMode = 1 + // Log policy violations and allow the traffic. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_AUDIT NetworkEnforcementMode = 2 +) + +// Enum value maps for NetworkEnforcementMode. +var ( + NetworkEnforcementMode_name = map[int32]string{ + 0: "NETWORK_ENFORCEMENT_MODE_UNSPECIFIED", + 1: "NETWORK_ENFORCEMENT_MODE_ENFORCE", + 2: "NETWORK_ENFORCEMENT_MODE_AUDIT", + } + NetworkEnforcementMode_value = map[string]int32{ + "NETWORK_ENFORCEMENT_MODE_UNSPECIFIED": 0, + "NETWORK_ENFORCEMENT_MODE_ENFORCE": 1, + "NETWORK_ENFORCEMENT_MODE_AUDIT": 2, + } +) + +func (x NetworkEnforcementMode) Enum() *NetworkEnforcementMode { + p := new(NetworkEnforcementMode) + *p = x + return p +} + +func (x NetworkEnforcementMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkEnforcementMode) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[1].Descriptor() +} + +func (NetworkEnforcementMode) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[1] +} + +func (x NetworkEnforcementMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkEnforcementMode.Descriptor instead. +func (NetworkEnforcementMode) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{1} +} + +// Shorthand access policy for an inspected network endpoint. +type NetworkAccessPreset int32 + +const ( + // No access preset; explicit rules may define allowed traffic. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_UNSPECIFIED NetworkAccessPreset = 0 + // Allow read-only operations for the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_READ_ONLY NetworkAccessPreset = 1 + // Allow read and write operations for the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_READ_WRITE NetworkAccessPreset = 2 + // Allow every operation supported by the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_FULL NetworkAccessPreset = 3 +) + +// Enum value maps for NetworkAccessPreset. +var ( + NetworkAccessPreset_name = map[int32]string{ + 0: "NETWORK_ACCESS_PRESET_UNSPECIFIED", + 1: "NETWORK_ACCESS_PRESET_READ_ONLY", + 2: "NETWORK_ACCESS_PRESET_READ_WRITE", + 3: "NETWORK_ACCESS_PRESET_FULL", + } + NetworkAccessPreset_value = map[string]int32{ + "NETWORK_ACCESS_PRESET_UNSPECIFIED": 0, + "NETWORK_ACCESS_PRESET_READ_ONLY": 1, + "NETWORK_ACCESS_PRESET_READ_WRITE": 2, + "NETWORK_ACCESS_PRESET_FULL": 3, + } +) + +func (x NetworkAccessPreset) Enum() *NetworkAccessPreset { + p := new(NetworkAccessPreset) + *p = x + return p +} + +func (x NetworkAccessPreset) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkAccessPreset) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[2].Descriptor() +} + +func (NetworkAccessPreset) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[2] +} + +func (x NetworkAccessPreset) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkAccessPreset.Descriptor instead. +func (NetworkAccessPreset) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{2} +} + +// Scope that currently controls a setting. +type SettingScope int32 + +const ( + SettingScope_SETTING_SCOPE_UNSPECIFIED SettingScope = 0 + SettingScope_SETTING_SCOPE_SANDBOX SettingScope = 1 + SettingScope_SETTING_SCOPE_GLOBAL SettingScope = 2 +) + +// Enum value maps for SettingScope. +var ( + SettingScope_name = map[int32]string{ + 0: "SETTING_SCOPE_UNSPECIFIED", + 1: "SETTING_SCOPE_SANDBOX", + 2: "SETTING_SCOPE_GLOBAL", + } + SettingScope_value = map[string]int32{ + "SETTING_SCOPE_UNSPECIFIED": 0, + "SETTING_SCOPE_SANDBOX": 1, + "SETTING_SCOPE_GLOBAL": 2, + } +) + +func (x SettingScope) Enum() *SettingScope { + p := new(SettingScope) + *p = x + return p +} + +func (x SettingScope) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SettingScope) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[3].Descriptor() +} + +func (SettingScope) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[3] +} + +func (x SettingScope) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SettingScope.Descriptor instead. +func (SettingScope) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{3} +} + +// Source used for the policy payload in GetSandboxConfigResponse. +type PolicySource int32 + +const ( + PolicySource_POLICY_SOURCE_UNSPECIFIED PolicySource = 0 + PolicySource_POLICY_SOURCE_SANDBOX PolicySource = 1 + PolicySource_POLICY_SOURCE_GLOBAL PolicySource = 2 +) + +// Enum value maps for PolicySource. +var ( + PolicySource_name = map[int32]string{ + 0: "POLICY_SOURCE_UNSPECIFIED", + 1: "POLICY_SOURCE_SANDBOX", + 2: "POLICY_SOURCE_GLOBAL", + } + PolicySource_value = map[string]int32{ + "POLICY_SOURCE_UNSPECIFIED": 0, + "POLICY_SOURCE_SANDBOX": 1, + "POLICY_SOURCE_GLOBAL": 2, + } +) + +func (x PolicySource) Enum() *PolicySource { + p := new(PolicySource) + *p = x + return p +} + +func (x PolicySource) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PolicySource) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[4].Descriptor() +} + +func (PolicySource) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[4] +} + +func (x PolicySource) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PolicySource.Descriptor instead. +func (PolicySource) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{4} +} + +// Sandbox security policy configuration. +type SandboxPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Policy version. + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // Filesystem access policy. + Filesystem *FilesystemPolicy `protobuf:"bytes,2,opt,name=filesystem,proto3" json:"filesystem,omitempty"` + // Landlock configuration. + Landlock *LandlockPolicy `protobuf:"bytes,3,opt,name=landlock,proto3" json:"landlock,omitempty"` + // Process execution policy. + Process *ProcessPolicy `protobuf:"bytes,4,opt,name=process,proto3" json:"process,omitempty"` + // Network access policies keyed by name (e.g. "claude_code", "gitlab"). + NetworkPolicies map[string]*NetworkPolicyRule `protobuf:"bytes,5,rep,name=network_policies,json=networkPolicies,proto3" json:"network_policies,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Reusable supervisor middleware configs for network egress, keyed by their + // policy-local names. At most 10 configs are accepted, and at most 10 stages + // can be selected per request. + NetworkMiddlewares map[string]*NetworkMiddlewareConfig `protobuf:"bytes,6,rep,name=network_middlewares,json=networkMiddlewares,proto3" json:"network_middlewares,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxPolicy) Reset() { + *x = SandboxPolicy{} + mi := &file_sandbox_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxPolicy) ProtoMessage() {} + +func (x *SandboxPolicy) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[0] + 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 SandboxPolicy.ProtoReflect.Descriptor instead. +func (*SandboxPolicy) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{0} +} + +func (x *SandboxPolicy) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *SandboxPolicy) GetFilesystem() *FilesystemPolicy { + if x != nil { + return x.Filesystem + } + return nil +} + +func (x *SandboxPolicy) GetLandlock() *LandlockPolicy { + if x != nil { + return x.Landlock + } + return nil +} + +func (x *SandboxPolicy) GetProcess() *ProcessPolicy { + if x != nil { + return x.Process + } + return nil +} + +func (x *SandboxPolicy) GetNetworkPolicies() map[string]*NetworkPolicyRule { + if x != nil { + return x.NetworkPolicies + } + return nil +} + +func (x *SandboxPolicy) GetNetworkMiddlewares() map[string]*NetworkMiddlewareConfig { + if x != nil { + return x.NetworkMiddlewares + } + return nil +} + +// Filesystem access policy. +type FilesystemPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Automatically include the workdir as read-write. + IncludeWorkdir bool `protobuf:"varint,1,opt,name=include_workdir,json=includeWorkdir,proto3" json:"include_workdir,omitempty"` + // Read-only directory allow list. + ReadOnly []string `protobuf:"bytes,2,rep,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + // Read-write directory allow list. + ReadWrite []string `protobuf:"bytes,3,rep,name=read_write,json=readWrite,proto3" json:"read_write,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FilesystemPolicy) Reset() { + *x = FilesystemPolicy{} + mi := &file_sandbox_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FilesystemPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FilesystemPolicy) ProtoMessage() {} + +func (x *FilesystemPolicy) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[1] + 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 FilesystemPolicy.ProtoReflect.Descriptor instead. +func (*FilesystemPolicy) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{1} +} + +func (x *FilesystemPolicy) GetIncludeWorkdir() bool { + if x != nil { + return x.IncludeWorkdir + } + return false +} + +func (x *FilesystemPolicy) GetReadOnly() []string { + if x != nil { + return x.ReadOnly + } + return nil +} + +func (x *FilesystemPolicy) GetReadWrite() []string { + if x != nil { + return x.ReadWrite + } + return nil +} + +// Landlock policy configuration. +type LandlockPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Compatibility mode (e.g. "best_effort", "hard_requirement"). + Compatibility string `protobuf:"bytes,1,opt,name=compatibility,proto3" json:"compatibility,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LandlockPolicy) Reset() { + *x = LandlockPolicy{} + mi := &file_sandbox_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LandlockPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LandlockPolicy) ProtoMessage() {} + +func (x *LandlockPolicy) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[2] + 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 LandlockPolicy.ProtoReflect.Descriptor instead. +func (*LandlockPolicy) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{2} +} + +func (x *LandlockPolicy) GetCompatibility() string { + if x != nil { + return x.Compatibility + } + return "" +} + +// Process execution policy. +type ProcessPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User name to run the sandboxed process as. + RunAsUser string `protobuf:"bytes,1,opt,name=run_as_user,json=runAsUser,proto3" json:"run_as_user,omitempty"` + // Group name to run the sandboxed process as. + RunAsGroup string `protobuf:"bytes,2,opt,name=run_as_group,json=runAsGroup,proto3" json:"run_as_group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProcessPolicy) Reset() { + *x = ProcessPolicy{} + mi := &file_sandbox_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProcessPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProcessPolicy) ProtoMessage() {} + +func (x *ProcessPolicy) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[3] + 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 ProcessPolicy.ProtoReflect.Descriptor instead. +func (*ProcessPolicy) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{3} +} + +func (x *ProcessPolicy) GetRunAsUser() string { + if x != nil { + return x.RunAsUser + } + return "" +} + +func (x *ProcessPolicy) GetRunAsGroup() string { + if x != nil { + return x.RunAsGroup + } + return "" +} + +// A named network access policy rule. +type NetworkPolicyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable name for this policy rule. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Allowed endpoint (host:port) pairs. + Endpoints []*NetworkEndpoint `protobuf:"bytes,2,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + // Allowed binary identities. + Binaries []*NetworkBinary `protobuf:"bytes,3,rep,name=binaries,proto3" json:"binaries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkPolicyRule) Reset() { + *x = NetworkPolicyRule{} + mi := &file_sandbox_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkPolicyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkPolicyRule) ProtoMessage() {} + +func (x *NetworkPolicyRule) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[4] + 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 NetworkPolicyRule.ProtoReflect.Descriptor instead. +func (*NetworkPolicyRule) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{4} +} + +func (x *NetworkPolicyRule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NetworkPolicyRule) GetEndpoints() []*NetworkEndpoint { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *NetworkPolicyRule) GetBinaries() []*NetworkBinary { + if x != nil { + return x.Binaries + } + return nil +} + +// A reusable middleware config selected for admitted egress by host. +type NetworkMiddlewareConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable name for this middleware config. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Built-in middleware name or operator-owned registration name. + Middleware string `protobuf:"bytes,2,opt,name=middleware,proto3" json:"middleware,omitempty"` + // Service-specific configuration. + Config *structpb.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + // Failure behavior: "fail_closed" (default) or "fail_open". + OnError string `protobuf:"bytes,4,opt,name=on_error,json=onError,proto3" json:"on_error,omitempty"` + // Host selector controlling which admitted destinations use this config. + Endpoints *MiddlewareEndpointSelector `protobuf:"bytes,5,opt,name=endpoints,proto3" json:"endpoints,omitempty"` + // Execution order. Values must be unique within a policy; lower values run first. + Order int32 `protobuf:"varint,6,opt,name=order,proto3" json:"order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkMiddlewareConfig) Reset() { + *x = NetworkMiddlewareConfig{} + mi := &file_sandbox_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkMiddlewareConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkMiddlewareConfig) ProtoMessage() {} + +func (x *NetworkMiddlewareConfig) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[5] + 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 NetworkMiddlewareConfig.ProtoReflect.Descriptor instead. +func (*NetworkMiddlewareConfig) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{5} +} + +func (x *NetworkMiddlewareConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *NetworkMiddlewareConfig) GetMiddleware() string { + if x != nil { + return x.Middleware + } + return "" +} + +func (x *NetworkMiddlewareConfig) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +func (x *NetworkMiddlewareConfig) GetOnError() string { + if x != nil { + return x.OnError + } + return "" +} + +func (x *NetworkMiddlewareConfig) GetEndpoints() *MiddlewareEndpointSelector { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *NetworkMiddlewareConfig) GetOrder() int32 { + if x != nil { + return x.Order + } + return 0 +} + +// Host selector controlling which admitted destinations use a middleware config. +type MiddlewareEndpointSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Exact host or DNS glob patterns included in the selection. Include and + // exclude accept at most 32 combined patterns. + Include []string `protobuf:"bytes,1,rep,name=include,proto3" json:"include,omitempty"` + // Exact host or DNS glob patterns removed from the selection. + // Exclusions take precedence over inclusions. + Exclude []string `protobuf:"bytes,2,rep,name=exclude,proto3" json:"exclude,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MiddlewareEndpointSelector) Reset() { + *x = MiddlewareEndpointSelector{} + mi := &file_sandbox_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MiddlewareEndpointSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MiddlewareEndpointSelector) ProtoMessage() {} + +func (x *MiddlewareEndpointSelector) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[6] + 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 MiddlewareEndpointSelector.ProtoReflect.Descriptor instead. +func (*MiddlewareEndpointSelector) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{6} +} + +func (x *MiddlewareEndpointSelector) GetInclude() []string { + if x != nil { + return x.Include + } + return nil +} + +func (x *MiddlewareEndpointSelector) GetExclude() []string { + if x != nil { + return x.Exclude + } + return nil +} + +// Binds a policy endpoint to static credentials from a sandbox provider. +type NetworkCredentialBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the provider attached to this sandbox whose static credentials + // may be resolved for requests admitted by this endpoint. + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkCredentialBinding) Reset() { + *x = NetworkCredentialBinding{} + mi := &file_sandbox_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkCredentialBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkCredentialBinding) ProtoMessage() {} + +func (x *NetworkCredentialBinding) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[7] + 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 NetworkCredentialBinding.ProtoReflect.Descriptor instead. +func (*NetworkCredentialBinding) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{7} +} + +func (x *NetworkCredentialBinding) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +// A network endpoint (host + port) with optional L7 inspection config. +type NetworkEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Hostname or host glob pattern. Exact match is case-insensitive. + // Glob patterns use "." as delimiter: "*.example.com" matches a single + // subdomain label, "**.example.com" matches across labels. + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + // Single port (backwards compat). Use `ports` for multiple ports. + // Mutually exclusive with `ports` — if both are set, `ports` takes precedence. + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + // Endpoint protocol. "tcp" and "" select L4-only handling; "rest", + // "websocket", "graphql", "sql", "json-rpc", and "mcp" select L7 inspection. + Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` + // TLS handling. Unspecified enables automatic detection and termination. + Tls NetworkTlsMode `protobuf:"varint,4,opt,name=tls,proto3,enum=openshell.sandbox.v1.NetworkTlsMode" json:"tls,omitempty"` + // Enforcement mode. Unspecified preserves the audit default. + Enforcement NetworkEnforcementMode `protobuf:"varint,5,opt,name=enforcement,proto3,enum=openshell.sandbox.v1.NetworkEnforcementMode" json:"enforcement,omitempty"` + // Access preset shorthand. Unspecified means no preset. + // Mutually exclusive with rules. + Access NetworkAccessPreset `protobuf:"varint,6,opt,name=access,proto3,enum=openshell.sandbox.v1.NetworkAccessPreset" json:"access,omitempty"` + // Explicit L7 rules (mutually exclusive with access). + Rules []*L7Rule `protobuf:"bytes,7,rep,name=rules,proto3" json:"rules,omitempty"` + // Allowed resolved IP addresses or CIDR ranges for this endpoint. + // When non-empty, the SSRF internal-IP check is replaced by an allowlist check: + // - If host is also set: domain must resolve to an IP in this list. + // - If host is empty: any domain is allowed as long as it resolves to an IP in this list. + // + // Supports exact IPs ("10.0.5.20") and CIDR notation ("10.0.5.0/24"). + // Loopback (127.0.0.0/8) and link-local (169.254.0.0/16) are always blocked + // regardless of this field. + AllowedIps []string `protobuf:"bytes,8,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"` + // Multiple ports. When non-empty, this endpoint covers all listed ports. + // If `port` is set and `ports` is empty, `port` is normalized to `ports: [port]`. + // If both are set, `ports` takes precedence. + Ports []uint32 `protobuf:"varint,9,rep,packed,name=ports,proto3" json:"ports,omitempty"` + // Explicit L7 deny rules. When present, requests matching any deny rule + // are blocked even if they match an allow rule or access preset. + // Deny rules take precedence over allow rules. + DenyRules []*L7DenyRule `protobuf:"bytes,10,rep,name=deny_rules,json=denyRules,proto3" json:"deny_rules,omitempty"` + // When true, percent-encoded '/' (%2F) is preserved in path segments + // rather than rejected by the L7 path canonicalizer. Required for + // upstreams like GitLab that embed %2F in namespaced resource paths. + // Defaults to false (strict). + AllowEncodedSlash bool `protobuf:"varint,11,opt,name=allow_encoded_slash,json=allowEncodedSlash,proto3" json:"allow_encoded_slash,omitempty"` + // GraphQL persisted-query behavior for hash-only/saved-query requests: + // "deny" (default) or "allow_registered". + PersistedQueries string `protobuf:"bytes,12,opt,name=persisted_queries,json=persistedQueries,proto3" json:"persisted_queries,omitempty"` + // Trusted GraphQL persisted-query registry keyed by hash or service-specific ID. + // Only used when persisted_queries is "allow_registered". + GraphqlPersistedQueries map[string]*GraphqlOperation `protobuf:"bytes,13,rep,name=graphql_persisted_queries,json=graphqlPersistedQueries,proto3" json:"graphql_persisted_queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Maximum GraphQL request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + GraphqlMaxBodyBytes uint32 `protobuf:"varint,14,opt,name=graphql_max_body_bytes,json=graphqlMaxBodyBytes,proto3" json:"graphql_max_body_bytes,omitempty"` + // Optional HTTP path glob that scopes this L7 endpoint on shared host:port APIs. + // Example: use path "/graphql" for protocol "graphql" and "/repos/**" for + // protocol "rest" when both surfaces live under api.example.com:443. + // Empty means all paths. + Path string `protobuf:"bytes,15,opt,name=path,proto3" json:"path,omitempty"` + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside client-to-server WebSocket text messages after an allowed HTTP 101 + // upgrade. Defaults to false. + WebsocketCredentialRewrite bool `protobuf:"varint,16,opt,name=websocket_credential_rewrite,json=websocketCredentialRewrite,proto3" json:"websocket_credential_rewrite,omitempty"` + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside supported textual HTTP request bodies before forwarding upstream. + // Defaults to false. + RequestBodyCredentialRewrite bool `protobuf:"varint,17,opt,name=request_body_credential_rewrite,json=requestBodyCredentialRewrite,proto3" json:"request_body_credential_rewrite,omitempty"` + // Internal provenance marker for policy-advisor generated endpoints. + // Advisor-proposed endpoints must not satisfy exact-host SSRF trust unless + // they are converted through an explicit user-authored policy path. + AdvisorProposed bool `protobuf:"varint,18,opt,name=advisor_proposed,json=advisorProposed,proto3" json:"advisor_proposed,omitempty"` + // Proxy-side credential signing mode: "sigv4" for AWS SigV4 re-signing. + // When set, the proxy strips the client's Authorization header and computes + // a fresh SigV4 signature using real credentials from the provider. + CredentialSigning string `protobuf:"bytes,19,opt,name=credential_signing,json=credentialSigning,proto3" json:"credential_signing,omitempty"` + // AWS signing service name override. Required when credential_signing is + // "sigv4" — e.g. "bedrock" for bedrock-runtime endpoints. + SigningService string `protobuf:"bytes,20,opt,name=signing_service,json=signingService,proto3" json:"signing_service,omitempty"` + // AWS region override for SigV4 signing. When set, takes precedence over + // hostname-based region extraction. Required for non-standard endpoints. + SigningRegion string `protobuf:"bytes,21,opt,name=signing_region,json=signingRegion,proto3" json:"signing_region,omitempty"` + // Maximum JSON-RPC-over-HTTP request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + JsonRpcMaxBodyBytes uint32 `protobuf:"varint,22,opt,name=json_rpc_max_body_bytes,json=jsonRpcMaxBodyBytes,proto3" json:"json_rpc_max_body_bytes,omitempty"` + // MCP-only policy and inspection options. Only used when protocol is "mcp". + Mcp *McpOptions `protobuf:"bytes,23,opt,name=mcp,proto3" json:"mcp,omitempty"` + // Explicit binding authority for static credentials from an attached + // endpointless provider profile. Profiles that already define endpoints + // continue to use those profile endpoints as their credential boundary. + CredentialBinding *NetworkCredentialBinding `protobuf:"bytes,24,opt,name=credential_binding,json=credentialBinding,proto3" json:"credential_binding,omitempty"` + // Explicitly permits credential-bearing traffic to use paths that OpenShell + // cannot inspect or rewrite. Defaults to false. This is a security-sensitive + // escape hatch and must be explicitly approved. + AllowUninspectedCredentials bool `protobuf:"varint,25,opt,name=allow_uninspected_credentials,json=allowUninspectedCredentials,proto3" json:"allow_uninspected_credentials,omitempty"` + // Internal gateway-derived marker indicating that this endpoint belongs to + // an attached credentialed provider. User-authored values are ignored. + ProviderCredentialed bool `protobuf:"varint,26,opt,name=provider_credentialed,json=providerCredentialed,proto3" json:"provider_credentialed,omitempty"` + // Internal gateway-derived identity of this endpoint's token-grant authority. + // It binds the provider record and complete profile policy, or the active + // global policy, to this endpoint. User-authored values are ignored. + TokenGrantOwner string `protobuf:"bytes,27,opt,name=token_grant_owner,json=tokenGrantOwner,proto3" json:"token_grant_owner,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkEndpoint) Reset() { + *x = NetworkEndpoint{} + mi := &file_sandbox_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkEndpoint) ProtoMessage() {} + +func (x *NetworkEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[8] + 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 NetworkEndpoint.ProtoReflect.Descriptor instead. +func (*NetworkEndpoint) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{8} +} + +func (x *NetworkEndpoint) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *NetworkEndpoint) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *NetworkEndpoint) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *NetworkEndpoint) GetTls() NetworkTlsMode { + if x != nil { + return x.Tls + } + return NetworkTlsMode_NETWORK_TLS_MODE_UNSPECIFIED +} + +func (x *NetworkEndpoint) GetEnforcement() NetworkEnforcementMode { + if x != nil { + return x.Enforcement + } + return NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_UNSPECIFIED +} + +func (x *NetworkEndpoint) GetAccess() NetworkAccessPreset { + if x != nil { + return x.Access + } + return NetworkAccessPreset_NETWORK_ACCESS_PRESET_UNSPECIFIED +} + +func (x *NetworkEndpoint) GetRules() []*L7Rule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *NetworkEndpoint) GetAllowedIps() []string { + if x != nil { + return x.AllowedIps + } + return nil +} + +func (x *NetworkEndpoint) GetPorts() []uint32 { + if x != nil { + return x.Ports + } + return nil +} + +func (x *NetworkEndpoint) GetDenyRules() []*L7DenyRule { + if x != nil { + return x.DenyRules + } + return nil +} + +func (x *NetworkEndpoint) GetAllowEncodedSlash() bool { + if x != nil { + return x.AllowEncodedSlash + } + return false +} + +func (x *NetworkEndpoint) GetPersistedQueries() string { + if x != nil { + return x.PersistedQueries + } + return "" +} + +func (x *NetworkEndpoint) GetGraphqlPersistedQueries() map[string]*GraphqlOperation { + if x != nil { + return x.GraphqlPersistedQueries + } + return nil +} + +func (x *NetworkEndpoint) GetGraphqlMaxBodyBytes() uint32 { + if x != nil { + return x.GraphqlMaxBodyBytes + } + return 0 +} + +func (x *NetworkEndpoint) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *NetworkEndpoint) GetWebsocketCredentialRewrite() bool { + if x != nil { + return x.WebsocketCredentialRewrite + } + return false +} + +func (x *NetworkEndpoint) GetRequestBodyCredentialRewrite() bool { + if x != nil { + return x.RequestBodyCredentialRewrite + } + return false +} + +func (x *NetworkEndpoint) GetAdvisorProposed() bool { + if x != nil { + return x.AdvisorProposed + } + return false +} + +func (x *NetworkEndpoint) GetCredentialSigning() string { + if x != nil { + return x.CredentialSigning + } + return "" +} + +func (x *NetworkEndpoint) GetSigningService() string { + if x != nil { + return x.SigningService + } + return "" +} + +func (x *NetworkEndpoint) GetSigningRegion() string { + if x != nil { + return x.SigningRegion + } + return "" +} + +func (x *NetworkEndpoint) GetJsonRpcMaxBodyBytes() uint32 { + if x != nil { + return x.JsonRpcMaxBodyBytes + } + return 0 +} + +func (x *NetworkEndpoint) GetMcp() *McpOptions { + if x != nil { + return x.Mcp + } + return nil +} + +func (x *NetworkEndpoint) GetCredentialBinding() *NetworkCredentialBinding { + if x != nil { + return x.CredentialBinding + } + return nil +} + +func (x *NetworkEndpoint) GetAllowUninspectedCredentials() bool { + if x != nil { + return x.AllowUninspectedCredentials + } + return false +} + +func (x *NetworkEndpoint) GetProviderCredentialed() bool { + if x != nil { + return x.ProviderCredentialed + } + return false +} + +func (x *NetworkEndpoint) GetTokenGrantOwner() string { + if x != nil { + return x.TokenGrantOwner + } + return "" +} + +// MCP options are grouped so MCP-specific policy can grow without adding more +// top-level NetworkEndpoint fields. OpenShell owns the supported revision +// profiles instead of treating dependency enums as the policy contract. +// +// Sources: +// - https://modelcontextprotocol.io/specification/2025-03-26/basic/transports +// - https://modelcontextprotocol.io/specification/2025-06-18/basic/transports +// - https://modelcontextprotocol.io/specification/2025-11-25/basic/transports +type McpOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Hardening boundary for tools/call params.name. When unset or true, the + // supervisor enforces the MCP recommended tool-name syntax + // ^[A-Za-z0-9_.-]{1,128}$ before policy evaluation. Set false only for + // compatibility with servers that intentionally use non-recommended names. + // + // Source: + // - https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names + StrictToolNames *bool `protobuf:"varint,1,opt,name=strict_tool_names,json=strictToolNames,proto3,oneof" json:"strict_tool_names,omitempty"` + // Method-layer default for MCP endpoints. When true, OpenShell allows parsed + // MCP-family methods at the method layer unless a tool-name policy narrows + // tools/call. When unset or false, explicit method rules are required. + AllowAllKnownMcpMethods *bool `protobuf:"varint,2,opt,name=allow_all_known_mcp_methods,json=allowAllKnownMcpMethods,proto3,oneof" json:"allow_all_known_mcp_methods,omitempty"` + // Exact MCP protocol revisions accepted by the endpoint's policy schema. + // Authors may omit this field. Because proto3 repeated fields do not retain + // presence, omission reaches protobuf ingress as an empty list. Checked + // normalization materializes OpenShell's pinned default revision + // "2025-11-25" before producing canonical downstream state, which is always + // nonempty. + // + // An explicit nonempty list remains an exact allowlist. Duplicates, moving + // aliases such as "draft" and "latest", and unknown dates are invalid. + // Normalization stores valid values in canonical semantic order. These + // values identify core revisions; they do not enable separate SEP overlays + // or select runtime parsing and forwarding behavior. + Versions []string `protobuf:"bytes,3,rep,name=versions,proto3" json:"versions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *McpOptions) Reset() { + *x = McpOptions{} + mi := &file_sandbox_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *McpOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*McpOptions) ProtoMessage() {} + +func (x *McpOptions) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[9] + 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 McpOptions.ProtoReflect.Descriptor instead. +func (*McpOptions) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{9} +} + +func (x *McpOptions) GetStrictToolNames() bool { + if x != nil && x.StrictToolNames != nil { + return *x.StrictToolNames + } + return false +} + +func (x *McpOptions) GetAllowAllKnownMcpMethods() bool { + if x != nil && x.AllowAllKnownMcpMethods != nil { + return *x.AllowAllKnownMcpMethods + } + return false +} + +func (x *McpOptions) GetVersions() []string { + if x != nil { + return x.Versions + } + return nil +} + +// Trusted GraphQL operation classification. +type GraphqlOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Operation type: "query", "mutation", or "subscription". + OperationType string `protobuf:"bytes,1,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // Operation name, if known. + OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // Root field names selected by the operation. + Fields []string `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GraphqlOperation) Reset() { + *x = GraphqlOperation{} + mi := &file_sandbox_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GraphqlOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GraphqlOperation) ProtoMessage() {} + +func (x *GraphqlOperation) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[10] + 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 GraphqlOperation.ProtoReflect.Descriptor instead. +func (*GraphqlOperation) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{10} +} + +func (x *GraphqlOperation) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *GraphqlOperation) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *GraphqlOperation) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + +// An L7 deny rule that blocks specific requests. +// Mirrors L7Allow — same fields, same matching semantics, inverted effect. +// Deny rules are evaluated after allow rules and take precedence. +type L7DenyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Protocol method: HTTP method (REST/WebSocket), JSON-RPC method name, or + // "*" for any when supported by the protocol. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // URL path glob pattern (REST): "/repos/*/pulls/*/reviews", "**" for any. + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // SQL command (SQL): SELECT, INSERT, etc. or "*" for any. + Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"` + // Query parameter matcher map (REST). + // Same semantics as L7Allow.query. + Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + OperationType string `protobuf:"bytes,5,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // GraphQL operation name glob. "*" matches any operation name. + OperationName string `protobuf:"bytes,6,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // GraphQL root field globs. Deny rules match when any selected root field + // matches any configured glob. + Fields []string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty"` + // MCP params matcher map. Currently only params.name is supported for + // tools/call filtering. Generic protocol "json-rpc" rejects params matchers. + Params map[string]*L7QueryMatcher `protobuf:"bytes,9,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7DenyRule) Reset() { + *x = L7DenyRule{} + mi := &file_sandbox_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7DenyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7DenyRule) ProtoMessage() {} + +func (x *L7DenyRule) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[11] + 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 L7DenyRule.ProtoReflect.Descriptor instead. +func (*L7DenyRule) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{11} +} + +func (x *L7DenyRule) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *L7DenyRule) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *L7DenyRule) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *L7DenyRule) GetQuery() map[string]*L7QueryMatcher { + if x != nil { + return x.Query + } + return nil +} + +func (x *L7DenyRule) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *L7DenyRule) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *L7DenyRule) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + +func (x *L7DenyRule) GetParams() map[string]*L7QueryMatcher { + if x != nil { + return x.Params + } + return nil +} + +// An L7 policy rule (allow-only). +type L7Rule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Allow *L7Allow `protobuf:"bytes,1,opt,name=allow,proto3" json:"allow,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7Rule) Reset() { + *x = L7Rule{} + mi := &file_sandbox_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7Rule) ProtoMessage() {} + +func (x *L7Rule) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[12] + 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 L7Rule.ProtoReflect.Descriptor instead. +func (*L7Rule) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{12} +} + +func (x *L7Rule) GetAllow() *L7Allow { + if x != nil { + return x.Allow + } + return nil +} + +// Allowed action definition for L7 rules. +type L7Allow struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Protocol method: HTTP method (REST/WebSocket), JSON-RPC method name, or + // "*" for any when supported by the protocol. + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // URL path glob pattern (REST): "/repos/**", "**" for any. + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // SQL command (SQL): SELECT, INSERT, etc. or "*" for any. + Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"` + // Query parameter matcher map (REST). + // Key is the decoded query parameter name (case-sensitive). + // Value supports either a single glob (`glob`) or a list (`any`). + Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + OperationType string `protobuf:"bytes,5,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // GraphQL operation name glob. "*" matches any operation name. + OperationName string `protobuf:"bytes,6,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // GraphQL root field globs. Allow rules match only when every selected root + // field matches one of the configured globs. Omit to match all fields. + Fields []string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty"` + // MCP params matcher map. Currently only params.name is supported for + // tools/call filtering. Generic protocol "json-rpc" rejects params matchers. + Params map[string]*L7QueryMatcher `protobuf:"bytes,9,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7Allow) Reset() { + *x = L7Allow{} + mi := &file_sandbox_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7Allow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7Allow) ProtoMessage() {} + +func (x *L7Allow) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[13] + 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 L7Allow.ProtoReflect.Descriptor instead. +func (*L7Allow) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{13} +} + +func (x *L7Allow) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *L7Allow) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *L7Allow) GetCommand() string { + if x != nil { + return x.Command + } + return "" +} + +func (x *L7Allow) GetQuery() map[string]*L7QueryMatcher { + if x != nil { + return x.Query + } + return nil +} + +func (x *L7Allow) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *L7Allow) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *L7Allow) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + +func (x *L7Allow) GetParams() map[string]*L7QueryMatcher { + if x != nil { + return x.Params + } + return nil +} + +// Query value matcher for one query parameter key. +type L7QueryMatcher struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Single glob pattern. + Glob string `protobuf:"bytes,1,opt,name=glob,proto3" json:"glob,omitempty"` + // Any-of glob patterns. + Any []string `protobuf:"bytes,2,rep,name=any,proto3" json:"any,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *L7QueryMatcher) Reset() { + *x = L7QueryMatcher{} + mi := &file_sandbox_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *L7QueryMatcher) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*L7QueryMatcher) ProtoMessage() {} + +func (x *L7QueryMatcher) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[14] + 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 L7QueryMatcher.ProtoReflect.Descriptor instead. +func (*L7QueryMatcher) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{14} +} + +func (x *L7QueryMatcher) GetGlob() string { + if x != nil { + return x.Glob + } + return "" +} + +func (x *L7QueryMatcher) GetAny() []string { + if x != nil { + return x.Any + } + return nil +} + +// A binary identity for network policy matching. +type NetworkBinary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkBinary) Reset() { + *x = NetworkBinary{} + mi := &file_sandbox_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkBinary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkBinary) ProtoMessage() {} + +func (x *NetworkBinary) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[15] + 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 NetworkBinary.ProtoReflect.Descriptor instead. +func (*NetworkBinary) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{15} +} + +func (x *NetworkBinary) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// Request to get sandbox settings by sandbox name. +type GetSandboxConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Workspace scope. Select one named workspace; authenticated sandbox callers may omit it during bootstrap. + WorkspaceScope *datamodelv1.WorkspaceSelector `protobuf:"bytes,3,opt,name=workspace_scope,json=workspaceScope,proto3" json:"workspace_scope,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxConfigRequest) Reset() { + *x = GetSandboxConfigRequest{} + mi := &file_sandbox_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxConfigRequest) ProtoMessage() {} + +func (x *GetSandboxConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[16] + 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 GetSandboxConfigRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxConfigRequest) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{16} +} + +func (x *GetSandboxConfigRequest) GetWorkspaceScope() *datamodelv1.WorkspaceSelector { + if x != nil { + return x.WorkspaceScope + } + return nil +} + +func (x *GetSandboxConfigRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Request to get gateway-global settings. +type GetGatewayConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayConfigRequest) Reset() { + *x = GetGatewayConfigRequest{} + mi := &file_sandbox_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayConfigRequest) ProtoMessage() {} + +func (x *GetGatewayConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[17] + 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 GetGatewayConfigRequest.ProtoReflect.Descriptor instead. +func (*GetGatewayConfigRequest) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{17} +} + +// Response containing gateway-global settings. +type GetGatewayConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-global settings map excluding the reserved policy key. + // Registered keys without a configured value are returned with an empty SettingValue. + Settings map[string]*SettingValue `protobuf:"bytes,1,rep,name=settings,proto3" json:"settings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Monotonically increasing revision for gateway-global settings. + SettingsRevision uint64 `protobuf:"varint,2,opt,name=settings_revision,json=settingsRevision,proto3" json:"settings_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayConfigResponse) Reset() { + *x = GetGatewayConfigResponse{} + mi := &file_sandbox_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayConfigResponse) ProtoMessage() {} + +func (x *GetGatewayConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[18] + 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 GetGatewayConfigResponse.ProtoReflect.Descriptor instead. +func (*GetGatewayConfigResponse) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{18} +} + +func (x *GetGatewayConfigResponse) GetSettings() map[string]*SettingValue { + if x != nil { + return x.Settings + } + return nil +} + +func (x *GetGatewayConfigResponse) GetSettingsRevision() uint64 { + if x != nil { + return x.SettingsRevision + } + return 0 +} + +// Type-aware setting value for sandbox/gateway settings. +type SettingValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Value: + // + // *SettingValue_StringValue + // *SettingValue_BoolValue + // *SettingValue_IntValue + // *SettingValue_BytesValue + Value isSettingValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingValue) Reset() { + *x = SettingValue{} + mi := &file_sandbox_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingValue) ProtoMessage() {} + +func (x *SettingValue) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[19] + 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 SettingValue.ProtoReflect.Descriptor instead. +func (*SettingValue) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{19} +} + +func (x *SettingValue) GetValue() isSettingValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *SettingValue) GetStringValue() string { + if x != nil { + if x, ok := x.Value.(*SettingValue_StringValue); ok { + return x.StringValue + } + } + return "" +} + +func (x *SettingValue) GetBoolValue() bool { + if x != nil { + if x, ok := x.Value.(*SettingValue_BoolValue); ok { + return x.BoolValue + } + } + return false +} + +func (x *SettingValue) GetIntValue() int64 { + if x != nil { + if x, ok := x.Value.(*SettingValue_IntValue); ok { + return x.IntValue + } + } + return 0 +} + +func (x *SettingValue) GetBytesValue() []byte { + if x != nil { + if x, ok := x.Value.(*SettingValue_BytesValue); ok { + return x.BytesValue + } + } + return nil +} + +type isSettingValue_Value interface { + isSettingValue_Value() +} + +type SettingValue_StringValue struct { + StringValue string `protobuf:"bytes,1,opt,name=string_value,json=stringValue,proto3,oneof"` +} + +type SettingValue_BoolValue struct { + BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type SettingValue_IntValue struct { + IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` +} + +type SettingValue_BytesValue struct { + BytesValue []byte `protobuf:"bytes,4,opt,name=bytes_value,json=bytesValue,proto3,oneof"` +} + +func (*SettingValue_StringValue) isSettingValue_Value() {} + +func (*SettingValue_BoolValue) isSettingValue_Value() {} + +func (*SettingValue_IntValue) isSettingValue_Value() {} + +func (*SettingValue_BytesValue) isSettingValue_Value() {} + +// Effective setting value and the scope it was resolved from. +type EffectiveSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *SettingValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Scope SettingScope `protobuf:"varint,2,opt,name=scope,proto3,enum=openshell.sandbox.v1.SettingScope" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EffectiveSetting) Reset() { + *x = EffectiveSetting{} + mi := &file_sandbox_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EffectiveSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EffectiveSetting) ProtoMessage() {} + +func (x *EffectiveSetting) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[20] + 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 EffectiveSetting.ProtoReflect.Descriptor instead. +func (*EffectiveSetting) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{20} +} + +func (x *EffectiveSetting) GetValue() *SettingValue { + if x != nil { + return x.Value + } + return nil +} + +func (x *EffectiveSetting) GetScope() SettingScope { + if x != nil { + return x.Scope + } + return SettingScope_SETTING_SCOPE_UNSPECIFIED +} + +// Response containing effective sandbox settings and policy. +type GetSandboxConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The sandbox policy configuration. + Policy *SandboxPolicy `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + // Current policy version (monotonically increasing per sandbox). + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // SHA-256 hash of the serialized policy payload. + PolicyHash string `protobuf:"bytes,3,opt,name=policy_hash,json=policyHash,proto3" json:"policy_hash,omitempty"` + // Effective settings resolved for this sandbox, excluding the reserved policy key. + // Registered keys without a configured value are returned with an empty EffectiveSetting.value. + Settings map[string]*EffectiveSetting `protobuf:"bytes,4,rep,name=settings,proto3" json:"settings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Fingerprint for effective config (policy + settings). Changes when any effective input changes. + ConfigRevision uint64 `protobuf:"varint,5,opt,name=config_revision,json=configRevision,proto3" json:"config_revision,omitempty"` + // Source of the policy payload for this response. + PolicySource PolicySource `protobuf:"varint,6,opt,name=policy_source,json=policySource,proto3,enum=openshell.sandbox.v1.PolicySource" json:"policy_source,omitempty"` + // When policy_source is GLOBAL, the version of the global policy revision. + // Zero when no global policy is active or when policy_source is SANDBOX. + GlobalPolicyVersion uint32 `protobuf:"varint,7,opt,name=global_policy_version,json=globalPolicyVersion,proto3" json:"global_policy_version,omitempty"` + // Fingerprint for provider credential inputs attached to this sandbox. + // Changes when attached provider names or attached provider records change. + ProviderEnvRevision uint64 `protobuf:"varint,8,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + // Operator-registered supervisor middleware services required by the + // effective policy. Built-in middleware is not included. + SupervisorMiddlewareServices []*SupervisorMiddlewareService `protobuf:"bytes,9,rep,name=supervisor_middleware_services,json=supervisorMiddlewareServices,proto3" json:"supervisor_middleware_services,omitempty"` + // Workspace the sandbox belongs to. Allows the supervisor to learn its + // workspace context for subsequent workspace-scoped RPCs. + Workspace string `protobuf:"bytes,10,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Gateway-configured posture for rejected policy generations. Valid values + // are "fail_closed" and "retain_last_valid". Unknown or empty values must + // be treated as fail_closed by the supervisor. + PolicyValidationFailureMode string `protobuf:"bytes,11,opt,name=policy_validation_failure_mode,json=policyValidationFailureMode,proto3" json:"policy_validation_failure_mode,omitempty"` + // Whether this gateway can mint authenticated extension credentials. + // False also covers older gateways that do not advertise this capability; + // supervisors preserve their legacy unauthenticated connection behavior. + ExtensionAuthenticationEnabled bool `protobuf:"varint,12,opt,name=extension_authentication_enabled,json=extensionAuthenticationEnabled,proto3" json:"extension_authentication_enabled,omitempty"` + // Gateway-owned attachment identity captured with this desired configuration. + // Compare for equality; reattachment invalidates previous installation evidence. + ProviderAttachmentEpoch string `protobuf:"bytes,14,opt,name=provider_attachment_epoch,json=providerAttachmentEpoch,proto3" json:"provider_attachment_epoch,omitempty"` + // True only after validating this complete policy/provider composition. + // Missing (older gateway) is deliberately not admission. + ConfigurationAdmitted bool `protobuf:"varint,13,opt,name=configuration_admitted,json=configurationAdmitted,proto3" json:"configuration_admitted,omitempty"` + // Bounded, credential-free admission diagnostic. Empty for admitted policy. + ConfigurationError string `protobuf:"bytes,16,opt,name=configuration_error,json=configurationError,proto3" json:"configuration_error,omitempty"` + // Registration fence for a new supervisor; capture once and retain on retry. + ConfigurationInstanceId string `protobuf:"bytes,15,opt,name=configuration_instance_id,json=configurationInstanceId,proto3" json:"configuration_instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxConfigResponse) Reset() { + *x = GetSandboxConfigResponse{} + mi := &file_sandbox_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxConfigResponse) ProtoMessage() {} + +func (x *GetSandboxConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[21] + 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 GetSandboxConfigResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxConfigResponse) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{21} +} + +func (x *GetSandboxConfigResponse) GetPolicy() *SandboxPolicy { + if x != nil { + return x.Policy + } + return nil +} + +func (x *GetSandboxConfigResponse) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *GetSandboxConfigResponse) GetPolicyHash() string { + if x != nil { + return x.PolicyHash + } + return "" +} + +func (x *GetSandboxConfigResponse) GetSettings() map[string]*EffectiveSetting { + if x != nil { + return x.Settings + } + return nil +} + +func (x *GetSandboxConfigResponse) GetConfigRevision() uint64 { + if x != nil { + return x.ConfigRevision + } + return 0 +} + +func (x *GetSandboxConfigResponse) GetPolicySource() PolicySource { + if x != nil { + return x.PolicySource + } + return PolicySource_POLICY_SOURCE_UNSPECIFIED +} + +func (x *GetSandboxConfigResponse) GetGlobalPolicyVersion() uint32 { + if x != nil { + return x.GlobalPolicyVersion + } + return 0 +} + +func (x *GetSandboxConfigResponse) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *GetSandboxConfigResponse) GetSupervisorMiddlewareServices() []*SupervisorMiddlewareService { + if x != nil { + return x.SupervisorMiddlewareServices + } + return nil +} + +func (x *GetSandboxConfigResponse) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *GetSandboxConfigResponse) GetPolicyValidationFailureMode() string { + if x != nil { + return x.PolicyValidationFailureMode + } + return "" +} + +func (x *GetSandboxConfigResponse) GetExtensionAuthenticationEnabled() bool { + if x != nil { + return x.ExtensionAuthenticationEnabled + } + return false +} + +func (x *GetSandboxConfigResponse) GetProviderAttachmentEpoch() string { + if x != nil { + return x.ProviderAttachmentEpoch + } + return "" +} + +func (x *GetSandboxConfigResponse) GetConfigurationAdmitted() bool { + if x != nil { + return x.ConfigurationAdmitted + } + return false +} + +func (x *GetSandboxConfigResponse) GetConfigurationError() string { + if x != nil { + return x.ConfigurationError + } + return "" +} + +func (x *GetSandboxConfigResponse) GetConfigurationInstanceId() string { + if x != nil { + return x.ConfigurationInstanceId + } + return "" +} + +// Connection details for one operator-registered supervisor middleware service. +// V1 supports plaintext and server-authenticated TLS gRPC. +type SupervisorMiddlewareService struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Operator-owned registration name used by policy attachments and diagnostics. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // gRPC endpoint reachable from the sandbox supervisor. + GrpcEndpoint string `protobuf:"bytes,2,opt,name=grpc_endpoint,json=grpcEndpoint,proto3" json:"grpc_endpoint,omitempty"` + // Operator-owned logical payload limit applied to every binding exposed by + // the service. This caps HTTP bodies and complete WebSocket messages. + MaxPayloadBytes uint64 `protobuf:"varint,3,opt,name=max_payload_bytes,json=maxPayloadBytes,proto3" json:"max_payload_bytes,omitempty"` + // Default RPC timeout for this service. Absence uses the platform default of + // 500ms. Values must be between 10ms and 30s. + RequestTimeout *durationpb.Duration `protobuf:"bytes,104,opt,name=request_timeout,json=requestTimeout,proto3" json:"request_timeout,omitempty"` + // PEM-encoded trust roots loaded by the gateway from the operator-configured + // tls_ca_cert_path. Empty uses the platform trust store. + TlsCaCertPem []byte `protobuf:"bytes,5,opt,name=tls_ca_cert_pem,json=tlsCaCertPem,proto3" json:"tls_ca_cert_pem,omitempty"` + // Exact JWT audience for this service. The gateway resolves an omitted + // operator value to a kind-scoped audience derived from the registration + // name before sending sandbox config. + Audience string `protobuf:"bytes,6,opt,name=audience,proto3" json:"audience,omitempty"` + // Operator opt-out from extension authentication for this registration. + // When true the service may use a plaintext endpoint and OpenShell attaches + // no bearer credential; supervisors must not request one. Intended only for + // trusted-network development deployments. + AllowInsecureTransport bool `protobuf:"varint,7,opt,name=allow_insecure_transport,json=allowInsecureTransport,proto3" json:"allow_insecure_transport,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SupervisorMiddlewareService) Reset() { + *x = SupervisorMiddlewareService{} + mi := &file_sandbox_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SupervisorMiddlewareService) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SupervisorMiddlewareService) ProtoMessage() {} + +func (x *SupervisorMiddlewareService) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SupervisorMiddlewareService.ProtoReflect.Descriptor instead. +func (*SupervisorMiddlewareService) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{22} +} + +func (x *SupervisorMiddlewareService) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SupervisorMiddlewareService) GetGrpcEndpoint() string { + if x != nil { + return x.GrpcEndpoint + } + return "" +} + +func (x *SupervisorMiddlewareService) GetMaxPayloadBytes() uint64 { + if x != nil { + return x.MaxPayloadBytes + } + return 0 +} + +func (x *SupervisorMiddlewareService) GetRequestTimeout() *durationpb.Duration { + if x != nil { + return x.RequestTimeout + } + return nil +} + +func (x *SupervisorMiddlewareService) GetTlsCaCertPem() []byte { + if x != nil { + return x.TlsCaCertPem + } + return nil +} + +func (x *SupervisorMiddlewareService) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *SupervisorMiddlewareService) GetAllowInsecureTransport() bool { + if x != nil { + return x.AllowInsecureTransport + } + return false +} + +var File_sandbox_proto protoreflect.FileDescriptor + +const file_sandbox_proto_rawDesc = "" + + "\n" + + "\rsandbox.proto\x12\x14openshell.sandbox.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0fdatamodel.proto\"\xa8\x05\n" + + "\rSandboxPolicy\x12\x18\n" + + "\aversion\x18\x01 \x01(\rR\aversion\x12F\n" + + "\n" + + "filesystem\x18\x02 \x01(\v2&.openshell.sandbox.v1.FilesystemPolicyR\n" + + "filesystem\x12@\n" + + "\blandlock\x18\x03 \x01(\v2$.openshell.sandbox.v1.LandlockPolicyR\blandlock\x12=\n" + + "\aprocess\x18\x04 \x01(\v2#.openshell.sandbox.v1.ProcessPolicyR\aprocess\x12c\n" + + "\x10network_policies\x18\x05 \x03(\v28.openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntryR\x0fnetworkPolicies\x12l\n" + + "\x13network_middlewares\x18\x06 \x03(\v2;.openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntryR\x12networkMiddlewares\x1ak\n" + + "\x14NetworkPoliciesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12=\n" + + "\x05value\x18\x02 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\x05value:\x028\x01\x1at\n" + + "\x17NetworkMiddlewaresEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12C\n" + + "\x05value\x18\x02 \x01(\v2-.openshell.sandbox.v1.NetworkMiddlewareConfigR\x05value:\x028\x01\"w\n" + + "\x10FilesystemPolicy\x12'\n" + + "\x0finclude_workdir\x18\x01 \x01(\bR\x0eincludeWorkdir\x12\x1b\n" + + "\tread_only\x18\x02 \x03(\tR\breadOnly\x12\x1d\n" + + "\n" + + "read_write\x18\x03 \x03(\tR\treadWrite\"6\n" + + "\x0eLandlockPolicy\x12$\n" + + "\rcompatibility\x18\x01 \x01(\tR\rcompatibility\"Q\n" + + "\rProcessPolicy\x12\x1e\n" + + "\vrun_as_user\x18\x01 \x01(\tR\trunAsUser\x12 \n" + + "\frun_as_group\x18\x02 \x01(\tR\n" + + "runAsGroup\"\xad\x01\n" + + "\x11NetworkPolicyRule\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12C\n" + + "\tendpoints\x18\x02 \x03(\v2%.openshell.sandbox.v1.NetworkEndpointR\tendpoints\x12?\n" + + "\bbinaries\x18\x03 \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\"\xff\x01\n" + + "\x17NetworkMiddlewareConfig\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + + "\n" + + "middleware\x18\x02 \x01(\tR\n" + + "middleware\x12/\n" + + "\x06config\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x06config\x12\x19\n" + + "\bon_error\x18\x04 \x01(\tR\aonError\x12N\n" + + "\tendpoints\x18\x05 \x01(\v20.openshell.sandbox.v1.MiddlewareEndpointSelectorR\tendpoints\x12\x14\n" + + "\x05order\x18\x06 \x01(\x05R\x05order\"P\n" + + "\x1aMiddlewareEndpointSelector\x12\x18\n" + + "\ainclude\x18\x01 \x03(\tR\ainclude\x12\x18\n" + + "\aexclude\x18\x02 \x03(\tR\aexclude\"6\n" + + "\x18NetworkCredentialBinding\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\"\x87\f\n" + + "\x0fNetworkEndpoint\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\x12\x1a\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\x126\n" + + "\x03tls\x18\x04 \x01(\x0e2$.openshell.sandbox.v1.NetworkTlsModeR\x03tls\x12N\n" + + "\venforcement\x18\x05 \x01(\x0e2,.openshell.sandbox.v1.NetworkEnforcementModeR\venforcement\x12A\n" + + "\x06access\x18\x06 \x01(\x0e2).openshell.sandbox.v1.NetworkAccessPresetR\x06access\x122\n" + + "\x05rules\x18\a \x03(\v2\x1c.openshell.sandbox.v1.L7RuleR\x05rules\x12\x1f\n" + + "\vallowed_ips\x18\b \x03(\tR\n" + + "allowedIps\x12\x14\n" + + "\x05ports\x18\t \x03(\rR\x05ports\x12?\n" + + "\n" + + "deny_rules\x18\n" + + " \x03(\v2 .openshell.sandbox.v1.L7DenyRuleR\tdenyRules\x12.\n" + + "\x13allow_encoded_slash\x18\v \x01(\bR\x11allowEncodedSlash\x12+\n" + + "\x11persisted_queries\x18\f \x01(\tR\x10persistedQueries\x12~\n" + + "\x19graphql_persisted_queries\x18\r \x03(\v2B.openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntryR\x17graphqlPersistedQueries\x123\n" + + "\x16graphql_max_body_bytes\x18\x0e \x01(\rR\x13graphqlMaxBodyBytes\x12\x12\n" + + "\x04path\x18\x0f \x01(\tR\x04path\x12@\n" + + "\x1cwebsocket_credential_rewrite\x18\x10 \x01(\bR\x1awebsocketCredentialRewrite\x12E\n" + + "\x1frequest_body_credential_rewrite\x18\x11 \x01(\bR\x1crequestBodyCredentialRewrite\x12)\n" + + "\x10advisor_proposed\x18\x12 \x01(\bR\x0fadvisorProposed\x12-\n" + + "\x12credential_signing\x18\x13 \x01(\tR\x11credentialSigning\x12'\n" + + "\x0fsigning_service\x18\x14 \x01(\tR\x0esigningService\x12%\n" + + "\x0esigning_region\x18\x15 \x01(\tR\rsigningRegion\x124\n" + + "\x17json_rpc_max_body_bytes\x18\x16 \x01(\rR\x13jsonRpcMaxBodyBytes\x122\n" + + "\x03mcp\x18\x17 \x01(\v2 .openshell.sandbox.v1.McpOptionsR\x03mcp\x12]\n" + + "\x12credential_binding\x18\x18 \x01(\v2..openshell.sandbox.v1.NetworkCredentialBindingR\x11credentialBinding\x12B\n" + + "\x1dallow_uninspected_credentials\x18\x19 \x01(\bR\x1ballowUninspectedCredentials\x123\n" + + "\x15provider_credentialed\x18\x1a \x01(\bR\x14providerCredentialed\x12*\n" + + "\x11token_grant_owner\x18\x1b \x01(\tR\x0ftokenGrantOwner\x1ar\n" + + "\x1cGraphqlPersistedQueriesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.GraphqlOperationR\x05value:\x028\x01\"\xd2\x01\n" + + "\n" + + "McpOptions\x12/\n" + + "\x11strict_tool_names\x18\x01 \x01(\bH\x00R\x0fstrictToolNames\x88\x01\x01\x12A\n" + + "\x1ballow_all_known_mcp_methods\x18\x02 \x01(\bH\x01R\x17allowAllKnownMcpMethods\x88\x01\x01\x12\x1a\n" + + "\bversions\x18\x03 \x03(\tR\bversionsB\x14\n" + + "\x12_strict_tool_namesB\x1e\n" + + "\x1c_allow_all_known_mcp_methods\"x\n" + + "\x10GraphqlOperation\x12%\n" + + "\x0eoperation_type\x18\x01 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x02 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\x03 \x03(\tR\x06fields\"\x88\x04\n" + + "\n" + + "L7DenyRule\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + + "\acommand\x18\x03 \x01(\tR\acommand\x12A\n" + + "\x05query\x18\x04 \x03(\v2+.openshell.sandbox.v1.L7DenyRule.QueryEntryR\x05query\x12%\n" + + "\x0eoperation_type\x18\x05 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x06 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\a \x03(\tR\x06fields\x12D\n" + + "\x06params\x18\t \x03(\v2,.openshell.sandbox.v1.L7DenyRule.ParamsEntryR\x06params\x1a^\n" + + "\n" + + "QueryEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + + "\x05value\x18\x02 \x01(\v2$.openshell.sandbox.v1.L7QueryMatcherR\x05value:\x028\x01\x1a_\n" + + "\vParamsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + + "\x05value\x18\x02 \x01(\v2$.openshell.sandbox.v1.L7QueryMatcherR\x05value:\x028\x01J\x04\b\b\x10\t\"=\n" + + "\x06L7Rule\x123\n" + + "\x05allow\x18\x01 \x01(\v2\x1d.openshell.sandbox.v1.L7AllowR\x05allow\"\xff\x03\n" + + "\aL7Allow\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + + "\acommand\x18\x03 \x01(\tR\acommand\x12>\n" + + "\x05query\x18\x04 \x03(\v2(.openshell.sandbox.v1.L7Allow.QueryEntryR\x05query\x12%\n" + + "\x0eoperation_type\x18\x05 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x06 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\a \x03(\tR\x06fields\x12A\n" + + "\x06params\x18\t \x03(\v2).openshell.sandbox.v1.L7Allow.ParamsEntryR\x06params\x1a^\n" + + "\n" + + "QueryEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + + "\x05value\x18\x02 \x01(\v2$.openshell.sandbox.v1.L7QueryMatcherR\x05value:\x028\x01\x1a_\n" + + "\vParamsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + + "\x05value\x18\x02 \x01(\v2$.openshell.sandbox.v1.L7QueryMatcherR\x05value:\x028\x01J\x04\b\b\x10\t\"6\n" + + "\x0eL7QueryMatcher\x12\x12\n" + + "\x04glob\x18\x01 \x01(\tR\x04glob\x12\x10\n" + + "\x03any\x18\x02 \x03(\tR\x03any\"2\n" + + "\rNetworkBinary\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04pathJ\x04\b\x02\x10\x03R\aharness\"\x81\x01\n" + + "\x17GetSandboxConfigRequest\x12R\n" + + "\x0fworkspace_scope\x18\x03 \x01(\v2).openshell.datamodel.v1.WorkspaceSelectorR\x0eworkspaceScope\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x19\n" + + "\x17GetGatewayConfigRequest\"\x82\x02\n" + + "\x18GetGatewayConfigResponse\x12X\n" + + "\bsettings\x18\x01 \x03(\v2<.openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntryR\bsettings\x12+\n" + + "\x11settings_revision\x18\x02 \x01(\x04R\x10settingsRevision\x1a_\n" + + "\rSettingsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x128\n" + + "\x05value\x18\x02 \x01(\v2\".openshell.sandbox.v1.SettingValueR\x05value:\x028\x01\"\x9f\x01\n" + + "\fSettingValue\x12#\n" + + "\fstring_value\x18\x01 \x01(\tH\x00R\vstringValue\x12\x1f\n" + + "\n" + + "bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12\x1d\n" + + "\tint_value\x18\x03 \x01(\x03H\x00R\bintValue\x12!\n" + + "\vbytes_value\x18\x04 \x01(\fH\x00R\n" + + "bytesValueB\a\n" + + "\x05value\"\x86\x01\n" + + "\x10EffectiveSetting\x128\n" + + "\x05value\x18\x01 \x01(\v2\".openshell.sandbox.v1.SettingValueR\x05value\x128\n" + + "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\xb1\b\n" + + "\x18GetSandboxConfigResponse\x12;\n" + + "\x06policy\x18\x01 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x18\n" + + "\aversion\x18\x02 \x01(\rR\aversion\x12\x1f\n" + + "\vpolicy_hash\x18\x03 \x01(\tR\n" + + "policyHash\x12X\n" + + "\bsettings\x18\x04 \x03(\v2<.openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntryR\bsettings\x12'\n" + + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x12G\n" + + "\rpolicy_source\x18\x06 \x01(\x0e2\".openshell.sandbox.v1.PolicySourceR\fpolicySource\x122\n" + + "\x15global_policy_version\x18\a \x01(\rR\x13globalPolicyVersion\x122\n" + + "\x15provider_env_revision\x18\b \x01(\x04R\x13providerEnvRevision\x12w\n" + + "\x1esupervisor_middleware_services\x18\t \x03(\v21.openshell.sandbox.v1.SupervisorMiddlewareServiceR\x1csupervisorMiddlewareServices\x12\x1c\n" + + "\tworkspace\x18\n" + + " \x01(\tR\tworkspace\x12C\n" + + "\x1epolicy_validation_failure_mode\x18\v \x01(\tR\x1bpolicyValidationFailureMode\x12H\n" + + " extension_authentication_enabled\x18\f \x01(\bR\x1eextensionAuthenticationEnabled\x12:\n" + + "\x19provider_attachment_epoch\x18\x0e \x01(\tR\x17providerAttachmentEpoch\x125\n" + + "\x16configuration_admitted\x18\r \x01(\bR\x15configurationAdmitted\x12/\n" + + "\x13configuration_error\x18\x10 \x01(\tR\x12configurationError\x12:\n" + + "\x19configuration_instance_id\x18\x0f \x01(\tR\x17configurationInstanceId\x1ac\n" + + "\rSettingsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.EffectiveSettingR\x05value:\x028\x01\"\xd2\x02\n" + + "\x1bSupervisorMiddlewareService\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rgrpc_endpoint\x18\x02 \x01(\tR\fgrpcEndpoint\x12*\n" + + "\x11max_payload_bytes\x18\x03 \x01(\x04R\x0fmaxPayloadBytes\x12B\n" + + "\x0frequest_timeout\x18h \x01(\v2\x19.google.protobuf.DurationR\x0erequestTimeout\x12%\n" + + "\x0ftls_ca_cert_pem\x18\x05 \x01(\fR\ftlsCaCertPem\x12\x1a\n" + + "\baudience\x18\x06 \x01(\tR\baudience\x128\n" + + "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransportJ\x04\b\x04\x10\x05R\atimeout*\x97\x01\n" + + "\x0eNetworkTlsMode\x12 \n" + + "\x1cNETWORK_TLS_MODE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15NETWORK_TLS_MODE_SKIP\x10\x01\x12\"\n" + + "\x1aNETWORK_TLS_MODE_TERMINATE\x10\x02\x1a\x02\b\x01\x12$\n" + + "\x1cNETWORK_TLS_MODE_PASSTHROUGH\x10\x03\x1a\x02\b\x01*\x8c\x01\n" + + "\x16NetworkEnforcementMode\x12(\n" + + "$NETWORK_ENFORCEMENT_MODE_UNSPECIFIED\x10\x00\x12$\n" + + " NETWORK_ENFORCEMENT_MODE_ENFORCE\x10\x01\x12\"\n" + + "\x1eNETWORK_ENFORCEMENT_MODE_AUDIT\x10\x02*\xa7\x01\n" + + "\x13NetworkAccessPreset\x12%\n" + + "!NETWORK_ACCESS_PRESET_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fNETWORK_ACCESS_PRESET_READ_ONLY\x10\x01\x12$\n" + + " NETWORK_ACCESS_PRESET_READ_WRITE\x10\x02\x12\x1e\n" + + "\x1aNETWORK_ACCESS_PRESET_FULL\x10\x03*b\n" + + "\fSettingScope\x12\x1d\n" + + "\x19SETTING_SCOPE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15SETTING_SCOPE_SANDBOX\x10\x01\x12\x18\n" + + "\x14SETTING_SCOPE_GLOBAL\x10\x02*b\n" + + "\fPolicySource\x12\x1d\n" + + "\x19POLICY_SOURCE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15POLICY_SOURCE_SANDBOX\x10\x01\x12\x18\n" + + "\x14POLICY_SOURCE_GLOBAL\x10\x02B\xe7\x01\n" + + "\x18com.openshell.sandbox.v1B\fSandboxProtoP\x01ZKgithub.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1;sandboxv1\xa2\x02\x03OSX\xaa\x02\x14Openshell.Sandbox.V1\xca\x02\x14Openshell\\Sandbox\\V1\xe2\x02 Openshell\\Sandbox\\V1\\GPBMetadata\xea\x02\x16Openshell::Sandbox::V1b\x06proto3" + +var ( + file_sandbox_proto_rawDescOnce sync.Once + file_sandbox_proto_rawDescData []byte +) + +func file_sandbox_proto_rawDescGZIP() []byte { + file_sandbox_proto_rawDescOnce.Do(func() { + file_sandbox_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc))) + }) + return file_sandbox_proto_rawDescData +} + +var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_sandbox_proto_goTypes = []any{ + (NetworkTlsMode)(0), // 0: openshell.sandbox.v1.NetworkTlsMode + (NetworkEnforcementMode)(0), // 1: openshell.sandbox.v1.NetworkEnforcementMode + (NetworkAccessPreset)(0), // 2: openshell.sandbox.v1.NetworkAccessPreset + (SettingScope)(0), // 3: openshell.sandbox.v1.SettingScope + (PolicySource)(0), // 4: openshell.sandbox.v1.PolicySource + (*SandboxPolicy)(nil), // 5: openshell.sandbox.v1.SandboxPolicy + (*FilesystemPolicy)(nil), // 6: openshell.sandbox.v1.FilesystemPolicy + (*LandlockPolicy)(nil), // 7: openshell.sandbox.v1.LandlockPolicy + (*ProcessPolicy)(nil), // 8: openshell.sandbox.v1.ProcessPolicy + (*NetworkPolicyRule)(nil), // 9: openshell.sandbox.v1.NetworkPolicyRule + (*NetworkMiddlewareConfig)(nil), // 10: openshell.sandbox.v1.NetworkMiddlewareConfig + (*MiddlewareEndpointSelector)(nil), // 11: openshell.sandbox.v1.MiddlewareEndpointSelector + (*NetworkCredentialBinding)(nil), // 12: openshell.sandbox.v1.NetworkCredentialBinding + (*NetworkEndpoint)(nil), // 13: openshell.sandbox.v1.NetworkEndpoint + (*McpOptions)(nil), // 14: openshell.sandbox.v1.McpOptions + (*GraphqlOperation)(nil), // 15: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 16: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 17: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 18: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 19: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 20: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 21: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 22: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 23: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 24: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 25: openshell.sandbox.v1.EffectiveSetting + (*GetSandboxConfigResponse)(nil), // 26: openshell.sandbox.v1.GetSandboxConfigResponse + (*SupervisorMiddlewareService)(nil), // 27: openshell.sandbox.v1.SupervisorMiddlewareService + nil, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 29: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + nil, // 30: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 31: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 32: openshell.sandbox.v1.L7DenyRule.ParamsEntry + nil, // 33: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 34: openshell.sandbox.v1.L7Allow.ParamsEntry + nil, // 35: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 36: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*structpb.Struct)(nil), // 37: google.protobuf.Struct + (*datamodelv1.WorkspaceSelector)(nil), // 38: openshell.datamodel.v1.WorkspaceSelector + (*durationpb.Duration)(nil), // 39: google.protobuf.Duration +} +var file_sandbox_proto_depIdxs = []int32{ + 6, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy + 7, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy + 8, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy + 28, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + 29, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + 13, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 20, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 37, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct + 11, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector + 0, // 9: openshell.sandbox.v1.NetworkEndpoint.tls:type_name -> openshell.sandbox.v1.NetworkTlsMode + 1, // 10: openshell.sandbox.v1.NetworkEndpoint.enforcement:type_name -> openshell.sandbox.v1.NetworkEnforcementMode + 2, // 11: openshell.sandbox.v1.NetworkEndpoint.access:type_name -> openshell.sandbox.v1.NetworkAccessPreset + 17, // 12: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule + 16, // 13: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 30, // 14: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + 14, // 15: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions + 12, // 16: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding + 31, // 17: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry + 32, // 18: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry + 18, // 19: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow + 33, // 20: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry + 34, // 21: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry + 38, // 22: openshell.sandbox.v1.GetSandboxConfigRequest.workspace_scope:type_name -> openshell.datamodel.v1.WorkspaceSelector + 35, // 23: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 24, // 24: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 3, // 25: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope + 5, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 36, // 27: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 4, // 28: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 27, // 29: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 39, // 30: openshell.sandbox.v1.SupervisorMiddlewareService.request_timeout:type_name -> google.protobuf.Duration + 9, // 31: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 10, // 32: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 15, // 33: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 19, // 34: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 35: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 36: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 37: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 24, // 38: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 25, // 39: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 40, // [40:40] is the sub-list for method output_type + 40, // [40:40] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name +} + +func init() { file_sandbox_proto_init() } +func file_sandbox_proto_init() { + if File_sandbox_proto != nil { + return + } + file_sandbox_proto_msgTypes[9].OneofWrappers = []any{} + file_sandbox_proto_msgTypes[19].OneofWrappers = []any{ + (*SettingValue_StringValue)(nil), + (*SettingValue_BoolValue)(nil), + (*SettingValue_IntValue)(nil), + (*SettingValue_BytesValue)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc)), + NumEnums: 5, + NumMessages: 32, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_sandbox_proto_goTypes, + DependencyIndexes: file_sandbox_proto_depIdxs, + EnumInfos: file_sandbox_proto_enumTypes, + MessageInfos: file_sandbox_proto_msgTypes, + }.Build() + File_sandbox_proto = out.File + file_sandbox_proto_goTypes = nil + file_sandbox_proto_depIdxs = nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..216b66b --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/agentnameservice/ans-openshell + +go 1.26.0 + +require ( + github.com/agentnameservice/ans-sdk-go v0.1.19 + github.com/fxamacker/cbor/v2 v2.9.4 + google.golang.org/grpc v1.84.0 + google.golang.org/protobuf v1.36.12 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/kr/pretty v0.2.1 // indirect + github.com/miekg/dns v1.1.73 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d6fe220 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/agentnameservice/ans-sdk-go v0.1.19 h1:NO36m/kwOiYejf4EJtlaWPBfOTEyTpgKHfQTi8cTJg8= +github.com/agentnameservice/ans-sdk-go v0.1.19/go.mod h1:MCXchxxnwdiISwpbufKqAxeE8oK1puD3tnTDjxb/Jw0= +github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo= +github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/callee/callee.go b/internal/callee/callee.go new file mode 100644 index 0000000..aba9fd1 --- /dev/null +++ b/internal/callee/callee.go @@ -0,0 +1,149 @@ +// Package callee is a reference ANS-6 Method B callee: an HTTP service that +// authenticates callers with the SDK's pop.Middleware and reports the proven +// identity. It exists to show, end to end, that a request signed by the +// OpenShell middleware verifies at an ordinary agent endpoint. +package callee + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "strings" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" +) + +// Config wires a Handler. +type Config struct { + // RootKeys are the transparency log's trusted signing keys in C2SP format. + RootKeys []string + // TrustedHosts are the externally visible authorities this callee answers + // on, the authority the proof's htu must name (pop.WithTrustedHosts). + TrustedHosts []string + // MaxReplayEntries bounds the single-use proof cache; zero selects the SDK + // default. + MaxReplayEntries int + // AllowedAnsNames, when set, pins the callers this callee accepts. + AllowedAnsNames []string +} + +// Response is the JSON body returned to an authenticated caller. +type Response struct { + Caller string `json:"caller"` + AgentID string `json:"agentId"` + Fingerprint string `json:"fingerprint"` + Method string `json:"method"` + Path string `json:"path"` + BodyBytes int64 `json:"bodyBytes"` +} + +// ReadRootKeys parses newline-delimited C2SP keys, skipping blank lines and +// lines starting with '#'. +func ReadRootKeys(path string) ([]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open root keys: %w", err) + } + defer f.Close() + return ParseRootKeys(f) +} + +// ParseRootKeys reads newline-delimited C2SP keys from r. +func ParseRootKeys(r io.Reader) ([]string, error) { + var keys []string + sc := bufio.NewScanner(r) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + keys = append(keys, line) + } + if err := sc.Err(); err != nil { + return nil, fmt.Errorf("read root keys: %w", err) + } + if len(keys) == 0 { + return nil, errors.New("no root keys found") + } + return keys, nil +} + +// Handler authenticates every request and echoes the caller identity. +type Handler struct { + chain http.Handler + stop func() + logger *slog.Logger +} + +// New builds a Handler. Close releases the replay cache. +func New(ctx context.Context, cfg Config, logger *slog.Logger) (*Handler, error) { + if len(cfg.RootKeys) == 0 { + return nil, errors.New("callee: at least one transparency log root key is required") + } + if len(cfg.TrustedHosts) == 0 { + return nil, errors.New("callee: at least one trusted host is required") + } + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + keys, err := scitt.NewKeyStore(cfg.RootKeys) + if err != nil { + return nil, fmt.Errorf("callee: root keys: %w", err) + } + replayCtx, cancel := context.WithCancel(ctx) + replay := pop.NewMemoryReplayCache(replayCtx, cfg.MaxReplayEntries) + opts := []pop.MiddlewareOption{ + pop.WithTrustedHosts(cfg.TrustedHosts...), + pop.WithMiddlewareLogger(logger), + } + if len(cfg.AllowedAnsNames) > 0 { + opts = append(opts, pop.WithMiddlewareCallerOptions(pop.WithAllowedAnsNames(cfg.AllowedAnsNames...))) + } + h := &Handler{ + stop: func() { + cancel() + replay.Close() + }, + logger: logger, + } + h.chain = pop.Middleware(keys, replay, opts...)(http.HandlerFunc(h.echo)) + return h, nil +} + +// ServeHTTP implements http.Handler. +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.chain.ServeHTTP(w, r) +} + +// Close releases background resources. +func (h *Handler) Close() { h.stop() } + +// echo runs behind pop.Middleware, which only calls it with a verified caller +// in the context. +func (h *Handler) echo(w http.ResponseWriter, r *http.Request) { + id, _ := pop.CallerFromContext(r.Context()) + n, err := io.Copy(io.Discard, r.Body) + if err != nil { + http.Error(w, "unreadable body", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + resp := Response{ + Caller: id.AnsName, + AgentID: id.AgentID, + Fingerprint: id.FingerprintHex(), + Method: r.Method, + Path: r.URL.Path, + BodyBytes: n, + } + if err := json.NewEncoder(w).Encode(resp); err != nil { + h.logger.WarnContext(r.Context(), "encode response", "err", err) + } +} diff --git a/internal/callee/callee_test.go b/internal/callee/callee_test.go new file mode 100644 index 0000000..d499df0 --- /dev/null +++ b/internal/callee/callee_test.go @@ -0,0 +1,199 @@ +package callee + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +// c2spKey renders a fresh P-256 public key in the C2SP form the SDK's key +// store accepts: name+hex(SHA-256(SPKI)[:4])+base64(SPKI). +func c2spKey(t *testing.T) string { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + spki, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(spki) + return "tl.example+" + hex.EncodeToString(sum[:4]) + "+" + base64.StdEncoding.EncodeToString(spki) +} + +func TestParseRootKeys(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in string + want int + wantErr bool + }{ + {name: "two keys with comments", in: "# comment\nkey-one\n\n key-two \n", want: 2}, + {name: "empty", in: "\n# only a comment\n", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := ParseRootKeys(strings.NewReader(tt.in)) + if (err != nil) != tt.wantErr { + t.Fatalf("ParseRootKeys() error = %v, wantErr %v", err, tt.wantErr) + } + if len(got) != tt.want { + t.Fatalf("ParseRootKeys() = %v, want %d keys", got, tt.want) + } + }) + } +} + +func TestReadRootKeys(t *testing.T) { + t.Parallel() + path := filepath.Join(t.TempDir(), "keys.txt") + if err := os.WriteFile(path, []byte("k1\nk2\n"), 0o600); err != nil { + t.Fatal(err) + } + keys, err := ReadRootKeys(path) + if err != nil || len(keys) != 2 { + t.Fatalf("ReadRootKeys() = %v, %v", keys, err) + } + if _, err := ReadRootKeys(filepath.Join(t.TempDir(), "missing")); err == nil { + t.Fatal("ReadRootKeys() = nil error for a missing file") + } +} + +func TestNew_Validation(t *testing.T) { + t.Parallel() + tests := []struct { + name string + cfg Config + }{ + {name: "no root keys", cfg: Config{TrustedHosts: []string{"a"}}}, + {name: "no trusted hosts", cfg: Config{RootKeys: []string{c2spKey(t)}}}, + {name: "unparseable root key", cfg: Config{RootKeys: []string{"not-a-key"}, TrustedHosts: []string{"a"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if _, err := New(context.Background(), tt.cfg, nil); err == nil { + t.Fatal("New() = nil error") + } + }) + } +} + +func TestHandler_RejectsUnsignedRequest(t *testing.T) { + t.Parallel() + h, err := New(context.Background(), Config{ + RootKeys: []string{c2spKey(t)}, + TrustedHosts: []string{"callee.example"}, + AllowedAnsNames: []string{"ans://v1.0.0.agent.example"}, + }, slog.New(slog.DiscardHandler)) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer h.Close() + srv := httptest.NewServer(h) + defer srv.Close() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL+"/v1/tasks", nil) + req.Host = "callee.example" + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("status = %d, want 401", resp.StatusCode) + } + if got := resp.Header.Get("WWW-Authenticate"); got != "DPoP" { + t.Fatalf("WWW-Authenticate = %q, want DPoP", got) + } +} + +func TestHandler_AcceptsSignedRequest(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity("agent-123", "ans://v1.0.0.agent.example") + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + authority := lis.Addr().String() + h, err := New(context.Background(), Config{RootKeys: []string{tl.RootKeyC2SP()}, TrustedHosts: []string{authority}}, + slog.New(slog.DiscardHandler)) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer h.Close() + srv := &http.Server{Handler: h, ReadHeaderTimeout: time.Second} + go func() { _ = srv.Serve(lis) }() + defer srv.Close() + + signer, err := pop.NewSigner(id.Key, id.CertDER) + if err != nil { + t.Fatal(err) + } + receipt, token := testfixtures.StaticArtifacts(tl, id) + sign := func(body string) *http.Request { + req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://"+authority+"/v1/tasks", + strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + if attachErr := pop.AttachIdentity(req, signer, http.Header{ + scitt.HeaderReceipt: {receipt}, + scitt.HeaderStatusToken: {token}, + }); attachErr != nil { + t.Fatal(attachErr) + } + return req + } + + resp, err := http.DefaultClient.Do(sign(`{"task":"hello"}`)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + var got Response + if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { + t.Fatal(err) + } + if got.Caller != id.AnsName || got.AgentID != id.AgentID || got.BodyBytes != int64(len(`{"task":"hello"}`)) || + got.Method != http.MethodPost || got.Path != "/v1/tasks" { + t.Fatalf("response = %+v", got) + } + + tampered := sign(`{"task":"hello"}`) + tampered.Body = io.NopCloser(strings.NewReader(`{"task":"evil!"}`)) + tampered.ContentLength = int64(len(`{"task":"evil!"}`)) + resp2, err := http.DefaultClient.Do(tampered) + if err != nil { + t.Fatal(err) + } + defer resp2.Body.Close() + if resp2.StatusCode != http.StatusUnauthorized { + t.Fatalf("tampered body status = %d, want 401", resp2.StatusCode) + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5aedd7d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,139 @@ +// Package config holds the configuration blocks shared by the extension +// binaries and loads them from YAML. +package config + +import ( + "bytes" + "errors" + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// TLS configures the extension's own gRPC listener certificate. OpenShell +// verifies it with the registration's tls_ca_cert_path. +type TLS struct { + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` +} + +// Enabled reports whether a certificate is configured. +func (t TLS) Enabled() bool { return t.CertFile != "" || t.KeyFile != "" } + +// Validate checks that both halves of the key pair are present when either is. +func (t TLS) Validate() error { + if t.Enabled() && (t.CertFile == "" || t.KeyFile == "") { + return errors.New("tls: cert_file and key_file must both be set") + } + return nil +} + +// Auth configures verification of the JWT OpenShell presents on every call. +type Auth struct { + // GatewayIssuer is the exact iss claim, "openshell-gateway:". + GatewayIssuer string `yaml:"gateway_issuer"` + // Audience is the exact aud claim configured on the OpenShell registration, + // by default "urn:openshell:extension:middleware:" or + // "urn:openshell:extension:interceptor:". + Audience string `yaml:"audience"` + // JWKSFile is the operator-provisioned gateway signing key set, the + // cold-start trust anchor. + JWKSFile string `yaml:"jwks_file"` + // JWKSURL is the gateway's /.well-known/jwks.json for steady-state key + // refresh. Optional; keys from the file remain trusted. + JWKSURL string `yaml:"jwks_url"` + // JWKSRefreshInterval bounds how often JWKSURL is polled. Zero selects the + // package default. + JWKSRefreshInterval time.Duration `yaml:"jwks_refresh_interval"` + // AllowInsecureNoAuth disables JWT verification. It matches an OpenShell + // registration with allow_insecure_transport = true and is for local + // development only: without it, anyone who can reach the service can have + // proofs minted in the agent's name. + AllowInsecureNoAuth bool `yaml:"allow_insecure_no_auth"` +} + +// Validate checks the block is usable: either insecure mode is explicit or an +// issuer, audience and at least one key source are set. +func (a Auth) Validate() error { + if a.AllowInsecureNoAuth { + return nil + } + var missing []string + if a.GatewayIssuer == "" { + missing = append(missing, "gateway_issuer") + } + if a.Audience == "" { + missing = append(missing, "audience") + } + if a.JWKSFile == "" && a.JWKSURL == "" { + missing = append(missing, "jwks_file or jwks_url") + } + if len(missing) > 0 { + return fmt.Errorf("auth: missing %v (or set allow_insecure_no_auth: true for local development)", missing) + } + return nil +} + +// TransparencyLog configures the ANS transparency log the identities' +// receipts and status tokens are fetched from. +type TransparencyLog struct { + BaseURL string `yaml:"base_url"` + // RootKeys are the log's trusted signing keys in C2SP format. RootKeysFile + // may supply them one per line instead of, or in addition to, RootKeys. + RootKeys []string `yaml:"root_keys"` + RootKeysFile string `yaml:"root_keys_file"` + // AllowInsecureTransport permits an http:// base URL for local development. + AllowInsecureTransport bool `yaml:"allow_insecure_transport"` +} + +// Validate checks a base URL is set. +func (t TransparencyLog) Validate() error { + if t.BaseURL == "" { + return errors.New("transparency_log: base_url is required") + } + return nil +} + +// Vetting configures destination vetting for the middleware and interceptor. +type Vetting struct { + // TrustedTLDomains is the allowlist of transparency-log hosts an + // _ans-badge record may point at. It is the trust root of the badge tier + // and must come from operator configuration, never from DNS. + TrustedTLDomains []string `yaml:"trusted_tl_domains"` + // CacheTTL is how long a verdict is served without a fresh lookup. + CacheTTL time.Duration `yaml:"cache_ttl"` + // RequestBudget is the longest the middleware waits for an in-flight lookup + // on a cache miss under require_registered; observe mode never waits. + RequestBudget time.Duration `yaml:"request_budget"` + // TrustIndexURL enables the Trust Vector threshold check when set; it + // requires MinTrustScore. + TrustIndexURL string `yaml:"trust_index_url"` + // MinTrustScore is the floor every Trust Vector dimension (0 to 100) must + // clear. The Trust Index publishes no composite score. + MinTrustScore float64 `yaml:"min_trust_score"` + // ExemptHosts are destinations never vetted (provider APIs that are not + // ANS agents, for example). Exact hostnames or "*.suffix" patterns. + ExemptHosts []string `yaml:"exempt_hosts"` +} + +// Load reads the YAML file at path into dst, rejecting unknown fields so a +// misspelled key cannot silently disable a control. +func Load(path string, dst any) error { + b, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read config: %w", err) + } + return Parse(b, dst) +} + +// Parse decodes YAML bytes into dst with strict field checking. +func Parse(b []byte, dst any) error { + dec := yaml.NewDecoder(bytes.NewReader(b)) + dec.KnownFields(true) + if err := dec.Decode(dst); err != nil { + return fmt.Errorf("parse config: %w", err) + } + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..9bbdb5a --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,165 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestTLSValidate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + tls TLS + enabled bool + wantErr bool + }{ + {name: "disabled", tls: TLS{}, enabled: false}, + {name: "both set", tls: TLS{CertFile: "c.pem", KeyFile: "k.pem"}, enabled: true}, + {name: "cert only", tls: TLS{CertFile: "c.pem"}, enabled: true, wantErr: true}, + {name: "key only", tls: TLS{KeyFile: "k.pem"}, enabled: true, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.tls.Enabled(); got != tt.enabled { + t.Fatalf("Enabled() = %v, want %v", got, tt.enabled) + } + if err := tt.tls.Validate(); (err != nil) != tt.wantErr { + t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestAuthValidate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + auth Auth + wantErr string + }{ + {name: "insecure explicit", auth: Auth{AllowInsecureNoAuth: true}}, + {name: "file source", auth: Auth{GatewayIssuer: "openshell-gateway:g1", Audience: "urn:openshell:extension:middleware:ans", JWKSFile: "jwks.json"}}, + {name: "url source", auth: Auth{GatewayIssuer: "openshell-gateway:g1", Audience: "urn:openshell:extension:middleware:ans", JWKSURL: "https://gw/.well-known/jwks.json"}}, + {name: "missing everything", auth: Auth{}, wantErr: "gateway_issuer audience jwks_file or jwks_url"}, + {name: "missing keys", auth: Auth{GatewayIssuer: "i", Audience: "a"}, wantErr: "jwks_file or jwks_url"}, + {name: "missing audience", auth: Auth{GatewayIssuer: "i", JWKSFile: "f"}, wantErr: "audience"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.auth.Validate() + if tt.wantErr == "" { + if err != nil { + t.Fatalf("Validate() unexpected error: %v", err) + } + return + } + if err == nil { + t.Fatalf("Validate() = nil, want error mentioning %q", tt.wantErr) + } + for _, want := range strings.Fields(strings.ReplaceAll(tt.wantErr, "jwks_file or jwks_url", "jwks_file")) { + if !strings.Contains(err.Error(), want) { + t.Fatalf("Validate() error %q does not mention %q", err, want) + } + } + }) + } +} + +func TestTransparencyLogValidate(t *testing.T) { + t.Parallel() + if err := (TransparencyLog{}).Validate(); err == nil { + t.Fatal("Validate() = nil for empty base_url") + } + if err := (TransparencyLog{BaseURL: "https://tl.example"}).Validate(); err != nil { + t.Fatalf("Validate() unexpected error: %v", err) + } +} + +type sample struct { + TLS TLS `yaml:"tls"` + Auth Auth `yaml:"auth"` + TL TransparencyLog `yaml:"transparency_log"` + Vetting Vetting `yaml:"vetting"` +} + +func TestParse(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yaml string + want func(t *testing.T, s sample) + wantErr bool + }{ + { + name: "full document", + yaml: ` +tls: {cert_file: c.pem, key_file: k.pem} +auth: + gateway_issuer: openshell-gateway:g1 + audience: urn:openshell:extension:middleware:ans + jwks_file: jwks.json + jwks_refresh_interval: 5m +transparency_log: + base_url: https://tl.example + root_keys: ["tl+aaaaaaaa+AAAA"] +vetting: + trusted_tl_domains: [tl.example] + cache_ttl: 10m + request_budget: 150ms + min_trust_score: 60 + exempt_hosts: ["*.openai.com"] +`, + want: func(t *testing.T, s sample) { + t.Helper() + if s.Auth.JWKSRefreshInterval != 5*time.Minute { + t.Fatalf("jwks_refresh_interval = %v", s.Auth.JWKSRefreshInterval) + } + if s.Vetting.RequestBudget != 150*time.Millisecond || s.Vetting.MinTrustScore != 60 { + t.Fatalf("vetting = %+v", s.Vetting) + } + if len(s.TL.RootKeys) != 1 || s.Vetting.ExemptHosts[0] != "*.openai.com" { + t.Fatalf("lists not decoded: %+v %+v", s.TL, s.Vetting) + } + }, + }, + {name: "unknown field rejected", yaml: "auth: {gateway_isuer: x}", wantErr: true}, + {name: "malformed", yaml: "auth: [", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var s sample + err := Parse([]byte(tt.yaml), &s) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.want != nil { + tt.want(t, s) + } + }) + } +} + +func TestLoad(t *testing.T) { + t.Parallel() + dir := t.TempDir() + path := filepath.Join(dir, "cfg.yaml") + if err := os.WriteFile(path, []byte("tls: {cert_file: c, key_file: k}\n"), 0o600); err != nil { + t.Fatal(err) + } + var s sample + if err := Load(path, &s); err != nil { + t.Fatalf("Load() error = %v", err) + } + if s.TLS.CertFile != "c" { + t.Fatalf("Load() decoded %+v", s.TLS) + } + if err := Load(filepath.Join(dir, "missing.yaml"), &s); err == nil { + t.Fatal("Load() = nil for a missing file") + } +} diff --git a/internal/extauth/caller.go b/internal/extauth/caller.go new file mode 100644 index 0000000..98953d7 --- /dev/null +++ b/internal/extauth/caller.go @@ -0,0 +1,43 @@ +// Package extauth authenticates OpenShell's calls into an extension service. +// The gateway and sandbox supervisors present a short-lived Ed25519 JWT +// (typ "openshell-ext+jwt") whose audience names this registration; the +// package verifies it against the operator-provisioned JWKS and exposes the +// proven caller to request handlers through the context. +package extauth + +import "context" + +// CallerKind is the OpenShell component that made the call. +type CallerKind string + +const ( + // CallerGateway is the gateway itself (Describe, ValidateConfig, Evaluate on + // interceptors). + CallerGateway CallerKind = "gateway" + // CallerSupervisor is a sandbox supervisor (Describe, EvaluateHttpRequest). + CallerSupervisor CallerKind = "supervisor" +) + +// Caller is the authenticated identity of an OpenShell caller. +type Caller struct { + Kind CallerKind + Issuer string + Subject string + // SandboxID is set for supervisor callers and names the one sandbox the + // credential was minted for. + SandboxID string +} + +type callerCtxKey struct{} + +// ContextWithCaller stores the authenticated caller in ctx. +func ContextWithCaller(ctx context.Context, c Caller) context.Context { + return context.WithValue(ctx, callerCtxKey{}, c) +} + +// CallerFromContext returns the authenticated caller, or ok=false when the +// request was not authenticated (insecure transport mode). +func CallerFromContext(ctx context.Context) (Caller, bool) { + c, ok := ctx.Value(callerCtxKey{}).(Caller) + return c, ok +} diff --git a/internal/extauth/caller_test.go b/internal/extauth/caller_test.go new file mode 100644 index 0000000..61bd10a --- /dev/null +++ b/internal/extauth/caller_test.go @@ -0,0 +1,19 @@ +package extauth + +import ( + "context" + "testing" +) + +func TestCallerContextRoundTrip(t *testing.T) { + t.Parallel() + if _, ok := CallerFromContext(context.Background()); ok { + t.Fatal("CallerFromContext() = ok on an empty context") + } + want := Caller{Kind: CallerSupervisor, Issuer: "openshell-gateway:g1", Subject: "spiffe://openshell/sandbox/sb-1", SandboxID: "sb-1"} + ctx := ContextWithCaller(context.Background(), want) + got, ok := CallerFromContext(ctx) + if !ok || got != want { + t.Fatalf("CallerFromContext() = %+v, %v; want %+v, true", got, ok, want) + } +} diff --git a/internal/extauth/grpc.go b/internal/extauth/grpc.go new file mode 100644 index 0000000..07a5d8f --- /dev/null +++ b/internal/extauth/grpc.go @@ -0,0 +1,87 @@ +package extauth + +import ( + "context" + "errors" + "fmt" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +// ErrNoCredential reports a call without exactly one "Bearer" authorization +// metadata value. +var ErrNoCredential = errors.New("extauth: missing or malformed authorization metadata") + +const unauthenticatedMessage = "invalid or missing credential" + +// UnaryInterceptor authenticates unary calls and exposes the caller through +// CallerFromContext. +func (v *Verifier) UnaryInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + ctx, err := v.authenticate(ctx, info.FullMethod) + if err != nil { + return nil, err + } + return handler(ctx, req) + } +} + +// StreamInterceptor authenticates streaming calls; the handler's stream +// Context carries the caller. +func (v *Verifier) StreamInterceptor() grpc.StreamServerInterceptor { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx, err := v.authenticate(ss.Context(), info.FullMethod) + if err != nil { + return err + } + return handler(srv, &callerStream{ServerStream: ss, ctx: ctx}) + } +} + +func (v *Verifier) authenticate(ctx context.Context, method string) (context.Context, error) { + if v.insecure { + return ctx, nil + } + token, err := bearerToken(ctx) + if err == nil { + var caller Caller + if caller, err = v.Verify(ctx, token); err == nil { + return ContextWithCaller(ctx, caller), nil + } + } + attrs := []any{"method", method, "err", err} + if p, ok := peer.FromContext(ctx); ok && p.Addr != nil { + attrs = append(attrs, "peer", p.Addr.String()) + } + v.logger.WarnContext(ctx, "rejected unauthenticated call", attrs...) + return nil, status.Error(codes.Unauthenticated, unauthenticatedMessage) +} + +func bearerToken(ctx context.Context) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", ErrNoCredential + } + values := md.Get("authorization") + if len(values) != 1 { + return "", fmt.Errorf("%w: got %d authorization values", ErrNoCredential, len(values)) + } + scheme, token, found := strings.Cut(values[0], " ") + token = strings.TrimSpace(token) + if !found || !strings.EqualFold(scheme, "Bearer") || token == "" { + return "", fmt.Errorf("%w: expected a Bearer credential", ErrNoCredential) + } + return token, nil +} + +type callerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s *callerStream) Context() context.Context { return s.ctx } diff --git a/internal/extauth/grpc_test.go b/internal/extauth/grpc_test.go new file mode 100644 index 0000000..70f914c --- /dev/null +++ b/internal/extauth/grpc_test.go @@ -0,0 +1,286 @@ +package extauth + +import ( + "context" + "net" + "strings" + "sync" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const testMethod = "/svc/Method" + +func assertUnauthenticated(t *testing.T, err error, token string) { + t.Helper() + st, ok := status.FromError(err) + if !ok || st.Code() != codes.Unauthenticated { + t.Fatalf("error = %v, want codes.Unauthenticated", err) + } + if st.Message() != unauthenticatedMessage { + t.Fatalf("status message = %q, want %q", st.Message(), unauthenticatedMessage) + } + if token != "" && strings.Contains(st.Message(), token) { + t.Fatalf("status message echoes the token: %q", st.Message()) + } +} + +func TestUnaryInterceptor(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + key := newSigner(t, "k1") + v := fileVerifier(t, now, key) + tok := gatewayToken(t, key, now) + gateway := Caller{Kind: CallerGateway, Issuer: testIssuer, Subject: testSubject} + tests := []struct { + name string + md metadata.MD + want Caller + denied bool + }{ + {name: "no metadata", denied: true}, + {name: "no authorization", md: metadata.Pairs("x-other", "1"), denied: true}, + {name: "two values", md: metadata.Pairs("authorization", "Bearer "+tok, "authorization", "Bearer "+tok), denied: true}, + {name: "basic scheme", md: metadata.Pairs("authorization", "Basic abc"), denied: true}, + {name: "no scheme", md: metadata.Pairs("authorization", tok), denied: true}, + {name: "empty token", md: metadata.Pairs("authorization", "Bearer "), denied: true}, + {name: "invalid token", md: metadata.Pairs("authorization", "Bearer "+tok+"x"), denied: true}, + {name: "bearer", md: metadata.Pairs("authorization", "Bearer "+tok), want: gateway}, + {name: "lowercase scheme", md: metadata.Pairs("authorization", "bearer "+tok), want: gateway}, + {name: "uppercase scheme", md: metadata.Pairs("authorization", "BEARER "+tok), want: gateway}, + {name: "mixed-case key", md: metadata.Pairs("Authorization", "Bearer "+tok), want: gateway}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := t.Context() + if tt.md != nil { + ctx = metadata.NewIncomingContext(ctx, tt.md) + } + var got Caller + var authenticated, called bool + handler := func(ctx context.Context, _ any) (any, error) { + called = true + got, authenticated = CallerFromContext(ctx) + return "ok", nil + } + resp, err := v.UnaryInterceptor()(ctx, "req", &grpc.UnaryServerInfo{FullMethod: testMethod}, handler) + if tt.denied { + assertUnauthenticated(t, err, tok) + if called || resp != nil { + t.Fatalf("handler called = %v, resp = %v; want no handler call", called, resp) + } + return + } + if err != nil || resp != "ok" { + t.Fatalf("interceptor = %v, %v; want ok, nil", resp, err) + } + if !authenticated || got != tt.want { + t.Fatalf("CallerFromContext() = %+v, %v; want %+v, true", got, authenticated, tt.want) + } + }) + } +} + +type fakeStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s fakeStream) Context() context.Context { return s.ctx } + +func TestStreamInterceptor(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + key := newSigner(t, "k1") + v := fileVerifier(t, now, key) + claims := testClaims(now) + set("caller_kind", "supervisor", "sandbox_id", "sb-1", "sub", "spiffe://openshell/sandbox/sb-1")(claims) + tok := key.mint(t, testHeader(key.kid), claims) + want := Caller{Kind: CallerSupervisor, Issuer: testIssuer, Subject: "spiffe://openshell/sandbox/sb-1", SandboxID: "sb-1"} + + t.Run("authenticated", func(t *testing.T) { + t.Parallel() + in := fakeStream{ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs("authorization", "Bearer "+tok))} + var got Caller + var ok bool + handler := func(_ any, ss grpc.ServerStream) error { + got, ok = CallerFromContext(ss.Context()) + return nil + } + if err := v.StreamInterceptor()("srv", in, &grpc.StreamServerInfo{FullMethod: testMethod}, handler); err != nil { + t.Fatalf("interceptor error = %v", err) + } + if !ok || got != want { + t.Fatalf("CallerFromContext() = %+v, %v; want %+v, true", got, ok, want) + } + }) + t.Run("denied", func(t *testing.T) { + t.Parallel() + in := fakeStream{ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs("authorization", "Bearer nope"))} + called := false + handler := func(_ any, _ grpc.ServerStream) error { + called = true + return nil + } + err := v.StreamInterceptor()("srv", in, &grpc.StreamServerInfo{FullMethod: testMethod}, handler) + assertUnauthenticated(t, err, "nope") + if called { + t.Fatal("handler called for a denied stream") + } + }) +} + +func TestInsecurePassThrough(t *testing.T) { + t.Parallel() + v, err := NewVerifier(config.Auth{AllowInsecureNoAuth: true}, discardLogger()) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + for name, ctx := range map[string]context.Context{ + "no metadata": t.Context(), + "garbage credential": metadata.NewIncomingContext(t.Context(), metadata.Pairs("authorization", "Bearer garbage")), + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + unary := func(ctx context.Context, _ any) (any, error) { + if _, ok := CallerFromContext(ctx); ok { + t.Error("unary handler saw a caller in insecure mode") + } + return "ok", nil + } + if resp, err := v.UnaryInterceptor()(ctx, "req", &grpc.UnaryServerInfo{FullMethod: testMethod}, unary); err != nil || resp != "ok" { + t.Fatalf("unary = %v, %v; want ok, nil", resp, err) + } + stream := func(_ any, ss grpc.ServerStream) error { + if _, ok := CallerFromContext(ss.Context()); ok { + t.Error("stream handler saw a caller in insecure mode") + } + return nil + } + if err := v.StreamInterceptor()("srv", fakeStream{ctx: ctx}, &grpc.StreamServerInfo{FullMethod: testMethod}, stream); err != nil { + t.Fatalf("stream error = %v", err) + } + }) + } +} + +func TestRejectedCallIsLogged(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + logger, buf := bufferLogger() + v, err := NewVerifier(fileAuth(t, newSigner(t, "k1")), logger, WithClock(fixedClock(now))) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + const tok = "zzqq.zzqq" + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("authorization", "Bearer "+tok)) + _, err = v.UnaryInterceptor()(ctx, "req", &grpc.UnaryServerInfo{FullMethod: testMethod}, func(context.Context, any) (any, error) { + return "ok", nil + }) + assertUnauthenticated(t, err, tok) + log := buf.String() + if !strings.Contains(log, "rejected unauthenticated call") || !strings.Contains(log, "method="+testMethod) { + t.Fatalf("missing rejection log entry:\n%s", log) + } + if strings.Contains(log, tok) { + t.Fatalf("log echoes the token:\n%s", log) + } +} + +type recordingHealth struct { + healthpb.UnimplementedHealthServer + mu sync.Mutex + callers []Caller +} + +func (h *recordingHealth) record(ctx context.Context) { + c, _ := CallerFromContext(ctx) + h.mu.Lock() + defer h.mu.Unlock() + h.callers = append(h.callers, c) +} + +func (h *recordingHealth) Check(ctx context.Context, _ *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + h.record(ctx) + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil +} + +func (h *recordingHealth) Watch(_ *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error { + h.record(stream.Context()) + return stream.Send(&healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}) +} + +func (h *recordingHealth) recorded() []Caller { + h.mu.Lock() + defer h.mu.Unlock() + return append([]Caller(nil), h.callers...) +} + +func startServer(t *testing.T, v *Verifier) (healthpb.HealthClient, *recordingHealth) { + t.Helper() + lis := bufconn.Listen(1 << 20) + srv := grpc.NewServer(grpc.UnaryInterceptor(v.UnaryInterceptor()), grpc.StreamInterceptor(v.StreamInterceptor())) + rec := &recordingHealth{} + healthpb.RegisterHealthServer(srv, rec) + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + return healthpb.NewHealthClient(conn), rec +} + +func TestInterceptorsOverBufconn(t *testing.T) { + t.Parallel() + now := time.Now() + key := newSigner(t, "k1") + v := fileVerifier(t, now, key) + client, rec := startServer(t, v) + gateway := Caller{Kind: CallerGateway, Issuer: testIssuer, Subject: testSubject} + supervisorClaims := testClaims(now) + set("caller_kind", "supervisor", "sandbox_id", "sb-1", "sub", "spiffe://openshell/sandbox/sb-1")(supervisorClaims) + supervisor := Caller{Kind: CallerSupervisor, Issuer: testIssuer, Subject: "spiffe://openshell/sandbox/sb-1", SandboxID: "sb-1"} + + ctx := metadata.AppendToOutgoingContext(t.Context(), "authorization", "Bearer "+gatewayToken(t, key, now)) + if _, err := client.Check(ctx, &healthpb.HealthCheckRequest{}); err != nil { + t.Fatalf("Check() error = %v", err) + } + ctx = metadata.AppendToOutgoingContext(t.Context(), "authorization", "Bearer "+key.mint(t, testHeader(key.kid), supervisorClaims)) + stream, err := client.Watch(ctx, &healthpb.HealthCheckRequest{}) + if err != nil { + t.Fatalf("Watch() error = %v", err) + } + if _, err = stream.Recv(); err != nil { + t.Fatalf("Recv() error = %v", err) + } + if got := rec.recorded(); len(got) != 2 || got[0] != gateway || got[1] != supervisor { + t.Fatalf("recorded callers = %+v, want [%+v %+v]", got, gateway, supervisor) + } + + _, err = client.Check(t.Context(), &healthpb.HealthCheckRequest{}) + assertUnauthenticated(t, err, "") + stream, err = client.Watch(t.Context(), &healthpb.HealthCheckRequest{}) + if err != nil { + t.Fatalf("Watch() error = %v", err) + } + _, err = stream.Recv() + assertUnauthenticated(t, err, "") + if got := rec.recorded(); len(got) != 2 { + t.Fatalf("unauthenticated calls reached the handler: %+v", got) + } +} diff --git a/internal/extauth/helpers_test.go b/internal/extauth/helpers_test.go new file mode 100644 index 0000000..b31fd9c --- /dev/null +++ b/internal/extauth/helpers_test.go @@ -0,0 +1,132 @@ +package extauth + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + testIssuer = "openshell-gateway:test-gw" + testAudience = "urn:openshell:extension:middleware:ans" + testSubject = "openshell-gateway" +) + +type signer struct { + kid string + pub ed25519.PublicKey + priv ed25519.PrivateKey +} + +func newSigner(t *testing.T, kid string) signer { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + return signer{kid: kid, pub: pub, priv: priv} +} + +func (s signer) jwk() map[string]any { + return map[string]any{"kty": "OKP", "crv": "Ed25519", "kid": s.kid, "x": b64(s.pub)} +} + +// mint signs hdr and claims as a compact JWS. +func (s signer) mint(t *testing.T, hdr, claims map[string]any) string { + t.Helper() + return s.sign(b64(mustJSON(t, hdr)) + "." + b64(mustJSON(t, claims))) +} + +func (s signer) sign(signingInput string) string { + return signingInput + "." + b64(ed25519.Sign(s.priv, []byte(signingInput))) +} + +func b64(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } + +func mustJSON(t *testing.T, v any) []byte { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + return b +} + +func jwksJSON(t *testing.T, keys ...map[string]any) []byte { + t.Helper() + return mustJSON(t, map[string]any{"keys": keys}) +} + +func testHeader(kid string) map[string]any { + return map[string]any{"alg": "EdDSA", "typ": "openshell-ext+jwt", "kid": kid} +} + +func testClaims(now time.Time) map[string]any { + return map[string]any{ + "iss": testIssuer, "aud": testAudience, "sub": testSubject, + "iat": now.Unix(), "exp": now.Add(5 * time.Minute).Unix(), + "jti": "jti-1", "caller_kind": "gateway", + } +} + +func gatewayToken(t *testing.T, s signer, now time.Time) string { + t.Helper() + return s.mint(t, testHeader(s.kid), testClaims(now)) +} + +func writeFile(t *testing.T, name string, data []byte) string { + t.Helper() + path := filepath.Join(t.TempDir(), name) + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + return path +} + +func fixedClock(now time.Time) func() time.Time { return func() time.Time { return now } } + +type fakeClock struct { + now time.Time +} + +func (c *fakeClock) Now() time.Time { return c.now } + +func (c *fakeClock) advance(d time.Duration) { c.now = c.now.Add(d) } + +func discardLogger() *slog.Logger { return slog.New(slog.DiscardHandler) } + +func bufferLogger() (*slog.Logger, *bytes.Buffer) { + var buf bytes.Buffer + return slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), &buf +} + +func fileAuth(t *testing.T, signers ...signer) config.Auth { + t.Helper() + keys := make([]map[string]any, 0, len(signers)) + for _, s := range signers { + keys = append(keys, s.jwk()) + } + return config.Auth{ + GatewayIssuer: testIssuer, + Audience: testAudience, + JWKSFile: writeFile(t, "jwks.json", jwksJSON(t, keys...)), + } +} + +func fileVerifier(t *testing.T, now time.Time, signers ...signer) *Verifier { + t.Helper() + v, err := NewVerifier(fileAuth(t, signers...), discardLogger(), WithClock(fixedClock(now))) + if err != nil { + t.Fatalf("NewVerifier: %v", err) + } + return v +} diff --git a/internal/extauth/jwks.go b/internal/extauth/jwks.go new file mode 100644 index 0000000..dbfac1a --- /dev/null +++ b/internal/extauth/jwks.go @@ -0,0 +1,249 @@ +package extauth + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "os" + "sync" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + maxJWKSBytes = 64 << 10 + defaultUnknownKeyCooldown = 30 * time.Second +) + +var ( + // ErrNoUsableKeys reports a JWKS document without any Ed25519 signing key. + ErrNoUsableKeys = errors.New("extauth: jwks contains no usable Ed25519 key") + // ErrJWKSTooLarge reports a jwks_url response over the 64 KiB cap. + ErrJWKSTooLarge = errors.New("extauth: jwks response exceeds 64 KiB") + // ErrInsecureJWKSURL reports a jwks_url that is not https; keys fetched + // over plaintext could be replaced by anyone on the path. + ErrInsecureJWKSURL = errors.New("extauth: jwks_url must use https") +) + +type jwk struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + Kid string `json:"kid"` + X string `json:"x"` + Alg string `json:"alg"` + Use string `json:"use"` +} + +func (k jwk) publicKey() (ed25519.PublicKey, string) { + switch { + case k.Kty != "OKP" || k.Crv != "Ed25519": + return nil, "not an Ed25519 OKP key" + case k.Kid == "": + return nil, "missing kid" + case k.Alg != "" && k.Alg != algEdDSA: + return nil, "alg is not EdDSA" + case k.Use != "" && k.Use != "sig": + return nil, "use is not sig" + } + raw, err := base64.RawURLEncoding.Strict().DecodeString(k.X) + if err != nil || len(raw) != ed25519.PublicKeySize { + return nil, "x is not a base64url Ed25519 public key" + } + return ed25519.PublicKey(raw), "" +} + +type jwkSet struct { + Keys []jwk `json:"keys"` +} + +type skippedKey struct { + kid string + reason string +} + +// parseJWKS returns the Ed25519 verification keys in data indexed by kid and +// the entries it skipped. +func parseJWKS(data []byte) (map[string]ed25519.PublicKey, []skippedKey, error) { + var set jwkSet + if err := json.Unmarshal(data, &set); err != nil { + return nil, nil, fmt.Errorf("extauth: parse jwks: %w", err) + } + keys := make(map[string]ed25519.PublicKey, len(set.Keys)) + var skipped []skippedKey + for _, k := range set.Keys { + pub, reason := k.publicKey() + if _, dup := keys[k.Kid]; reason == "" && dup { + reason = "duplicate kid" + } + if reason != "" { + skipped = append(skipped, skippedKey{kid: k.Kid, reason: reason}) + continue + } + keys[k.Kid] = pub + } + if len(keys) == 0 { + return nil, skipped, ErrNoUsableKeys + } + return keys, skipped, nil +} + +// keyStore holds the operator-provisioned keys from jwks_file, which are +// trusted for the life of the process, and the latest key set fetched from +// jwks_url, which every successful refresh replaces. +type keyStore struct { + logger *slog.Logger + client *http.Client + now func() time.Time + url string + cooldown time.Duration + + mu sync.Mutex + static map[string]ed25519.PublicKey + remote map[string]ed25519.PublicKey + lastFetch time.Time +} + +func newKeyStore(cfg config.Auth, logger *slog.Logger, s settings) (*keyStore, error) { + client := *s.client + client.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + store := &keyStore{ + logger: logger, + client: &client, + now: s.now, + url: cfg.JWKSURL, + cooldown: defaultUnknownKeyCooldown, + static: map[string]ed25519.PublicKey{}, + } + if cfg.JWKSURL != "" { + u, err := url.Parse(cfg.JWKSURL) + if err != nil { + return nil, fmt.Errorf("extauth: jwks_url: %w", err) + } + if u.Scheme != "https" { + return nil, ErrInsecureJWKSURL + } + } + if cfg.JWKSFile == "" { + return store, nil + } + data, err := os.ReadFile(cfg.JWKSFile) + if err != nil { + return nil, fmt.Errorf("extauth: read jwks_file: %w", err) + } + keys, skipped, err := parseJWKS(data) + for _, sk := range skipped { + logger.Warn("ignoring jwks_file entry", "kid", sk.kid, "reason", sk.reason) + } + if err != nil { + return nil, fmt.Errorf("extauth: jwks_file: %w", err) + } + store.static = keys + return store, nil +} + +// lookup returns the key for kid, fetching jwks_url once per cooldown when +// the kid is not already known. +func (s *keyStore) lookup(ctx context.Context, kid string) (ed25519.PublicKey, bool) { + if pub, ok := s.get(kid); ok { + return pub, true + } + if !s.beginOnDemandFetch() { + return nil, false + } + if err := s.refresh(ctx); err != nil { + s.logger.WarnContext(ctx, "jwks fetch for unknown kid failed", "err", err) + return nil, false + } + return s.get(kid) +} + +// count returns the number of keys held across both sets. +func (s *keyStore) count() int { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.static) + len(s.remote) +} + +func (s *keyStore) get(kid string) (ed25519.PublicKey, bool) { + s.mu.Lock() + defer s.mu.Unlock() + if pub, ok := s.static[kid]; ok { + return pub, true + } + pub, ok := s.remote[kid] + return pub, ok +} + +// beginOnDemandFetch reports whether an unknown kid may trigger a fetch now +// and records the attempt, so concurrent misses inside the cooldown share one +// fetch instead of each starting their own. +func (s *keyStore) beginOnDemandFetch() bool { + if s.url == "" { + return false + } + s.mu.Lock() + defer s.mu.Unlock() + now := s.now() + if !s.lastFetch.IsZero() && now.Sub(s.lastFetch) < s.cooldown { + return false + } + s.lastFetch = now + return true +} + +// refresh fetches jwks_url and replaces the remote key set on success. +func (s *keyStore) refresh(ctx context.Context) error { + s.mu.Lock() + s.lastFetch = s.now() + s.mu.Unlock() + body, err := s.fetch(ctx) + if err != nil { + return err + } + keys, skipped, err := parseJWKS(body) + for _, sk := range skipped { + s.logger.WarnContext(ctx, "ignoring jwks_url entry", "kid", sk.kid, "reason", sk.reason) + } + if err != nil { + return err + } + s.mu.Lock() + s.remote = keys + s.mu.Unlock() + s.logger.InfoContext(ctx, "gateway jwks refreshed", "keys", len(keys)) + return nil +} + +func (s *keyStore) fetch(ctx context.Context) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.url, nil) + if err != nil { + return nil, fmt.Errorf("extauth: jwks request: %w", err) + } + req.Header.Set("Accept", "application/json") + resp, err := s.client.Do(req) //nolint:gosec // G704: jwks_url is operator configuration, restricted to https at construction + if err != nil { + return nil, fmt.Errorf("extauth: fetch jwks: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("extauth: fetch jwks: unexpected status %d", resp.StatusCode) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, maxJWKSBytes+1)) + if err != nil { + return nil, fmt.Errorf("extauth: read jwks: %w", err) + } + if len(body) > maxJWKSBytes { + return nil, ErrJWKSTooLarge + } + return body, nil +} diff --git a/internal/extauth/jwks_test.go b/internal/extauth/jwks_test.go new file mode 100644 index 0000000..712878a --- /dev/null +++ b/internal/extauth/jwks_test.go @@ -0,0 +1,336 @@ +package extauth + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "slices" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +func TestParseJWKS(t *testing.T) { + t.Parallel() + k1, k2 := newSigner(t, "k1"), newSigner(t, "k2") + rsa := map[string]any{"kty": "RSA", "kid": "r1", "n": "AQAB", "e": "AQAB"} + padded := k1.jwk() + padded["x"] = padded["x"].(string) + "=" + short := map[string]any{"kty": "OKP", "crv": "Ed25519", "kid": "short", "x": "AQAB"} + x25519 := map[string]any{"kty": "OKP", "crv": "X25519", "kid": "x", "x": b64(k1.pub)} + tests := []struct { + name string + data []byte + wantKids []string + wantSkipped []string + wantErr error + wantAnyErr bool + }{ + {name: "single key", data: jwksJSON(t, k1.jwk()), wantKids: []string{"k1"}}, + {name: "two keys", data: jwksJSON(t, k1.jwk(), k2.jwk()), wantKids: []string{"k1", "k2"}}, + {name: "invalid json", data: []byte("{"), wantAnyErr: true}, + {name: "keys not an array", data: []byte(`{"keys":{}}`), wantAnyErr: true}, + {name: "no keys member", data: []byte(`{}`), wantErr: ErrNoUsableKeys}, + {name: "empty keys", data: []byte(`{"keys":[]}`), wantErr: ErrNoUsableKeys}, + {name: "only rsa", data: jwksJSON(t, rsa), wantErr: ErrNoUsableKeys, wantSkipped: []string{"not an Ed25519 OKP key"}}, + {name: "x25519 is not a signing curve", data: jwksJSON(t, x25519), wantErr: ErrNoUsableKeys, wantSkipped: []string{"not an Ed25519 OKP key"}}, + {name: "padded x", data: jwksJSON(t, padded), wantErr: ErrNoUsableKeys, wantSkipped: []string{"x is not a base64url Ed25519 public key"}}, + {name: "short x", data: jwksJSON(t, short), wantErr: ErrNoUsableKeys, wantSkipped: []string{"x is not a base64url Ed25519 public key"}}, + {name: "mixed", data: jwksJSON(t, rsa, k1.jwk(), short, k1.jwk()), wantKids: []string{"k1"}, + wantSkipped: []string{"not an Ed25519 OKP key", "x is not a base64url Ed25519 public key", "duplicate kid"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + keys, skipped, err := parseJWKS(tt.data) + if tt.wantAnyErr || tt.wantErr != nil { + if err == nil { + t.Fatalf("parseJWKS() = %v, want error", keys) + } + if tt.wantErr != nil && !errors.Is(err, tt.wantErr) { + t.Fatalf("parseJWKS() error = %v, want %v", err, tt.wantErr) + } + } else if err != nil { + t.Fatalf("parseJWKS() error = %v", err) + } + var kids []string + for kid := range keys { + kids = append(kids, kid) + } + slices.Sort(kids) + if !slices.Equal(kids, tt.wantKids) { + t.Fatalf("parseJWKS() kids = %v, want %v", kids, tt.wantKids) + } + var reasons []string + for _, sk := range skipped { + reasons = append(reasons, sk.reason) + } + if !slices.Equal(reasons, tt.wantSkipped) { + t.Fatalf("parseJWKS() skipped = %v, want %v", reasons, tt.wantSkipped) + } + }) + } +} + +type jwksServer struct { + *httptest.Server + hits atomic.Int32 + mu sync.Mutex + body []byte + code int +} + +func newJWKSServer(t *testing.T, body []byte) *jwksServer { + t.Helper() + s := &jwksServer{body: body, code: http.StatusOK} + s.Server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + s.hits.Add(1) + s.mu.Lock() + body, code := s.body, s.code + s.mu.Unlock() + w.WriteHeader(code) + _, _ = w.Write(body) + })) + t.Cleanup(s.Close) + return s +} + +func (s *jwksServer) serve(body []byte, code int) { + s.mu.Lock() + defer s.mu.Unlock() + s.body, s.code = body, code +} + +func remoteAuth(t *testing.T, url string, file ...signer) config.Auth { + t.Helper() + cfg := config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSURL: url} + if len(file) > 0 { + cfg.JWKSFile = fileAuth(t, file...).JWKSFile + } + return cfg +} + +func mustVerify(t *testing.T, v *Verifier, tok string) { + t.Helper() + if _, err := v.Verify(t.Context(), tok); err != nil { + t.Fatalf("Verify() error = %v", err) + } +} + +func mustReject(t *testing.T, v *Verifier, tok string, want error) { + t.Helper() + if _, err := v.Verify(t.Context(), tok); !errors.Is(err, want) { + t.Fatalf("Verify() error = %v, want %v", err, want) + } +} + +func mustHits(t *testing.T, srv *jwksServer, want int32) { + t.Helper() + if got := srv.hits.Load(); got != want { + t.Fatalf("jwks fetches = %d, want %d", got, want) + } +} + +func TestRemoteKeyLookup(t *testing.T) { + t.Parallel() + clock := &fakeClock{now: time.Unix(1_800_000_000, 0)} + fileKey, k1, k2 := newSigner(t, "file"), newSigner(t, "k1"), newSigner(t, "k2") + srv := newJWKSServer(t, jwksJSON(t, k1.jwk())) + v, err := NewVerifier(remoteAuth(t, srv.URL, fileKey), discardLogger(), WithClock(clock.Now), WithHTTPClient(srv.Client())) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + + mustVerify(t, v, gatewayToken(t, fileKey, clock.now)) + mustHits(t, srv, 0) + + mustVerify(t, v, gatewayToken(t, k1, clock.now)) + mustHits(t, srv, 1) + + mustReject(t, v, gatewayToken(t, k2, clock.now), ErrUnknownKey) + clock.advance(29 * time.Second) + mustReject(t, v, gatewayToken(t, k2, clock.now), ErrUnknownKey) + mustHits(t, srv, 1) + + clock.advance(2 * time.Second) + mustReject(t, v, gatewayToken(t, k2, clock.now), ErrUnknownKey) + mustHits(t, srv, 2) + + srv.serve(jwksJSON(t, k2.jwk()), http.StatusOK) + clock.advance(31 * time.Second) + mustVerify(t, v, gatewayToken(t, k2, clock.now)) + mustHits(t, srv, 3) + mustReject(t, v, gatewayToken(t, k1, clock.now), ErrUnknownKey) + mustHits(t, srv, 3) + + srv.serve(nil, http.StatusInternalServerError) + clock.advance(31 * time.Second) + mustReject(t, v, gatewayToken(t, k1, clock.now), ErrUnknownKey) + mustHits(t, srv, 4) + mustVerify(t, v, gatewayToken(t, k2, clock.now)) + mustVerify(t, v, gatewayToken(t, fileKey, clock.now)) +} + +func TestRefreshErrorsKeepPreviousKeys(t *testing.T) { + t.Parallel() + k1 := newSigner(t, "k1") + rsa := map[string]any{"kty": "RSA", "kid": "r1", "n": "AQAB", "e": "AQAB"} + oversize := []byte(`{"keys":[],"pad":"` + strings.Repeat("x", maxJWKSBytes) + `"}`) + tests := []struct { + name string + body []byte + code int + wantErr error + }{ + {name: "not found", code: http.StatusNotFound}, + {name: "oversize", body: oversize, code: http.StatusOK, wantErr: ErrJWKSTooLarge}, + {name: "bad json", body: []byte("{"), code: http.StatusOK}, + {name: "no usable keys", body: jwksJSON(t, rsa), code: http.StatusOK, wantErr: ErrNoUsableKeys}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + clock := &fakeClock{now: time.Unix(1_800_000_000, 0)} + srv := newJWKSServer(t, jwksJSON(t, k1.jwk())) + v, err := NewVerifier(remoteAuth(t, srv.URL), discardLogger(), WithClock(clock.Now), WithHTTPClient(srv.Client())) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + if err := v.keys.refresh(t.Context()); err != nil { + t.Fatalf("initial refresh error = %v", err) + } + srv.serve(tt.body, tt.code) + err = v.keys.refresh(t.Context()) + if err == nil { + t.Fatal("refresh() = nil, want error") + } + if tt.wantErr != nil && !errors.Is(err, tt.wantErr) { + t.Fatalf("refresh() error = %v, want %v", err, tt.wantErr) + } + mustVerify(t, v, gatewayToken(t, k1, clock.now)) + }) + } +} + +type failingTransport struct{} + +func (failingTransport) RoundTrip(*http.Request) (*http.Response, error) { + return nil, errors.New("connection refused") +} + +func TestFetchTransportError(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + logger, buf := bufferLogger() + cfg := remoteAuth(t, "https://gateway.invalid/.well-known/jwks.json") + v, err := NewVerifier(cfg, logger, WithClock(fixedClock(now)), WithHTTPClient(&http.Client{Transport: failingTransport{}})) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + if err := v.keys.refresh(t.Context()); err == nil || !strings.Contains(err.Error(), "fetch jwks") { + t.Fatalf("refresh() error = %v, want fetch failure", err) + } + clock := &fakeClock{now: now.Add(time.Minute)} + v.now, v.keys.now = clock.Now, clock.Now + mustReject(t, v, gatewayToken(t, newSigner(t, "k1"), clock.now), ErrUnknownKey) + if !strings.Contains(buf.String(), "jwks fetch for unknown kid failed") { + t.Fatalf("missing fetch failure warning in log:\n%s", buf.String()) + } +} + +func TestRunRefreshesPeriodically(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + k1 := newSigner(t, "k1") + srv := newJWKSServer(t, jwksJSON(t, k1.jwk())) + v, err := NewVerifier(remoteAuth(t, srv.URL), discardLogger(), + WithClock(fixedClock(now)), WithHTTPClient(srv.Client()), WithRefreshInterval(2*time.Millisecond)) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan struct{}) + go func() { + v.Run(ctx) + close(done) + }() + deadline := time.Now().Add(10 * time.Second) + for srv.hits.Load() < 3 { + if time.Now().After(deadline) { + t.Fatalf("Run() fetched %d times, want at least 3", srv.hits.Load()) + } + time.Sleep(time.Millisecond) + } + cancel() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("Run() did not return after cancel") + } + mustVerify(t, v, gatewayToken(t, k1, now)) +} + +func TestRunLogsRefreshFailure(t *testing.T) { + t.Parallel() + logger, buf := bufferLogger() + srv := newJWKSServer(t, nil) + srv.serve(nil, http.StatusInternalServerError) + v, err := NewVerifier(remoteAuth(t, srv.URL), logger, WithHTTPClient(srv.Client()), WithRefreshInterval(time.Hour)) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan struct{}) + go func() { + v.Run(ctx) + close(done) + }() + deadline := time.Now().Add(10 * time.Second) + for srv.hits.Load() < 1 { + if time.Now().After(deadline) { + t.Fatal("Run() never fetched") + } + time.Sleep(time.Millisecond) + } + cancel() + <-done + if !strings.Contains(buf.String(), "scheduled jwks refresh failed") { + t.Fatalf("missing refresh failure warning in log:\n%s", buf.String()) + } +} + +func TestRunWithoutURLWaits(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + insecureV, err := NewVerifier(config.Auth{AllowInsecureNoAuth: true}, discardLogger()) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + for name, v := range map[string]*Verifier{"file only": fileVerifier(t, now, newSigner(t, "k1")), "insecure": insecureV} { + t.Run(name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan struct{}) + go func() { + v.Run(ctx) + close(done) + }() + select { + case <-done: + t.Fatal("Run() returned before cancel") + case <-time.After(20 * time.Millisecond): + } + cancel() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("Run() did not return after cancel") + } + }) + } +} diff --git a/internal/extauth/redirect_test.go b/internal/extauth/redirect_test.go new file mode 100644 index 0000000..f5de0d8 --- /dev/null +++ b/internal/extauth/redirect_test.go @@ -0,0 +1,123 @@ +package extauth + +import ( + "bytes" + "errors" + "io" + "net/http" + "sync/atomic" + "testing" + "time" +) + +type jwksTransportFunc func(*http.Request) (*http.Response, error) + +func (f jwksTransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestJWKSRedirectsCannotReplaceTrustedKeys(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + redirect string + }{ + {name: "direct trusted response"}, + {name: "another authority", redirect: "https://attacker.example.com/keys"}, + {name: "plaintext downgrade", redirect: "http://gateway.example.com/keys"}, + {name: "same origin redirect", redirect: "/moved"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + clock := &fakeClock{now: time.Unix(1_800_000_000, 0)} + trusted, replacement := newSigner(t, "trusted"), newSigner(t, "replacement") + trustedDocument := jwksJSON(t, trusted.jwk()) + replacementDocument := jwksJSON(t, replacement.jwk()) + var calls atomic.Int32 + var suppliedRedirectCalls atomic.Int32 + client := &http.Client{ + Timeout: time.Second, + CheckRedirect: func(*http.Request, []*http.Request) error { + suppliedRedirectCalls.Add(1) + return nil + }, + Transport: jwksTransportFunc(func(req *http.Request) (*http.Response, error) { + header := make(http.Header) + status := http.StatusOK + body := replacementDocument + switch calls.Add(1) { + case 1: + body = trustedDocument + case 2: + if tc.redirect != "" { + status = http.StatusFound + header.Set("Location", tc.redirect) + } + } + return &http.Response{StatusCode: status, Header: header, Body: io.NopCloser(bytes.NewReader(body)), Request: req}, nil + }), + } + verifier, err := NewVerifier(remoteAuth(t, "https://gateway.example.com/.well-known/jwks.json"), + discardLogger(), WithClock(clock.Now), WithHTTPClient(client)) + if err != nil { + t.Fatal(err) + } + mustVerify(t, verifier, gatewayToken(t, trusted, clock.now)) + clock.advance(defaultUnknownKeyCooldown + time.Second) + replacementToken := gatewayToken(t, replacement, clock.now) + if tc.redirect == "" { + mustVerify(t, verifier, replacementToken) + } else { + mustReject(t, verifier, replacementToken, ErrUnknownKey) + mustVerify(t, verifier, gatewayToken(t, trusted, clock.now)) + if _, imported := verifier.keys.get(replacement.kid); imported { + t.Fatal("redirected key entered the verification cache") + } + } + if tc.redirect == "" { + mustVerify(t, verifier, replacementToken) + } else { + mustReject(t, verifier, replacementToken, ErrUnknownKey) + } + if calls.Load() != 2 || suppliedRedirectCalls.Load() != 0 { + t.Fatalf("HTTP requests %d, supplied redirect callbacks %d; want 2 and 0", calls.Load(), suppliedRedirectCalls.Load()) + } + if client == verifier.keys.client || verifier.keys.client.Timeout != client.Timeout { + t.Fatal("the supplied HTTP client must be copied with its timeout preserved") + } + if err := client.CheckRedirect(nil, nil); err != nil || suppliedRedirectCalls.Load() != 1 { + t.Fatalf("supplied redirect policy changed: %v", err) + } + }) + } +} + +func TestDefaultJWKSClientRejectsRedirects(t *testing.T) { + t.Parallel() + clock := time.Unix(1_800_000_000, 0) + forged := newSigner(t, "forged") + document := jwksJSON(t, forged.jwk()) + verifier, err := NewVerifier(remoteAuth(t, "https://gateway.example.com/.well-known/jwks.json"), + discardLogger(), WithClock(fixedClock(clock))) + if err != nil { + t.Fatal(err) + } + var calls atomic.Int32 + verifier.keys.client.Transport = jwksTransportFunc(func(req *http.Request) (*http.Response, error) { + header := make(http.Header) + status := http.StatusOK + if calls.Add(1) == 1 { + status = http.StatusFound + header.Set("Location", "http://attacker.example.com/keys") + } + return &http.Response{StatusCode: status, Header: header, Body: io.NopCloser(bytes.NewReader(document)), Request: req}, nil + }) + for range 2 { + if _, err := verifier.Verify(t.Context(), gatewayToken(t, forged, clock)); !errors.Is(err, ErrUnknownKey) { + t.Fatalf("Verify() error = %v, want ErrUnknownKey", err) + } + } + if calls.Load() != 1 || verifier.keys.count() != 0 { + t.Fatalf("HTTP requests %d, cached keys %d; want 1 and 0", calls.Load(), verifier.keys.count()) + } +} diff --git a/internal/extauth/verifier.go b/internal/extauth/verifier.go new file mode 100644 index 0000000..21c7475 --- /dev/null +++ b/internal/extauth/verifier.go @@ -0,0 +1,311 @@ +package extauth + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + algEdDSA = "EdDSA" + headerTyp = "openshell-ext+jwt" + + maxTokenBytes = 8 << 10 + jwsSegments = 3 + clockSkew = 30 * time.Second + maxLifetime = time.Hour + defaultRefreshInterval = 5 * time.Minute + minRefreshInterval = 30 * time.Second + defaultHTTPTimeout = 5 * time.Second +) + +var ( + // ErrTokenTooLarge reports a token over the 8 KiB cap. + ErrTokenTooLarge = errors.New("extauth: token exceeds 8 KiB") + // ErrMalformedToken reports a token that is not a three-segment compact + // JWS with a JSON header. + ErrMalformedToken = errors.New("extauth: malformed token") + // ErrUnsupportedAlg reports a header alg other than EdDSA. + ErrUnsupportedAlg = errors.New("extauth: unsupported alg") + // ErrUnexpectedTokenType reports a header typ other than openshell-ext+jwt. + ErrUnexpectedTokenType = errors.New("extauth: unexpected typ") + // ErrUnknownKey reports a missing kid or one that no trusted key set holds. + ErrUnknownKey = errors.New("extauth: unknown signing key") + // ErrInvalidSignature reports a signature that does not verify. + ErrInvalidSignature = errors.New("extauth: invalid signature") + // ErrInvalidClaims reports claims that are missing, empty or of the wrong + // JSON type. + ErrInvalidClaims = errors.New("extauth: invalid claims") + // ErrIssuerMismatch reports an iss other than the configured gateway issuer. + ErrIssuerMismatch = errors.New("extauth: issuer mismatch") + // ErrAudienceMismatch reports an aud other than the configured audience. + ErrAudienceMismatch = errors.New("extauth: audience mismatch") + // ErrTokenExpired reports an exp in the past beyond the clock skew. + ErrTokenExpired = errors.New("extauth: token expired") + // ErrTokenNotYetValid reports an iat in the future beyond the clock skew. + ErrTokenNotYetValid = errors.New("extauth: token issued in the future") + // ErrTokenLifetime reports a lifetime that is not positive or exceeds one hour. + ErrTokenLifetime = errors.New("extauth: token lifetime out of bounds") + // ErrInvalidCaller reports an unknown caller_kind or a sandbox_id that + // does not match the caller kind. + ErrInvalidCaller = errors.New("extauth: invalid caller_kind or sandbox_id") +) + +type settings struct { + now func() time.Time + client *http.Client + refreshInterval time.Duration +} + +// Option adjusts a Verifier. +type Option func(*settings) + +// WithClock replaces the wall clock used for claim checks and the fetch cooldown. +func WithClock(now func() time.Time) Option { + return func(s *settings) { s.now = now } +} + +// WithHTTPClient replaces the client that fetches jwks_url. +func WithHTTPClient(c *http.Client) Option { + return func(s *settings) { s.client = c } +} + +// WithRefreshInterval overrides the jwks_url polling interval used by Run. +func WithRefreshInterval(d time.Duration) Option { + return func(s *settings) { s.refreshInterval = d } +} + +// Verifier authenticates OpenShell extension tokens against the gateway's +// signing keys. +type Verifier struct { + logger *slog.Logger + issuer string + audience string + insecure bool + now func() time.Time + refreshInterval time.Duration + keys *keyStore +} + +// NewVerifier loads the trust anchor from cfg.JWKSFile and prepares refresh +// from cfg.JWKSURL. With cfg.AllowInsecureNoAuth the interceptors pass every +// call through unauthenticated, which is logged once here. +func NewVerifier(cfg config.Auth, logger *slog.Logger, opts ...Option) (*Verifier, error) { + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + if err := cfg.Validate(); err != nil { + return nil, err + } + s := settings{ + now: time.Now, + client: &http.Client{Timeout: defaultHTTPTimeout}, + refreshInterval: defaultRefreshInterval, + } + if cfg.JWKSRefreshInterval > 0 { + s.refreshInterval = max(cfg.JWKSRefreshInterval, minRefreshInterval) + } + for _, opt := range opts { + opt(&s) + } + v := &Verifier{ + logger: logger, + issuer: cfg.GatewayIssuer, + audience: cfg.Audience, + insecure: cfg.AllowInsecureNoAuth, + now: s.now, + refreshInterval: s.refreshInterval, + } + if v.insecure { + logger.Warn("extension authentication disabled: allow_insecure_no_auth is set, every caller is trusted") + v.keys = &keyStore{logger: logger, now: s.now} + return v, nil + } + keys, err := newKeyStore(cfg, logger, s) + if err != nil { + return nil, err + } + v.keys = keys + return v, nil +} + +// Run refreshes the keys from jwks_url now and then every refresh interval. +// It blocks until ctx is done; without a jwks_url it only waits. +func (v *Verifier) Run(ctx context.Context) { + if v.keys.url == "" { + <-ctx.Done() + return + } + v.refreshKeys(ctx) + ticker := time.NewTicker(v.refreshInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + v.refreshKeys(ctx) + } + } +} + +// Verify checks token and returns the caller it proves. +func (v *Verifier) Verify(ctx context.Context, token string) (Caller, error) { + if len(token) > maxTokenBytes { + return Caller{}, ErrTokenTooLarge + } + parts := strings.Split(token, ".") + if len(parts) != jwsSegments { + return Caller{}, fmt.Errorf("%w: expected %d segments", ErrMalformedToken, jwsSegments) + } + kid, err := checkHeader(parts[0]) + if err != nil { + return Caller{}, err + } + pub, ok := v.keys.lookup(ctx, kid) + if !ok { + return Caller{}, ErrUnknownKey + } + sig, err := decodeSegment(parts[2]) + if err != nil || len(sig) != ed25519.SignatureSize { + return Caller{}, ErrInvalidSignature + } + if !ed25519.Verify(pub, []byte(parts[0]+"."+parts[1]), sig) { + return Caller{}, ErrInvalidSignature + } + payload, err := decodeSegment(parts[1]) + if err != nil { + return Caller{}, fmt.Errorf("%w: payload is not base64url", ErrMalformedToken) + } + var c claims + if err := json.Unmarshal(payload, &c); err != nil { + return Caller{}, fmt.Errorf("%w: payload is not a claims object", ErrInvalidClaims) + } + return c.caller(v.issuer, v.audience, v.now()) +} + +func (v *Verifier) refreshKeys(ctx context.Context) { + err := v.keys.refresh(ctx) + switch { + case err == nil: + case v.keys.count() == 0: + v.logger.ErrorContext(ctx, "scheduled jwks refresh failed and no verification key is held; every call is rejected", "err", err) + default: + v.logger.WarnContext(ctx, "scheduled jwks refresh failed; continuing with the keys held", "err", err) + } +} + +// Ready reports whether the verifier can accept a call: it is in insecure +// mode or holds at least one verification key. +func (v *Verifier) Ready() bool { + return v.insecure || v.keys.count() > 0 +} + +type header struct { + Alg string `json:"alg"` + Typ string `json:"typ"` + Kid string `json:"kid"` +} + +// checkHeader decodes the JOSE header and returns its kid once alg and typ +// match the OpenShell extension token contract. +func checkHeader(segment string) (string, error) { + raw, err := decodeSegment(segment) + if err != nil { + return "", fmt.Errorf("%w: header is not base64url", ErrMalformedToken) + } + var h header + if err := json.Unmarshal(raw, &h); err != nil { + return "", fmt.Errorf("%w: header is not a JSON object", ErrMalformedToken) + } + switch { + case h.Alg != algEdDSA: + return "", ErrUnsupportedAlg + case h.Typ != headerTyp: + return "", ErrUnexpectedTokenType + case h.Kid == "": + return "", fmt.Errorf("%w: kid is required", ErrUnknownKey) + } + return h.Kid, nil +} + +func decodeSegment(s string) ([]byte, error) { + return base64.RawURLEncoding.Strict().DecodeString(s) +} + +type claims struct { + Issuer string `json:"iss"` + Audience string `json:"aud"` + Subject string `json:"sub"` + IssuedAt *int64 `json:"iat"` + ExpiresAt *int64 `json:"exp"` + ID string `json:"jti"` + CallerKind string `json:"caller_kind"` + SandboxID string `json:"sandbox_id"` +} + +func (c claims) caller(issuer, audience string, now time.Time) (Caller, error) { + if err := c.checkPresence(); err != nil { + return Caller{}, err + } + if c.Issuer != issuer { + return Caller{}, ErrIssuerMismatch + } + if c.Audience != audience { + return Caller{}, ErrAudienceMismatch + } + if err := checkTimes(*c.IssuedAt, *c.ExpiresAt, now); err != nil { + return Caller{}, err + } + kind := CallerKind(c.CallerKind) + switch kind { + case CallerGateway: + if c.SandboxID != "" { + return Caller{}, fmt.Errorf("%w: sandbox_id is only valid for supervisor callers", ErrInvalidCaller) + } + case CallerSupervisor: + if c.SandboxID == "" { + return Caller{}, fmt.Errorf("%w: supervisor callers require sandbox_id", ErrInvalidCaller) + } + default: + return Caller{}, fmt.Errorf("%w: unknown caller_kind", ErrInvalidCaller) + } + return Caller{Kind: kind, Issuer: c.Issuer, Subject: c.Subject, SandboxID: c.SandboxID}, nil +} + +func (c claims) checkPresence() error { + switch { + case c.IssuedAt == nil || c.ExpiresAt == nil: + return fmt.Errorf("%w: iat and exp are required", ErrInvalidClaims) + case *c.IssuedAt < 0 || *c.ExpiresAt < 0: + return fmt.Errorf("%w: iat and exp must not be negative", ErrInvalidClaims) + case c.Subject == "": + return fmt.Errorf("%w: sub is required", ErrInvalidClaims) + case c.ID == "": + return fmt.Errorf("%w: jti is required", ErrInvalidClaims) + } + return nil +} + +func checkTimes(iat, exp int64, now time.Time) error { + unix := now.Unix() + skew := int64(clockSkew.Seconds()) + switch { + case unix >= exp+skew: + return ErrTokenExpired + case iat > unix+skew: + return ErrTokenNotYetValid + case exp <= iat || exp-iat > int64(maxLifetime.Seconds()): + return ErrTokenLifetime + } + return nil +} diff --git a/internal/extauth/verifier_test.go b/internal/extauth/verifier_test.go new file mode 100644 index 0000000..10afb5d --- /dev/null +++ b/internal/extauth/verifier_test.go @@ -0,0 +1,273 @@ +package extauth + +import ( + "errors" + "math" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +func set(kv ...any) func(map[string]any) { + return func(m map[string]any) { + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] + } + } +} + +func del(keys ...string) func(map[string]any) { + return func(m map[string]any) { + for _, k := range keys { + delete(m, k) + } + } +} + +func TestVerify(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + unix := now.Unix() + key := newSigner(t, "k1") + other := newSigner(t, "k2") + impostor := signer{kid: "k1", pub: other.pub, priv: other.priv} + v := fileVerifier(t, now, key) + + gateway := Caller{Kind: CallerGateway, Issuer: testIssuer, Subject: testSubject} + supervisor := Caller{Kind: CallerSupervisor, Issuer: testIssuer, Subject: "spiffe://openshell/sandbox/sb-1", SandboxID: "sb-1"} + asSupervisor := set("caller_kind", "supervisor", "sandbox_id", "sb-1", "sub", supervisor.Subject) + + tests := []struct { + name string + header func(map[string]any) + claims func(map[string]any) + signer *signer + mutate func(string) string + wantErr error + want Caller + }{ + {name: "gateway", want: gateway}, + {name: "supervisor", claims: asSupervisor, want: supervisor}, + {name: "extra header members tolerated", header: set("cty", "json", "x-build", 7), want: gateway}, + {name: "extra claims tolerated", claims: set("nbf", unix, "scope", "x"), want: gateway}, + {name: "exp inside leeway", claims: set("iat", unix-600, "exp", unix-29), want: gateway}, + {name: "iat inside leeway", claims: set("iat", unix+29, "exp", unix+329), want: gateway}, + {name: "lifetime exactly one hour", claims: set("iat", unix, "exp", unix+3600), want: gateway}, + {name: "too large", mutate: func(tok string) string { return tok + strings.Repeat("a", maxTokenBytes) }, wantErr: ErrTokenTooLarge}, + {name: "two segments", mutate: func(tok string) string { return tok[:strings.LastIndex(tok, ".")] }, wantErr: ErrMalformedToken}, + {name: "four segments", mutate: func(tok string) string { return tok + ".extra" }, wantErr: ErrMalformedToken}, + {name: "padded header", mutate: func(tok string) string { return strings.Replace(tok, ".", "==.", 1) }, wantErr: ErrMalformedToken}, + {name: "header not json", mutate: replaceSegment(0, b64([]byte("not json"))), wantErr: ErrMalformedToken}, + {name: "header is array", mutate: replaceSegment(0, b64([]byte("[]"))), wantErr: ErrMalformedToken}, + {name: "alg RS256", header: set("alg", "RS256"), wantErr: ErrUnsupportedAlg}, + {name: "alg lowercase", header: set("alg", "eddsa"), wantErr: ErrUnsupportedAlg}, + {name: "alg none", header: set("alg", "none"), wantErr: ErrUnsupportedAlg}, + {name: "alg missing", header: del("alg"), wantErr: ErrUnsupportedAlg}, + {name: "typ JWT", header: set("typ", "JWT"), wantErr: ErrUnexpectedTokenType}, + {name: "typ missing", header: del("typ"), wantErr: ErrUnexpectedTokenType}, + {name: "kid missing", header: del("kid"), wantErr: ErrUnknownKey}, + {name: "kid unknown", signer: &other, wantErr: ErrUnknownKey}, + {name: "wrong key for kid", signer: &impostor, wantErr: ErrInvalidSignature}, + {name: "tampered payload", mutate: replaceSegment(1, b64([]byte(`{"iss":"x"}`))), wantErr: ErrInvalidSignature}, + {name: "signature not base64", mutate: replaceSegment(2, "!!!"), wantErr: ErrInvalidSignature}, + {name: "signature wrong length", mutate: replaceSegment(2, b64([]byte("short"))), wantErr: ErrInvalidSignature}, + {name: "payload not base64", mutate: resign(key, 1, "!!!"), wantErr: ErrMalformedToken}, + {name: "payload not an object", mutate: resign(key, 1, b64([]byte("[]"))), wantErr: ErrInvalidClaims}, + {name: "aud array", claims: set("aud", []string{testAudience}), wantErr: ErrInvalidClaims}, + {name: "aud number", claims: set("aud", 42), wantErr: ErrInvalidClaims}, + {name: "iat float", claims: set("iat", 1.5), wantErr: ErrInvalidClaims}, + {name: "iat string", claims: set("iat", "123"), wantErr: ErrInvalidClaims}, + {name: "iat missing", claims: del("iat"), wantErr: ErrInvalidClaims}, + {name: "exp missing", claims: del("exp"), wantErr: ErrInvalidClaims}, + {name: "iat negative", claims: set("iat", -5), wantErr: ErrInvalidClaims}, + {name: "sub missing", claims: del("sub"), wantErr: ErrInvalidClaims}, + {name: "sub empty", claims: set("sub", ""), wantErr: ErrInvalidClaims}, + {name: "jti missing", claims: del("jti"), wantErr: ErrInvalidClaims}, + {name: "jti empty", claims: set("jti", ""), wantErr: ErrInvalidClaims}, + {name: "issuer mismatch", claims: set("iss", "openshell-gateway:other"), wantErr: ErrIssuerMismatch}, + {name: "issuer missing", claims: del("iss"), wantErr: ErrIssuerMismatch}, + {name: "audience mismatch", claims: set("aud", "urn:openshell:extension:interceptor:ans"), wantErr: ErrAudienceMismatch}, + {name: "expired at leeway edge", claims: set("iat", unix-600, "exp", unix-30), wantErr: ErrTokenExpired}, + {name: "expired long ago", claims: set("iat", unix-7260, "exp", unix-7200), wantErr: ErrTokenExpired}, + {name: "exp overflows", claims: set("iat", unix, "exp", int64(math.MaxInt64)), wantErr: ErrTokenExpired}, + {name: "iat beyond leeway", claims: set("iat", unix+31, "exp", unix+331), wantErr: ErrTokenNotYetValid}, + {name: "lifetime over one hour", claims: set("iat", unix, "exp", unix+3601), wantErr: ErrTokenLifetime}, + {name: "exp before iat", claims: set("iat", unix, "exp", unix-1), wantErr: ErrTokenLifetime}, + {name: "exp equals iat", claims: set("iat", unix, "exp", unix), wantErr: ErrTokenLifetime}, + {name: "caller_kind missing", claims: del("caller_kind"), wantErr: ErrInvalidCaller}, + {name: "caller_kind unknown", claims: set("caller_kind", "admin"), wantErr: ErrInvalidCaller}, + {name: "supervisor without sandbox_id", claims: set("caller_kind", "supervisor"), wantErr: ErrInvalidCaller}, + {name: "gateway with sandbox_id", claims: set("sandbox_id", "sb-1"), wantErr: ErrInvalidCaller}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s := key + if tt.signer != nil { + s = *tt.signer + } + hdr, claims := testHeader(s.kid), testClaims(now) + if tt.header != nil { + tt.header(hdr) + } + if tt.claims != nil { + tt.claims(claims) + } + tok := s.mint(t, hdr, claims) + if tt.mutate != nil { + tok = tt.mutate(tok) + } + got, err := v.Verify(t.Context(), tok) + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Verify() error = %v, want %v", err, tt.wantErr) + } + if seg := strings.Split(tok, ".")[0]; strings.Contains(err.Error(), seg) { + t.Fatalf("Verify() error echoes the token: %v", err) + } + return + } + if err != nil { + t.Fatalf("Verify() error = %v", err) + } + if got != tt.want { + t.Fatalf("Verify() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func replaceSegment(i int, with string) func(string) string { + return func(tok string) string { + parts := strings.Split(tok, ".") + parts[i] = with + return strings.Join(parts, ".") + } +} + +// resign replaces segment i and signs the result so the signature is valid +// over the corrupted content. +func resign(s signer, i int, with string) func(string) string { + return func(tok string) string { + parts := strings.Split(tok, ".") + parts[i] = with + return s.sign(parts[0] + "." + parts[1]) + } +} + +func TestNewVerifierErrors(t *testing.T) { + t.Parallel() + key := newSigner(t, "k1") + rsa := map[string]any{"kty": "RSA", "kid": "r1", "n": "AQAB", "e": "AQAB"} + tests := []struct { + name string + cfg config.Auth + wantErr error + }{ + {name: "invalid config", cfg: config.Auth{Audience: testAudience, JWKSFile: "x"}}, + {name: "missing file", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSFile: "/nonexistent/jwks.json"}}, + {name: "file not json", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSFile: writeFile(t, "bad.json", []byte("{"))}}, + {name: "file without usable keys", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSFile: writeFile(t, "rsa.json", jwksJSON(t, rsa))}, wantErr: ErrNoUsableKeys}, + {name: "http jwks_url", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSURL: "http://gateway/.well-known/jwks.json"}, wantErr: ErrInsecureJWKSURL}, + {name: "unparsable jwks_url", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSURL: "https://[::1"}}, + {name: "jwks_url with bad file", cfg: config.Auth{GatewayIssuer: testIssuer, Audience: testAudience, JWKSURL: "https://gateway/jwks", JWKSFile: writeFile(t, "bad.json", []byte("{"))}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + v, err := NewVerifier(tt.cfg, discardLogger()) + if err == nil { + t.Fatalf("NewVerifier() = %+v, want error", v) + } + if tt.wantErr != nil && !errors.Is(err, tt.wantErr) { + t.Fatalf("NewVerifier() error = %v, want %v", err, tt.wantErr) + } + }) + } + if _, err := NewVerifier(fileAuth(t, key), nil); err != nil { + t.Fatalf("NewVerifier(nil logger) error = %v", err) + } +} + +func TestNewVerifierInsecure(t *testing.T) { + t.Parallel() + logger, buf := bufferLogger() + v, err := NewVerifier(config.Auth{AllowInsecureNoAuth: true}, logger) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + if n := strings.Count(buf.String(), "level=WARN"); n != 1 || !strings.Contains(buf.String(), "allow_insecure_no_auth") { + t.Fatalf("expected exactly one WARN about allow_insecure_no_auth, got log:\n%s", buf.String()) + } + now := time.Unix(1_800_000_000, 0) + if _, err := v.Verify(t.Context(), gatewayToken(t, newSigner(t, "k1"), now)); !errors.Is(err, ErrUnknownKey) { + t.Fatalf("Verify() error = %v, want %v (no trust anchor in insecure mode)", err, ErrUnknownKey) + } +} + +func TestNewVerifierSkipsUnusableFileKeys(t *testing.T) { + t.Parallel() + now := time.Unix(1_800_000_000, 0) + good := newSigner(t, "good") + noKid := newSigner(t, "").jwk() + wrongAlg := newSigner(t, "wrong-alg").jwk() + wrongAlg["alg"] = "ES256" + wrongUse := newSigner(t, "wrong-use").jwk() + wrongUse["use"] = "enc" + dup := newSigner(t, "good").jwk() + rsa := map[string]any{"kty": "RSA", "kid": "r1", "n": "AQAB", "e": "AQAB"} + badX := map[string]any{"kty": "OKP", "crv": "Ed25519", "kid": "bad-x", "x": "AQAB"} + + logger, buf := bufferLogger() + cfg := config.Auth{ + GatewayIssuer: testIssuer, Audience: testAudience, + JWKSFile: writeFile(t, "jwks.json", jwksJSON(t, rsa, noKid, wrongAlg, wrongUse, badX, good.jwk(), dup)), + } + v, err := NewVerifier(cfg, logger, WithClock(fixedClock(now))) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + for _, reason := range []string{"not an Ed25519 OKP key", "missing kid", "alg is not EdDSA", "use is not sig", "base64url Ed25519", "duplicate kid"} { + if !strings.Contains(buf.String(), reason) { + t.Errorf("missing warning %q in log:\n%s", reason, buf.String()) + } + } + if _, err := v.Verify(t.Context(), gatewayToken(t, good, now)); err != nil { + t.Fatalf("Verify() with the usable key error = %v", err) + } + impostor := signer{kid: "good", priv: newSigner(t, "x").priv} + if _, err := v.Verify(t.Context(), gatewayToken(t, impostor, now)); !errors.Is(err, ErrInvalidSignature) { + t.Fatalf("Verify() with the duplicate kid's key error = %v, want %v", err, ErrInvalidSignature) + } +} + +func TestRefreshIntervalPrecedence(t *testing.T) { + t.Parallel() + key := newSigner(t, "k1") + tests := []struct { + name string + cfg time.Duration + opts []Option + want time.Duration + }{ + {name: "default", want: defaultRefreshInterval}, + {name: "config", cfg: time.Minute, want: time.Minute}, + {name: "option overrides config", cfg: time.Minute, opts: []Option{WithRefreshInterval(2 * time.Minute)}, want: 2 * time.Minute}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cfg := fileAuth(t, key) + cfg.JWKSRefreshInterval = tt.cfg + v, err := NewVerifier(cfg, discardLogger(), tt.opts...) + if err != nil { + t.Fatalf("NewVerifier() error = %v", err) + } + if v.refreshInterval != tt.want { + t.Fatalf("refreshInterval = %v, want %v", v.refreshInterval, tt.want) + } + }) + } +} diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go new file mode 100644 index 0000000..ae657b9 --- /dev/null +++ b/internal/grpcserver/server.go @@ -0,0 +1,259 @@ +// Package grpcserver starts the extension's gRPC listener with optional TLS, +// a standard health service and graceful shutdown. +package grpcserver + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "sync" + "sync/atomic" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/keepalive" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + defaultShutdownGrace = 10 * time.Second + minClientPingPeriod = 10 * time.Second + defaultMaxRecvBytes = 8 << 20 + healthReadTimeout = 5 * time.Second + keepaliveTime = 2 * time.Minute + keepaliveTimeout = 20 * time.Second + // serveErrorSlots buffers one error from each listener goroutine. + serveErrorSlots = 2 +) + +// Options configure a Server. +type Options struct { + Listen string + TLS config.TLS + // Interceptors are installed on every RPC (authentication first). + Unary []grpc.UnaryServerInterceptor + Stream []grpc.StreamServerInterceptor + // MaxRecvBytes bounds inbound messages; OpenShell sends at most 4 MiB + // bodies plus headers, so the default of 8 MiB leaves headroom. + MaxRecvBytes int + // ShutdownGrace bounds GracefulStop before Stop is forced. + ShutdownGrace time.Duration + // HealthListen, when set, serves plaintext HTTP GET /healthz (process is + // serving) and /readyz (SetHealthy state) for orchestrator probes, which + // cannot reach the authenticated, TLS gRPC health service. + HealthListen string + // DrainDelay is how long Serve reports not-ready before it stops accepting + // RPCs, so load balancers and probes see the change first. + DrainDelay time.Duration +} + +// Server owns the listener, the gRPC server and its health service. +type Server struct { + grpc *grpc.Server + health *health.Server + lis net.Listener + healthLis net.Listener + ready atomic.Bool + mu sync.Mutex + draining bool + opts Options + logger *slog.Logger +} + +// New builds the server and binds the listener so configuration errors (bad +// address, unreadable certificate) surface before any service is registered. +func New(ctx context.Context, opts Options, logger *slog.Logger) (*Server, error) { + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + if err := opts.TLS.Validate(); err != nil { + return nil, err + } + if opts.MaxRecvBytes <= 0 { + opts.MaxRecvBytes = defaultMaxRecvBytes + } + if opts.ShutdownGrace <= 0 { + opts.ShutdownGrace = defaultShutdownGrace + } + serverOpts := []grpc.ServerOption{ + grpc.MaxRecvMsgSize(opts.MaxRecvBytes), + grpc.ChainUnaryInterceptor(opts.Unary...), + grpc.ChainStreamInterceptor(opts.Stream...), + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{MinTime: minClientPingPeriod, PermitWithoutStream: true}), + grpc.KeepaliveParams(keepalive.ServerParameters{Time: keepaliveTime, Timeout: keepaliveTimeout}), + } + if opts.TLS.Enabled() { + cert, err := tls.LoadX509KeyPair(opts.TLS.CertFile, opts.TLS.KeyFile) + if err != nil { + return nil, fmt.Errorf("load serving certificate: %w", err) + } + serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS13, + }))) + } + lc := &net.ListenConfig{} + lis, err := lc.Listen(ctx, "tcp", opts.Listen) + if err != nil { + return nil, fmt.Errorf("listen on %q: %w", opts.Listen, err) + } + s := &Server{grpc: grpc.NewServer(serverOpts...), health: health.NewServer(), lis: lis, opts: opts, logger: logger} + if opts.HealthListen != "" { + if s.healthLis, err = lc.Listen(ctx, "tcp", opts.HealthListen); err != nil { + _ = lis.Close() + return nil, fmt.Errorf("listen on health address %q: %w", opts.HealthListen, err) + } + } + grpc_health_v1.RegisterHealthServer(s.grpc, s.health) + s.health.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING) + return s, nil +} + +// Registrar returns the underlying gRPC server for service registration. +func (s *Server) Registrar() grpc.ServiceRegistrar { return s.grpc } + +// Addr is the bound gRPC listener address. +func (s *Server) Addr() net.Addr { return s.lis.Addr() } + +// HealthAddr is the bound HTTP health listener address, or nil when +// HealthListen was not set. +func (s *Server) HealthAddr() net.Addr { + if s.healthLis == nil { + return nil + } + return s.healthLis.Addr() +} + +// SetHealthy flips the gRPC health service between SERVING and NOT_SERVING and +// the HTTP /readyz endpoint between 200 and 503. A new server is not ready +// until its owner says so, and once Serve starts draining it stays not ready. +// Repeating the current state changes nothing, so health watchers only see +// transitions. +func (s *Server) SetHealthy(healthy bool) { + s.mu.Lock() + defer s.mu.Unlock() + if healthy && s.draining { + return + } + if s.ready.Swap(healthy) == healthy { + return + } + status := grpc_health_v1.HealthCheckResponse_NOT_SERVING + if healthy { + status = grpc_health_v1.HealthCheckResponse_SERVING + } + s.health.SetServingStatus("", status) +} + +// drain takes readiness away for good: later SetHealthy(true) calls, such as +// a readiness tick racing the shutdown, are ignored. +func (s *Server) drain() { + s.mu.Lock() + s.draining = true + s.mu.Unlock() + s.SetHealthy(false) +} + +// WatchReadiness applies probe's verdict immediately and then on every +// interval until ctx ends, so a server is never reported ready before its owner +// has checked that it can serve. A non-positive interval applies the probe once +// and returns. +func (s *Server) WatchReadiness(ctx context.Context, interval time.Duration, probe func() bool) { + s.SetHealthy(probe()) + if interval <= 0 { + return + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + s.SetHealthy(probe()) + } + } +} + +func (s *Server) healthHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("ok\n")) + }) + mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, _ *http.Request) { + if !s.ready.Load() { + http.Error(w, "not ready", http.StatusServiceUnavailable) + return + } + _, _ = w.Write([]byte("ready\n")) + }) + return mux +} + +// Serve runs until ctx is canceled or the listener fails, then stops +// gracefully. It returns once in-flight RPCs have drained or ShutdownGrace +// has passed, whichever comes first; a handler that ignores its context past +// the grace period is abandoned to process exit. +func (s *Server) Serve(ctx context.Context) error { + errc := make(chan error, serveErrorSlots) + go func() { + s.logger.InfoContext(ctx, "grpc server listening", "addr", s.lis.Addr().String(), "tls", s.opts.TLS.Enabled()) + errc <- s.grpc.Serve(s.lis) + }() + var httpSrv *http.Server + if s.healthLis != nil { + httpSrv = &http.Server{Handler: s.healthHandler(), ReadHeaderTimeout: healthReadTimeout} + go func() { + s.logger.InfoContext(ctx, "health endpoints listening", "addr", s.healthLis.Addr().String()) + if err := httpSrv.Serve(s.healthLis); err != nil && !errors.Is(err, http.ErrServerClosed) { + errc <- fmt.Errorf("health serve: %w", err) + } + }() + } + var err error + select { + case err = <-errc: + if errors.Is(err, grpc.ErrServerStopped) { + err = nil + } else if err != nil { + err = fmt.Errorf("grpc serve: %w", err) + } + case <-ctx.Done(): + } + s.drain() + s.health.Shutdown() + if s.opts.DrainDelay > 0 { + time.Sleep(s.opts.DrainDelay) + } + s.stop() + if httpSrv != nil { + shutdownCtx, cancel := context.WithTimeout(context.Background(), s.opts.ShutdownGrace) + defer cancel() + _ = httpSrv.Shutdown(shutdownCtx) + } + return err +} + +func (s *Server) stop() { + done := make(chan struct{}) + go func() { + s.grpc.GracefulStop() + close(done) + }() + select { + case <-done: + case <-time.After(s.opts.ShutdownGrace): + // GracefulStop holds the server mutex while it waits for handlers, so + // Stop can block behind it until they return; it must not hold up exit. + go s.grpc.Stop() + s.logger.Warn("grpc shutdown grace exceeded; abandoning in-flight handlers") + } +} diff --git a/internal/grpcserver/server_test.go b/internal/grpcserver/server_test.go new file mode 100644 index 0000000..ae574be --- /dev/null +++ b/internal/grpcserver/server_test.go @@ -0,0 +1,434 @@ +package grpcserver + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "net/http" + "os" + "path/filepath" + "sync/atomic" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +func TestNew_Errors(t *testing.T) { + t.Parallel() + dir := t.TempDir() + tests := []struct { + name string + opts Options + }{ + {name: "half tls", opts: Options{Listen: "127.0.0.1:0", TLS: config.TLS{CertFile: "c"}}}, + {name: "missing cert files", opts: Options{Listen: "127.0.0.1:0", TLS: config.TLS{CertFile: filepath.Join(dir, "no.crt"), KeyFile: filepath.Join(dir, "no.key")}}}, + {name: "bad address", opts: Options{Listen: "256.256.256.256:99999"}}, + {name: "bad health address", opts: Options{Listen: "127.0.0.1:0", HealthListen: "256.256.256.256:99999"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if _, err := New(context.Background(), tt.opts, nil); err == nil { + t.Fatal("New() = nil error") + } + }) + } +} + +func TestServe_PlaintextHealthAndShutdown(t *testing.T) { + t.Parallel() + var seen int + count := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) { + seen++ + return h(ctx, req) + } + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0", Unary: []grpc.UnaryServerInterceptor{count}, ShutdownGrace: time.Second}, nil) + if err != nil { + t.Fatalf("New() error = %v", err) + } + s.SetHealthy(true) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Serve(ctx) }() + + conn, err := grpc.NewClient(s.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + hc := grpc_health_v1.NewHealthClient(conn) + resp, err := hc.Check(ctx, &grpc_health_v1.HealthCheckRequest{}) + if err != nil || resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING { + t.Fatalf("health = %v, %v; want SERVING", resp.GetStatus(), err) + } + s.SetHealthy(false) + resp, err = hc.Check(ctx, &grpc_health_v1.HealthCheckRequest{}) + if err != nil || resp.GetStatus() != grpc_health_v1.HealthCheckResponse_NOT_SERVING { + t.Fatalf("health = %v, %v; want NOT_SERVING", resp.GetStatus(), err) + } + s.SetHealthy(true) + if seen != 2 { + t.Fatalf("interceptor saw %d calls, want 2", seen) + } + cancel() + select { + case err := <-done: + if err != nil { + t.Fatalf("Serve() error = %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Serve() did not return after cancel") + } + if _, err := hc.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{}); status.Code(err) == codes.OK { + t.Fatal("health check succeeded after shutdown") + } +} + +func TestServe_TLS(t *testing.T) { + t.Parallel() + tc := selfSigned(t) + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0", TLS: config.TLS{CertFile: tc.certFile, KeyFile: tc.keyFile}}, nil) + if err != nil { + t.Fatalf("New() error = %v", err) + } + s.SetHealthy(true) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { _ = s.Serve(ctx) }() + + creds := credentials.NewTLS(&tls.Config{RootCAs: tc.pool, ServerName: "localhost", MinVersion: tls.VersionTLS13}) + conn, err := grpc.NewClient(s.Addr().String(), grpc.WithTransportCredentials(creds)) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + resp, err := grpc_health_v1.NewHealthClient(conn).Check(ctx, &grpc_health_v1.HealthCheckRequest{}) + if err != nil || resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING { + t.Fatalf("health over TLS = %v, %v", resp.GetStatus(), err) + } + plain, err := grpc.NewClient(s.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + defer plain.Close() + shortCtx, shortCancel := context.WithTimeout(ctx, 2*time.Second) + defer shortCancel() + if _, err := grpc_health_v1.NewHealthClient(plain).Check(shortCtx, &grpc_health_v1.HealthCheckRequest{}); err == nil { + t.Fatal("plaintext client reached a TLS server") + } +} + +func TestServe_ListenerFailure(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0"}, nil) + if err != nil { + t.Fatal(err) + } + if err := s.lis.Close(); err != nil { + t.Fatal(err) + } + if err := s.Serve(context.Background()); err == nil { + t.Fatal("Serve() = nil on a closed listener") + } +} + +type testCert struct { + certFile, keyFile string + pool *x509.CertPool +} + +func selfSigned(t *testing.T) testCert { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "localhost"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + certFile := filepath.Join(dir, "tls.crt") + keyFile := filepath.Join(dir, "tls.key") + if err := os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), 0o600); err != nil { + t.Fatal(err) + } + pool := x509.NewCertPool() + pool.AddCert(mustParse(t, der)) + return testCert{certFile: certFile, keyFile: keyFile, pool: pool} +} + +func TestServe_ForcedStopAfterGrace(t *testing.T) { + t.Parallel() + entered := make(chan struct{}) + release := make(chan struct{}) + block := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) { + close(entered) + <-release + return h(ctx, req) + } + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0", Unary: []grpc.UnaryServerInterceptor{block}, ShutdownGrace: 50 * time.Millisecond}, nil) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Serve(ctx) }() + conn, err := grpc.NewClient(s.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + callErr := make(chan error, 1) + go func() { + _, err := grpc_health_v1.NewHealthClient(conn).Check(context.Background(), &grpc_health_v1.HealthCheckRequest{}) + callErr <- err + }() + select { + case <-entered: + case <-time.After(10 * time.Second): + t.Fatal("the blocking call never reached the server") + } + cancel() + select { + case err := <-done: + if err != nil { + t.Fatalf("Serve() error = %v", err) + } + case <-time.After(10 * time.Second): + t.Fatal("Serve() did not return: forced stop did not happen") + } + close(release) + <-callErr +} + +func mustParse(t *testing.T, der []byte) *x509.Certificate { + t.Helper() + c, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return c +} + +func TestServe_HealthEndpoints(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0", HealthListen: "127.0.0.1:0"}, nil) + if err != nil { + t.Fatalf("New() error = %v", err) + } + s.SetHealthy(true) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Serve(ctx) }() + base := "http://" + s.HealthAddr().String() + get := func(path string) int { + t.Helper() + var code int + for range 50 { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil) + resp, err := http.DefaultClient.Do(req) + if err == nil { + code = resp.StatusCode + _ = resp.Body.Close() + return code + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("GET %s never answered", path) + return 0 + } + if got := get("/healthz"); got != http.StatusOK { + t.Fatalf("/healthz = %d, want 200", got) + } + if got := get("/readyz"); got != http.StatusOK { + t.Fatalf("/readyz = %d, want 200 while healthy", got) + } + s.SetHealthy(false) + if got := get("/readyz"); got != http.StatusServiceUnavailable { + t.Fatalf("/readyz = %d, want 503 when unhealthy", got) + } + if got := get("/healthz"); got != http.StatusOK { + t.Fatalf("/healthz = %d, want 200 regardless of readiness", got) + } + cancel() + if err := <-done; err != nil { + t.Fatalf("Serve() error = %v", err) + } + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, base+"/healthz", nil) + if resp, err := http.DefaultClient.Do(req); err == nil { + _ = resp.Body.Close() + t.Fatal("health listener still answering after shutdown") + } +} + +func TestHealthAddr_NilWithoutListener(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0"}, nil) + if err != nil { + t.Fatal(err) + } + defer s.lis.Close() + if s.HealthAddr() != nil { + t.Fatal("HealthAddr() != nil without HealthListen") + } +} + +func TestServe_DrainDelayReportsNotReadyFirst(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0", HealthListen: "127.0.0.1:0", DrainDelay: 300 * time.Millisecond}, nil) + if err != nil { + t.Fatal(err) + } + s.SetHealthy(true) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Serve(ctx) }() + base := "http://" + s.HealthAddr().String() + var code int + for range 50 { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+"/readyz", nil) + if resp, err := http.DefaultClient.Do(req); err == nil { + code = resp.StatusCode + _ = resp.Body.Close() + break + } + time.Sleep(20 * time.Millisecond) + } + if code != http.StatusOK { + t.Fatalf("/readyz before the drain = %d, want 200", code) + } + started := time.Now() + cancel() + time.Sleep(50 * time.Millisecond) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, base+"/readyz", nil) + resp, getErr := http.DefaultClient.Do(req) + if getErr != nil { + t.Fatalf("health listener closed before the drain delay elapsed: %v", getErr) + } + _ = resp.Body.Close() + if resp.StatusCode != http.StatusServiceUnavailable { + t.Fatalf("/readyz during drain = %d, want 503", resp.StatusCode) + } + s.SetHealthy(true) + if s.ready.Load() { + t.Fatal("SetHealthy(true) re-advertised readiness during the drain") + } + if err := <-done; err != nil { + t.Fatalf("Serve() error = %v", err) + } + if elapsed := time.Since(started); elapsed < 300*time.Millisecond { + t.Fatalf("Serve() returned after %v, before the drain delay", elapsed) + } +} + +func TestNewStartsNotReady(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0"}, nil) + if err != nil { + t.Fatal(err) + } + defer s.lis.Close() + if s.ready.Load() { + t.Fatal("New() reports ready before the owner asserted readiness") + } + resp, err := s.health.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{}) + if err != nil || resp.GetStatus() != grpc_health_v1.HealthCheckResponse_NOT_SERVING { + t.Fatalf("health after New() = %v, %v; want NOT_SERVING", resp.GetStatus(), err) + } +} + +func TestWatchReadiness(t *testing.T) { + t.Parallel() + tests := []struct { + name string + verdicts []bool + interval time.Duration + want bool + }{ + {name: "not ready is applied before the first tick", verdicts: []bool{false}, want: false}, + {name: "ready is applied before the first tick", verdicts: []bool{true}, want: true}, + {name: "recovery is applied on a tick", verdicts: []bool{false, true}, interval: 5 * time.Millisecond, want: true}, + {name: "loss is applied on a tick", verdicts: []bool{true, false}, interval: 5 * time.Millisecond, want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s, err := New(context.Background(), Options{Listen: "127.0.0.1:0"}, nil) + if err != nil { + t.Fatal(err) + } + defer s.lis.Close() + var calls atomic.Int32 + probe := func() bool { + return tt.verdicts[min(int(calls.Add(1))-1, len(tt.verdicts)-1)] + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan struct{}) + go func() { + s.WatchReadiness(ctx, tt.interval, probe) + close(done) + }() + if tt.interval == 0 { + <-done + if calls.Load() != 1 { + t.Fatalf("probe called %d times, want once without an interval", calls.Load()) + } + } else { + waitUntil(t, "every scripted verdict to be applied", func() bool { + return int(calls.Load()) >= len(tt.verdicts) && s.ready.Load() == tt.want + }) + } + if s.ready.Load() != tt.want { + t.Fatalf("ready = %v, want %v", s.ready.Load(), tt.want) + } + cancel() + <-done + }) + } +} + +func waitUntil(t *testing.T, what string, cond func() bool) { + t.Helper() + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + if cond() { + return + } + time.Sleep(time.Millisecond) + } + t.Fatalf("timed out waiting for %s", what) +} diff --git a/internal/hydrator/agentmap.go b/internal/hydrator/agentmap.go new file mode 100644 index 0000000..5f4fdf9 --- /dev/null +++ b/internal/hydrator/agentmap.go @@ -0,0 +1,88 @@ +package hydrator + +import ( + "errors" + "fmt" +) + +// AgentMapping attributes one ANS agent to the OpenShell sandboxes it runs in. +// The YAML tags match the hydrator's configuration block. +type AgentMapping struct { + AgentID string `yaml:"agent_id"` + AnsName string `yaml:"ans_name"` + SandboxUIDs []string `yaml:"sandbox_uids"` + SandboxNames []string `yaml:"sandbox_names"` +} + +// AgentMap resolves events to agents. The zero value maps nothing. +type AgentMap struct { + byUID map[string]AgentMapping + byName map[string]AgentMapping +} + +// NewAgentMap validates the mappings and indexes them. Every mapping needs a +// non-empty agent_id and at least one sandbox uid or name; an agent_id, a +// sandbox uid or a sandbox name may appear only once. +func NewAgentMap(mappings []AgentMapping) (AgentMap, error) { + m := AgentMap{ + byUID: make(map[string]AgentMapping, len(mappings)), + byName: make(map[string]AgentMapping, len(mappings)), + } + seenAgents := make(map[string]struct{}, len(mappings)) + for i, mapping := range mappings { + if mapping.AgentID == "" { + return AgentMap{}, fmt.Errorf("hydrator: agents[%d]: agent_id is required", i) + } + if _, dup := seenAgents[mapping.AgentID]; dup { + return AgentMap{}, fmt.Errorf("hydrator: agent %q is mapped more than once", mapping.AgentID) + } + seenAgents[mapping.AgentID] = struct{}{} + if len(mapping.SandboxUIDs) == 0 && len(mapping.SandboxNames) == 0 { + return AgentMap{}, fmt.Errorf("hydrator: agent %q names no sandbox_uids or sandbox_names", mapping.AgentID) + } + if err := index(m.byUID, "sandbox uid", mapping.SandboxUIDs, mapping); err != nil { + return AgentMap{}, err + } + if err := index(m.byName, "sandbox name", mapping.SandboxNames, mapping); err != nil { + return AgentMap{}, err + } + } + return m, nil +} + +// Lookup resolves the event's sandbox to an agent, by container.uid first and +// container.name second. Events without a container never resolve. +func (m AgentMap) Lookup(ev Event) (AgentMapping, bool) { + if ev.Container == nil { + return AgentMapping{}, false + } + if ev.Container.UID != "" { + if mapping, ok := m.byUID[ev.Container.UID]; ok { + return mapping, true + } + } + if ev.Container.Name != "" { + if mapping, ok := m.byName[ev.Container.Name]; ok { + return mapping, true + } + } + return AgentMapping{}, false +} + +// Len returns the number of sandbox keys (uids and names) the map resolves. +func (m AgentMap) Len() int { + return len(m.byUID) + len(m.byName) +} + +func index(into map[string]AgentMapping, kind string, keys []string, mapping AgentMapping) error { + for _, key := range keys { + if key == "" { + return errors.New("hydrator: agent " + mapping.AgentID + ": empty " + kind) + } + if prev, dup := into[key]; dup { + return fmt.Errorf("hydrator: %s %q is mapped to both %q and %q", kind, key, prev.AgentID, mapping.AgentID) + } + into[key] = mapping + } + return nil +} diff --git a/internal/hydrator/agentmap_test.go b/internal/hydrator/agentmap_test.go new file mode 100644 index 0000000..a9829ae --- /dev/null +++ b/internal/hydrator/agentmap_test.go @@ -0,0 +1,102 @@ +package hydrator + +import ( + "strings" + "testing" +) + +func TestNewAgentMap(t *testing.T) { + t.Parallel() + tests := []struct { + name string + mappings []AgentMapping + wantLen int + wantErr string + }{ + { + name: "two agents", + mappings: []AgentMapping{ + {AgentID: "a", SandboxUIDs: []string{"u1", "u2"}, SandboxNames: []string{"n1"}}, + {AgentID: "b", SandboxNames: []string{"n2"}}, + }, + wantLen: 4, + }, + {name: "empty", mappings: nil, wantLen: 0}, + {name: "missing agent id", mappings: []AgentMapping{{SandboxUIDs: []string{"u1"}}}, wantErr: "agents[0]: agent_id is required"}, + { + name: "duplicate agent id", + mappings: []AgentMapping{{AgentID: "a", SandboxUIDs: []string{"u1"}}, {AgentID: "a", SandboxUIDs: []string{"u2"}}}, + wantErr: `agent "a" is mapped more than once`, + }, + {name: "no sandboxes", mappings: []AgentMapping{{AgentID: "a"}}, wantErr: "names no sandbox_uids or sandbox_names"}, + {name: "empty uid", mappings: []AgentMapping{{AgentID: "a", SandboxUIDs: []string{""}}}, wantErr: "empty sandbox uid"}, + {name: "empty name", mappings: []AgentMapping{{AgentID: "a", SandboxNames: []string{""}}}, wantErr: "empty sandbox name"}, + { + name: "uid shared by two agents", + mappings: []AgentMapping{{AgentID: "a", SandboxUIDs: []string{"u1"}}, {AgentID: "b", SandboxUIDs: []string{"u1"}}}, + wantErr: `sandbox uid "u1" is mapped to both "a" and "b"`, + }, + { + name: "name shared by two agents", + mappings: []AgentMapping{{AgentID: "a", SandboxNames: []string{"n"}}, {AgentID: "b", SandboxNames: []string{"n"}}}, + wantErr: `sandbox name "n" is mapped to both "a" and "b"`, + }, + {name: "uid repeated within one agent", mappings: []AgentMapping{{AgentID: "a", SandboxUIDs: []string{"u", "u"}}}, wantErr: "mapped to both"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + m, err := NewAgentMap(tt.mappings) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewAgentMap() error = %v, want containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("NewAgentMap() error = %v", err) + } + if m.Len() != tt.wantLen { + t.Fatalf("Len() = %d, want %d", m.Len(), tt.wantLen) + } + }) + } +} + +func TestAgentMapLookup(t *testing.T) { + t.Parallel() + m := mustAgentMap(t, + AgentMapping{AgentID: "a", AnsName: "ans://v1.0.0.a.example.com", SandboxUIDs: []string{"u-a"}, SandboxNames: []string{"n-a"}}, + AgentMapping{AgentID: "b", SandboxNames: []string{"n-b"}}, + ) + tests := []struct { + name string + m AgentMap + container *Container + wantID string + }{ + {name: "by uid", m: m, container: &Container{UID: "u-a", Name: "renamed"}, wantID: "a"}, + {name: "by name when uid unknown", m: m, container: &Container{UID: "other", Name: "n-a"}, wantID: "a"}, + {name: "uid wins over another agent's name", m: m, container: &Container{UID: "u-a", Name: "n-b"}, wantID: "a"}, + {name: "name only", m: m, container: &Container{Name: "n-b"}, wantID: "b"}, + {name: "unknown sandbox", m: m, container: &Container{UID: "x", Name: "y"}}, + {name: "empty container", m: m, container: &Container{}}, + {name: "no container", m: m}, + {name: "zero map", m: AgentMap{}, container: &Container{UID: "u-a"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, ok := tt.m.Lookup(Event{Container: tt.container}) + if ok != (tt.wantID != "") { + t.Fatalf("Lookup() ok = %v, want %v", ok, tt.wantID != "") + } + if got.AgentID != tt.wantID { + t.Fatalf("Lookup() agent = %q, want %q", got.AgentID, tt.wantID) + } + if ok && tt.wantID == "a" && got.AnsName != "ans://v1.0.0.a.example.com" { + t.Fatalf("Lookup() mapping = %+v, lost ans_name", got) + } + }) + } +} diff --git a/internal/hydrator/aggregate.go b/internal/hydrator/aggregate.go new file mode 100644 index 0000000..f9d0bf7 --- /dev/null +++ b/internal/hydrator/aggregate.go @@ -0,0 +1,157 @@ +package hydrator + +import "time" + +// maxDenyReasons caps the distinct status_detail values kept per agent; +// further values are summed under denyReasonOther. +const maxDenyReasons = 50 + +// maxDistinctDestinations bounds the per-agent destination set so a flood of +// unique hosts in the audit stream cannot grow memory without limit; the +// reported distinct-destination count saturates at this value. +const maxDistinctDestinations = 10_000 + +const ( + denyReasonUnspecified = "unspecified" + denyReasonOther = "other" +) + +// Window is the observation period, inclusive at both ends. +type Window struct { + Start time.Time + End time.Time +} + +// Contains reports whether t falls inside the window. +func (w Window) Contains(t time.Time) bool { + return !t.Before(w.Start) && !t.After(w.End) +} + +type agentStats struct { + mapping AgentMapping + events int + total int + denied int + classes map[int]int + denyReasons map[string]int + destinations map[string]struct{} + findings map[int]int +} + +func newAgentStats(mapping AgentMapping) *agentStats { + return &agentStats{ + mapping: mapping, + classes: map[int]int{ClassNetworkActivity: 0, ClassHTTPActivity: 0}, + denyReasons: map[string]int{}, + destinations: map[string]struct{}{}, + findings: map[int]int{}, + } +} + +func (s *agentStats) add(ev Event) { + s.events++ + switch { + case ev.IsNetwork(): + s.addNetwork(ev) + case ev.ClassUID == ClassDetectionFinding: + s.findings[ev.SeverityID]++ + } +} + +func (s *agentStats) addNetwork(ev Event) { + s.total++ + s.classes[ev.ClassUID]++ + if dst := ev.Destination(); dst != "" { + if len(s.destinations) < maxDistinctDestinations { + s.destinations[dst] = struct{}{} + } + } + if !ev.Denied() { + return + } + s.denied++ + s.denyReasons[s.denyReasonKey(ev.StatusDetail)]++ +} + +func (s *agentStats) denyReasonKey(detail string) string { + if detail == "" { + detail = denyReasonUnspecified + } + if _, known := s.denyReasons[detail]; known || len(s.denyReasons) < maxDenyReasons { + return detail + } + return denyReasonOther +} + +func (s *agentStats) rate() float64 { + if s.total == 0 { + return 0 + } + return float64(s.denied) / float64(s.total) +} + +// aggregator attributes events to agents and tracks the input window. +type aggregator struct { + agents AgentMap + window *Window + earliest time.Time + latest time.Time + stats map[string]*agentStats + unmapped int + noContainer int + outOfWindow int + attributed int +} + +func newAggregator(agents AgentMap, window *Window) *aggregator { + return &aggregator{agents: agents, window: window, stats: map[string]*agentStats{}} +} + +func (a *aggregator) add(ev Event) { + ts, hasTime := ev.Timestamp() + if a.window != nil { + if !hasTime || !a.window.Contains(ts) { + a.outOfWindow++ + return + } + } else if hasTime { + a.extend(ts) + } + mapping, ok := a.agents.Lookup(ev) + if !ok { + a.unmapped++ + if ev.Container == nil { + a.noContainer++ + } + return + } + a.attributed++ + st, seen := a.stats[mapping.AgentID] + if !seen { + st = newAgentStats(mapping) + a.stats[mapping.AgentID] = st + } + st.add(ev) +} + +func (a *aggregator) extend(ts time.Time) { + if a.earliest.IsZero() || ts.Before(a.earliest) { + a.earliest = ts + } + if ts.After(a.latest) { + a.latest = ts + } +} + +// resolveWindow returns the explicit window, else the span of event times, +// else a window collapsed onto now when no event carried a timestamp. +func (a *aggregator) resolveWindow(now time.Time) Window { + if a.window != nil { + return *a.window + } + if a.latest.IsZero() { + now = now.UTC() + return Window{Start: now, End: now} + } + return Window{Start: a.earliest, End: a.latest} +} diff --git a/internal/hydrator/aggregate_test.go b/internal/hydrator/aggregate_test.go new file mode 100644 index 0000000..a775cd0 --- /dev/null +++ b/internal/hydrator/aggregate_test.go @@ -0,0 +1,144 @@ +package hydrator + +import ( + "fmt" + "reflect" + "testing" + "time" +) + +func TestAggregatorNetworkMath(t *testing.T) { + t.Parallel() + agg := newAggregator(mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), nil) + events := []Event{ + netEvent(ClassNetworkActivity, "sb", false, "", "api.github.com", "", 1000), + netEvent(ClassNetworkActivity, "sb", true, "transparent_tcp_policy_denied", "github.com", "198.18.0.2", 2000), + {ClassUID: ClassNetworkActivity, Container: &Container{UID: "sb"}, StatusID: deniedID, DstEndpoint: &Endpoint{IP: "198.18.0.3"}}, + {ClassUID: ClassHTTPActivity, Container: &Container{UID: "sb"}, DispositionID: deniedID, DstEndpoint: &Endpoint{Domain: "api.github.com"}}, + netEvent(ClassHTTPActivity, "sb", false, "", "", "", 3000), + {ClassUID: ClassDetectionFinding, Container: &Container{UID: "sb"}, SeverityID: 3}, + {ClassUID: ClassDetectionFinding, Container: &Container{UID: "sb"}, SeverityID: 3}, + {ClassUID: ClassDetectionFinding, Container: &Container{UID: "sb"}, SeverityID: 5}, + {ClassUID: ClassProcessActivity, Container: &Container{UID: "sb"}, StatusID: deniedID}, + } + for _, ev := range events { + agg.add(ev) + } + st := agg.stats["a"] + if st == nil { + t.Fatal("no stats for agent a") + } + if st.events != 9 || st.total != 5 || st.denied != 3 { + t.Fatalf("events/total/denied = %d/%d/%d, want 9/5/3", st.events, st.total, st.denied) + } + if want := map[int]int{ClassNetworkActivity: 3, ClassHTTPActivity: 2}; !reflect.DeepEqual(st.classes, want) { + t.Fatalf("classes = %v, want %v", st.classes, want) + } + if want := map[string]int{"transparent_tcp_policy_denied": 1, denyReasonUnspecified: 2}; !reflect.DeepEqual(st.denyReasons, want) { + t.Fatalf("denyReasons = %v, want %v", st.denyReasons, want) + } + if len(st.destinations) != 3 { + t.Fatalf("destinations = %v, want 3 distinct", st.destinations) + } + if want := map[int]int{3: 2, 5: 1}; !reflect.DeepEqual(st.findings, want) { + t.Fatalf("findings = %v, want %v", st.findings, want) + } + if got := st.rate(); got != 0.6 { + t.Fatalf("rate() = %v, want 0.6", got) + } + if agg.unmapped != 0 || agg.outOfWindow != 0 || agg.attributed != 9 { + t.Fatalf("unmapped/outOfWindow/attributed = %d/%d/%d, want 0/0/9", agg.unmapped, agg.outOfWindow, agg.attributed) + } +} + +func TestAgentStatsRateWithoutTraffic(t *testing.T) { + t.Parallel() + st := newAgentStats(AgentMapping{AgentID: "a"}) + st.add(Event{ClassUID: ClassDetectionFinding, SeverityID: 1}) + if st.total != 0 || st.rate() != 0 { + t.Fatalf("total/rate = %d/%v, want 0/0", st.total, st.rate()) + } +} + +func TestAgentStatsDenyReasonCap(t *testing.T) { + t.Parallel() + st := newAgentStats(AgentMapping{AgentID: "a"}) + for i := range maxDenyReasons + 10 { + st.add(netEvent(ClassNetworkActivity, "", true, fmt.Sprintf("reason_%02d", i), "h", "", 0)) + } + st.add(netEvent(ClassNetworkActivity, "", true, "reason_00", "h", "", 0)) + if len(st.denyReasons) != maxDenyReasons+1 { + t.Fatalf("len(denyReasons) = %d, want %d distinct plus %q", len(st.denyReasons), maxDenyReasons, denyReasonOther) + } + if st.denyReasons[denyReasonOther] != 10 { + t.Fatalf("denyReasons[other] = %d, want 10", st.denyReasons[denyReasonOther]) + } + if st.denyReasons["reason_00"] != 2 { + t.Fatalf("denyReasons[reason_00] = %d, want 2 (known keys keep counting after the cap)", st.denyReasons["reason_00"]) + } + if _, kept := st.denyReasons[fmt.Sprintf("reason_%02d", maxDenyReasons)]; kept { + t.Fatal("a reason beyond the cap was kept as its own key") + } + if st.denied != maxDenyReasons+11 { + t.Fatalf("denied = %d", st.denied) + } +} + +func TestAggregatorDerivedWindow(t *testing.T) { + t.Parallel() + agg := newAggregator(mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), nil) + agg.add(netEvent(ClassNetworkActivity, "sb", false, "", "h", "", 3000)) + agg.add(netEvent(ClassNetworkActivity, "", false, "", "h", "", 1000)) + agg.add(netEvent(ClassNetworkActivity, "other", false, "", "h", "", 5000)) + agg.add(netEvent(ClassNetworkActivity, "sb", false, "", "h", "", 0)) + now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) + want := Window{Start: time.UnixMilli(1000).UTC(), End: time.UnixMilli(5000).UTC()} + if got := agg.resolveWindow(now); got != want { + t.Fatalf("resolveWindow() = %v, want %v", got, want) + } + if agg.unmapped != 2 || agg.noContainer != 1 || agg.outOfWindow != 0 { + t.Fatalf("unmapped/noContainer/outOfWindow = %d/%d/%d, want 2/1/0", agg.unmapped, agg.noContainer, agg.outOfWindow) + } + if agg.stats["a"].events != 2 { + t.Fatalf("events for a = %d, want 2 (timestamp-less events count)", agg.stats["a"].events) + } +} + +func TestAggregatorExplicitWindow(t *testing.T) { + t.Parallel() + window := Window{Start: time.UnixMilli(2000).UTC(), End: time.UnixMilli(4000).UTC()} + agg := newAggregator(mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), &window) + for _, ms := range []int64{1000, 2000, 4000, 5000, 0} { + agg.add(netEvent(ClassNetworkActivity, "sb", false, "", "h", "", ms)) + } + if agg.outOfWindow != 3 || agg.attributed != 2 { + t.Fatalf("outOfWindow/attributed = %d/%d, want 3/2", agg.outOfWindow, agg.attributed) + } + if agg.stats["a"].total != 2 { + t.Fatalf("total = %d, want 2", agg.stats["a"].total) + } + if got := agg.resolveWindow(time.Now()); got != window { + t.Fatalf("resolveWindow() = %v, want %v", got, window) + } +} + +func TestAggregatorWindowFallsBackToNow(t *testing.T) { + t.Parallel() + agg := newAggregator(AgentMap{}, nil) + agg.add(Event{ClassUID: ClassBaseEvent}) + now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.FixedZone("x", 3600)) + got := agg.resolveWindow(now) + if !got.Start.Equal(now) || !got.End.Equal(now) || got.Start.Location() != time.UTC { + t.Fatalf("resolveWindow() = %v, want collapsed onto %v in UTC", got, now) + } +} + +func TestWindowContains(t *testing.T) { + t.Parallel() + w := Window{Start: time.UnixMilli(2000), End: time.UnixMilli(4000)} + for ms, want := range map[int64]bool{1999: false, 2000: true, 3000: true, 4000: true, 4001: false} { + if got := w.Contains(time.UnixMilli(ms)); got != want { + t.Errorf("Contains(%d) = %v, want %v", ms, got, want) + } + } +} diff --git a/internal/hydrator/doc.go b/internal/hydrator/doc.go new file mode 100644 index 0000000..b006ee7 --- /dev/null +++ b/internal/hydrator/doc.go @@ -0,0 +1,36 @@ +// Package hydrator turns OpenShell's OCSF audit export into behavioral +// observations for the ANS Trust Index. +// +// The input is OpenShell's OCSF JSON export in either of its two forms: a +// JSONL file with one OCSF v1.8.0 object per line, or supervisor stderr where +// each record appears as " OCSF-JSON {...}" among shorthand lines +// that are ignored. Every line is untrusted: lines are bounded by +// MaxLineBytes, unknown fields are ignored, missing fields are tolerated and a +// malformed line is counted and skipped, never fatal. +// +// Events are attributed to agents through their sandbox (container.uid, then +// container.name). Records that OpenShell downgraded to OCSF 1.1 or 1.3 have +// the container stripped and therefore cannot be attributed; they are counted +// as unmapped. +// +// Per agent and input window the package aggregates Network Activity (4001) +// and HTTP Activity (4002) events into two observations. An event counts as +// denied when any of action_id, disposition_id or status_id equals 2 (Denied, +// Blocked, Failure). A failed-but-allowed HTTP exchange therefore counts as +// denied, which errs toward over-reporting. +// +// The signal identifiers for these observations are not yet registered in the +// Trust Index. They are configurable through SignalConfig and default to +// "openshell.policyViolationRate" and "openshell.protocolAdherence"; a Trust +// Index deployment must register signals under those identifiers (or the +// configured ones) before it accepts the import. +// +// Batches are posted to the Trust Index import API +// (POST /v1/internal/observations/import) with the admin key presented as +// "Authorization: Bearer ". A batch may additionally be signed with the +// operator's ANS identity key through a provenance.BatchSigner: a compact +// ES256 JWS over the request body that the sink sends in the +// BatchSignatureHeader header. This signature format is a proposal pending +// Trust Index support; the Trust Index currently ignores the header, and +// pkg/provenance ships Verify so it can adopt the format. +package hydrator diff --git a/internal/hydrator/event.go b/internal/hydrator/event.go new file mode 100644 index 0000000..54c4b79 --- /dev/null +++ b/internal/hydrator/event.go @@ -0,0 +1,237 @@ +package hydrator + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "time" +) + +// OCSF class_uid values emitted by OpenShell. +const ( + ClassBaseEvent = 0 + ClassProcessActivity = 1007 + ClassDetectionFinding = 2004 + ClassNetworkActivity = 4001 + ClassHTTPActivity = 4002 + ClassSSHActivity = 4007 + ClassDeviceConfigChange = 5019 + ClassAppLifecycle = 6002 +) + +// deniedID is the shared OCSF enum value of action_id Denied, disposition_id +// Blocked and status_id Failure. +const deniedID = 2 + +// MaxLineBytes bounds one input line including its terminator. Longer lines +// are discarded and counted in ParseStats.Oversized. +const MaxLineBytes = 1 << 20 + +const readChunkBytes = 64 << 10 + +// ocsfJSONMarker separates the timestamp from the JSON payload on supervisor +// stderr; the payload is everything after it. +const ocsfJSONMarker = "OCSF-JSON " + +// Event is the subset of an OCSF record the hydrator reads. Unknown classes +// decode too, so they can be counted even when they carry nothing of interest. +type Event struct { + ClassUID int `json:"class_uid"` + ActivityID int `json:"activity_id"` + ActionID int `json:"action_id"` + DispositionID int `json:"disposition_id"` + StatusID int `json:"status_id"` + SeverityID int `json:"severity_id"` + StatusDetail string `json:"status_detail"` + Message string `json:"message"` + Time int64 `json:"time"` + Metadata Metadata `json:"metadata"` + Container *Container `json:"container"` + DstEndpoint *Endpoint `json:"dst_endpoint"` + Actor *Actor `json:"actor"` + FirewallRule *FirewallRule `json:"firewall_rule"` +} + +// Metadata is the OCSF metadata object. +type Metadata struct { + UID string `json:"uid"` + Version string `json:"version"` + Product Product `json:"product"` +} + +// Product identifies the emitting OpenShell component. +type Product struct { + Name string `json:"name"` + VendorName string `json:"vendor_name"` + Version string `json:"version"` +} + +// Container is the sandbox the event belongs to. OpenShell omits it for +// events without a sandbox association and strips it on schema downgrade. +type Container struct { + UID string `json:"uid"` + Name string `json:"name"` +} + +// Endpoint is an OCSF network endpoint. +type Endpoint struct { + Domain string `json:"domain"` + IP string `json:"ip"` + Port int `json:"port"` +} + +// Actor is the OCSF actor object. +type Actor struct { + Process *Process `json:"process"` +} + +// Process is the sandbox process that caused the event. +type Process struct { + Name string `json:"name"` + PID int `json:"pid"` +} + +// FirewallRule is the network policy rule that decided the event. +type FirewallRule struct { + Name string `json:"name"` + Type string `json:"type"` +} + +// Timestamp returns the event time (OCSF time is milliseconds since the Unix +// epoch) and whether the record carried one. +func (e Event) Timestamp() (time.Time, bool) { + if e.Time <= 0 { + return time.Time{}, false + } + return time.UnixMilli(e.Time).UTC(), true +} + +// Denied reports whether the event was denied, blocked or failed: any of +// action_id, disposition_id or status_id equals 2. +func (e Event) Denied() bool { + return e.ActionID == deniedID || e.DispositionID == deniedID || e.StatusID == deniedID +} + +// IsNetwork reports whether the event is Network Activity or HTTP Activity, +// the classes the observations are computed from. +func (e Event) IsNetwork() bool { + return e.ClassUID == ClassNetworkActivity || e.ClassUID == ClassHTTPActivity +} + +// Destination returns the destination domain, or the IP when no domain is +// recorded, or "" when the event has no destination. +func (e Event) Destination() string { + if e.DstEndpoint == nil { + return "" + } + if e.DstEndpoint.Domain != "" { + return e.DstEndpoint.Domain + } + return e.DstEndpoint.IP +} + +// ParseStats counts what a Reader saw. Lines equals Events plus Skipped; +// Oversized is the subset of Skipped that exceeded MaxLineBytes. +type ParseStats struct { + Lines int + Events int + Skipped int + Oversized int +} + +// Reader decodes OCSF events from a JSONL file or from supervisor stderr. +type Reader struct { + br *bufio.Reader + line []byte + stats ParseStats +} + +// NewReader returns a Reader over r. +func NewReader(r io.Reader) *Reader { + return &Reader{br: bufio.NewReaderSize(r, readChunkBytes)} +} + +// Read returns the next event. It returns io.EOF once the input is exhausted +// and any other error only when the underlying reader fails; lines that do +// not hold an event are skipped and counted. +func (r *Reader) Read() (Event, error) { + for { + line, oversized, err := r.readLine() + if err != nil { + return Event{}, err + } + r.stats.Lines++ + if oversized { + r.stats.Oversized++ + r.stats.Skipped++ + continue + } + ev, ok := decodeLine(line) + if !ok { + r.stats.Skipped++ + continue + } + r.stats.Events++ + return ev, nil + } +} + +// Stats returns the counters accumulated so far. +func (r *Reader) Stats() ParseStats { + return r.stats +} + +// readLine returns the next line and whether it exceeded MaxLineBytes, in +// which case the line content is discarded. +func (r *Reader) readLine() ([]byte, bool, error) { + r.line = r.line[:0] + oversized := false + for { + chunk, err := r.br.ReadSlice('\n') + if err != nil && !errors.Is(err, bufio.ErrBufferFull) && !errors.Is(err, io.EOF) { + return nil, false, fmt.Errorf("hydrator: read input: %w", err) + } + if !oversized { + if len(r.line)+len(chunk) > MaxLineBytes { + oversized = true + r.line = r.line[:0] + } else { + r.line = append(r.line, chunk...) + } + } + if errors.Is(err, bufio.ErrBufferFull) { + continue + } + if errors.Is(err, io.EOF) && len(chunk) == 0 && len(r.line) == 0 && !oversized { + return nil, false, io.EOF + } + return r.line, oversized, nil + } +} + +// decodeLine extracts the JSON payload from a JSONL or stderr line and +// decodes it. It reports false for blank, shorthand and malformed lines. +func decodeLine(line []byte) (Event, bool) { + payload := bytes.TrimSpace(line) + if len(payload) == 0 { + return Event{}, false + } + if payload[0] != '{' { + idx := bytes.Index(payload, []byte(ocsfJSONMarker)) + if idx < 0 { + return Event{}, false + } + payload = bytes.TrimSpace(payload[idx+len(ocsfJSONMarker):]) + if len(payload) == 0 || payload[0] != '{' { + return Event{}, false + } + } + var ev Event + if err := json.Unmarshal(payload, &ev); err != nil { + return Event{}, false + } + return ev, true +} diff --git a/internal/hydrator/event_test.go b/internal/hydrator/event_test.go new file mode 100644 index 0000000..00bd450 --- /dev/null +++ b/internal/hydrator/event_test.go @@ -0,0 +1,341 @@ +package hydrator + +import ( + "errors" + "io" + "strings" + "testing" + "testing/iotest" + "time" +) + +func TestReaderRead(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input func(t *testing.T) string + wantStats ParseStats + check func(t *testing.T, events []Event) + }{ + { + name: "jsonl allowed example", + input: func(*testing.T) string { return allowedExampleJSON + "\n" }, + wantStats: ParseStats{Lines: 1, Events: 1}, + check: checkAllowedExample, + }, + { + name: "jsonl denied example", + input: func(*testing.T) string { return deniedExampleJSON + "\n" }, + wantStats: ParseStats{Lines: 1, Events: 1}, + check: checkDeniedExample, + }, + { + name: "supervisor stderr with shorthand lines", + input: func(*testing.T) string { + return stderrTimestamp + " OCSF NET:OPEN [MED] DENIED github.com:443\n" + + stderrLine(deniedExampleJSON) + "\n" + + stderrTimestamp + " INFO supervisor ready\n" + }, + wantStats: ParseStats{Lines: 3, Events: 1, Skipped: 2}, + check: func(t *testing.T, events []Event) { + if events[0].Container == nil || events[0].Container.UID != deniedExampleUID { + t.Fatalf("event = %+v", events[0]) + } + }, + }, + { + name: "collector timestamp prefixed stderr", + input: func(*testing.T) string { + return "2026-10-07T23:21:19.123456789Z " + stderrLine(allowedExampleJSON) + "\n" + }, + wantStats: ParseStats{Lines: 1, Events: 1}, + check: func(t *testing.T, events []Event) { + if events[0].ClassUID != ClassNetworkActivity { + t.Fatalf("ClassUID = %d", events[0].ClassUID) + } + }, + }, + { + name: "blank and malformed lines are skipped", + input: func(*testing.T) string { + return "\n \nnot json\n{\"class_uid\":\n[1,2]\nnull\n\"str\"\n" + allowedExampleJSON + "\n" + }, + wantStats: ParseStats{Lines: 8, Events: 1, Skipped: 7}, + }, + { + name: "field type mismatch is skipped", + input: func(*testing.T) string { return `{"class_uid":"4001"}` + "\n" }, + wantStats: ParseStats{Lines: 1, Skipped: 1}, + }, + { + name: "unknown class and fields decode", + input: func(*testing.T) string { + return `{"class_uid":6002,"status_detail":"restart","unknown":{"deep":[1,2]}}` + "\n" + }, + wantStats: ParseStats{Lines: 1, Events: 1}, + check: func(t *testing.T, events []Event) { + if events[0].ClassUID != ClassAppLifecycle || events[0].StatusDetail != "restart" { + t.Fatalf("event = %+v", events[0]) + } + }, + }, + { + name: "downgraded record without container", + input: func(t *testing.T) string { + fields := exampleFields(t, deniedExampleJSON) + delete(fields, "container") + fields["unmapped"] = map[string]any{"downgraded_from": "1.8.0"} + return jsonLine(t, fields) + "\n" + }, + wantStats: ParseStats{Lines: 1, Events: 1}, + check: func(t *testing.T, events []Event) { + if events[0].Container != nil || !events[0].Denied() { + t.Fatalf("event = %+v", events[0]) + } + }, + }, + { + name: "marker without an object payload", + input: func(*testing.T) string { + return stderrLine("[1]") + "\n" + stderrLine("") + "\n" + stderrTimestamp + " OCSF-JSON\n" + }, + wantStats: ParseStats{Lines: 3, Skipped: 3}, + }, + { + name: "final line without newline", + input: func(*testing.T) string { return allowedExampleJSON }, + wantStats: ParseStats{Lines: 1, Events: 1}, + }, + { + name: "crlf line endings", + input: func(*testing.T) string { return allowedExampleJSON + "\r\n" + deniedExampleJSON + "\r\n" }, + wantStats: ParseStats{Lines: 2, Events: 2}, + }, + { + name: "oversized line is skipped and parsing continues", + input: func(*testing.T) string { + return strings.Repeat("x", MaxLineBytes) + "\n" + allowedExampleJSON + "\n" + }, + wantStats: ParseStats{Lines: 2, Events: 1, Skipped: 1, Oversized: 1}, + }, + { + name: "oversized json object is skipped", + input: func(*testing.T) string { + return `{"class_uid":4001,"message":"` + strings.Repeat("a", MaxLineBytes) + `"}` + "\n" + }, + wantStats: ParseStats{Lines: 1, Skipped: 1, Oversized: 1}, + }, + { + name: "oversized final line without newline", + input: func(*testing.T) string { return strings.Repeat("x", MaxLineBytes+5) }, + wantStats: ParseStats{Lines: 1, Skipped: 1, Oversized: 1}, + }, + { + name: "line exactly at the limit is read", + input: func(*testing.T) string { return strings.Repeat("x", MaxLineBytes-1) + "\n" }, + wantStats: ParseStats{Lines: 1, Skipped: 1}, + }, + { + name: "line longer than one read chunk but under the limit", + input: func(*testing.T) string { + return `{"class_uid":4001,"message":"` + strings.Repeat("a", 3*readChunkBytes) + `"}` + "\n" + deniedExampleJSON + "\n" + }, + wantStats: ParseStats{Lines: 2, Events: 2}, + check: func(t *testing.T, events []Event) { + if len(events[0].Message) != 3*readChunkBytes { + t.Fatalf("len(Message) = %d", len(events[0].Message)) + } + if events[1].Container == nil { + t.Fatal("second event lost after a long line") + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + events, stats := parseAll(t, tt.input(t)) + if stats != tt.wantStats { + t.Fatalf("Stats() = %+v, want %+v", stats, tt.wantStats) + } + if len(events) != tt.wantStats.Events { + t.Fatalf("len(events) = %d, want %d", len(events), tt.wantStats.Events) + } + if tt.check != nil { + tt.check(t, events) + } + }) + } +} + +func checkAllowedExample(t *testing.T, events []Event) { + t.Helper() + ev := events[0] + if ev.ClassUID != ClassNetworkActivity || ev.ActivityID != 1 || ev.ActionID != 1 || ev.DispositionID != 1 || ev.StatusID != 1 { + t.Fatalf("ids = %+v", ev) + } + if ev.Denied() { + t.Fatal("Denied() = true for the allowed example") + } + if ev.Container != nil { + t.Fatalf("Container = %+v, want nil (the example carries none)", ev.Container) + } + if got := ev.Destination(); got != "api.github.com" { + t.Fatalf("Destination() = %q", got) + } + ts, ok := ev.Timestamp() + if !ok || !ts.Equal(time.UnixMilli(allowedExampleTime)) || ts.Location() != time.UTC { + t.Fatalf("Timestamp() = %v, %v", ts, ok) + } + if ev.Metadata.Product.Version != "0.1.4" || ev.Metadata.Version != "1.8.0" || ev.Metadata.Product.Name != "OpenShell Sandbox Supervisor" { + t.Fatalf("Metadata = %+v", ev.Metadata) + } + if ev.FirewallRule == nil || ev.FirewallRule.Name != "github_api" || ev.FirewallRule.Type != "opa" { + t.Fatalf("FirewallRule = %+v", ev.FirewallRule) + } + if ev.Actor == nil || ev.Actor.Process == nil || ev.Actor.Process.Name != "/usr/bin/curl" || ev.Actor.Process.PID != 57 { + t.Fatalf("Actor = %+v", ev.Actor) + } + if ev.DstEndpoint.Port != 443 || ev.Message != "CONNECT allowed api.github.com:443" { + t.Fatalf("endpoint/message = %+v %q", ev.DstEndpoint, ev.Message) + } +} + +func checkDeniedExample(t *testing.T, events []Event) { + t.Helper() + ev := events[0] + if !ev.Denied() || ev.ActionID != 2 || ev.DispositionID != 2 || ev.StatusID != 2 || ev.SeverityID != 3 { + t.Fatalf("denied fields = %+v", ev) + } + if ev.StatusDetail != "transparent_tcp_policy_denied" { + t.Fatalf("StatusDetail = %q", ev.StatusDetail) + } + if ev.Container == nil || ev.Container.UID != deniedExampleUID || ev.Container.Name != deniedExampleName { + t.Fatalf("Container = %+v", ev.Container) + } + if got := ev.Destination(); got != "github.com" { + t.Fatalf("Destination() = %q", got) + } + if ev.DstEndpoint.IP != "198.18.0.2" || ev.DstEndpoint.Port != 443 { + t.Fatalf("DstEndpoint = %+v", ev.DstEndpoint) + } + if ev.Metadata.UID != "b885957d-124a-4af7-9eef-75cc410ff070" { + t.Fatalf("Metadata.UID = %q", ev.Metadata.UID) + } + ts, ok := ev.Timestamp() + if !ok || ts.UnixMilli() != deniedExampleTime { + t.Fatalf("Timestamp() = %v, %v", ts, ok) + } +} + +func TestReaderUnderlyingError(t *testing.T) { + t.Parallel() + boom := errors.New("boom") + + t.Run("immediately", func(t *testing.T) { + t.Parallel() + r := NewReader(iotest.ErrReader(boom)) + if _, err := r.Read(); !errors.Is(err, boom) { + t.Fatalf("Read() error = %v, want %v", err, boom) + } + }) + t.Run("after one line", func(t *testing.T) { + t.Parallel() + r := NewReader(io.MultiReader(strings.NewReader(allowedExampleJSON+"\n"), iotest.ErrReader(boom))) + if _, err := r.Read(); err != nil { + t.Fatalf("first Read() error = %v", err) + } + if _, err := r.Read(); !errors.Is(err, boom) { + t.Fatalf("second Read() error = %v, want %v", err, boom) + } + if stats := r.Stats(); stats != (ParseStats{Lines: 1, Events: 1}) { + t.Fatalf("Stats() = %+v", stats) + } + }) +} + +func TestEventTimestamp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + ms int64 + wantOK bool + }{ + {name: "missing", ms: 0}, + {name: "negative", ms: -5}, + {name: "present", ms: deniedExampleTime, wantOK: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ts, ok := Event{Time: tt.ms}.Timestamp() + if ok != tt.wantOK { + t.Fatalf("Timestamp() ok = %v, want %v", ok, tt.wantOK) + } + if ok && (ts.UnixMilli() != tt.ms || ts.Location() != time.UTC) { + t.Fatalf("Timestamp() = %v", ts) + } + if !ok && !ts.IsZero() { + t.Fatalf("Timestamp() = %v, want zero", ts) + } + }) + } +} + +func TestEventDenied(t *testing.T) { + t.Parallel() + tests := []struct { + name string + ev Event + want bool + }{ + {name: "all allowed", ev: Event{ActionID: 1, DispositionID: 1, StatusID: 1}}, + {name: "no ids", ev: Event{}}, + {name: "action denied", ev: Event{ActionID: 2, DispositionID: 1, StatusID: 1}, want: true}, + {name: "disposition blocked", ev: Event{ActionID: 1, DispositionID: 2, StatusID: 1}, want: true}, + {name: "status failure", ev: Event{ActionID: 1, DispositionID: 1, StatusID: 2}, want: true}, + {name: "other disposition values", ev: Event{ActionID: 1, DispositionID: 3, StatusID: 99}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.ev.Denied(); got != tt.want { + t.Fatalf("Denied() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEventDestination(t *testing.T) { + t.Parallel() + tests := []struct { + name string + ev Event + want string + }{ + {name: "no endpoint", ev: Event{}}, + {name: "empty endpoint", ev: Event{DstEndpoint: &Endpoint{Port: 443}}}, + {name: "domain preferred", ev: Event{DstEndpoint: &Endpoint{Domain: "github.com", IP: "198.18.0.2"}}, want: "github.com"}, + {name: "ip fallback", ev: Event{DstEndpoint: &Endpoint{IP: "198.18.0.2"}}, want: "198.18.0.2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.ev.Destination(); got != tt.want { + t.Fatalf("Destination() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestEventIsNetwork(t *testing.T) { + t.Parallel() + for class, want := range map[int]bool{ + ClassNetworkActivity: true, ClassHTTPActivity: true, + ClassSSHActivity: false, ClassDetectionFinding: false, ClassBaseEvent: false, + } { + if got := (Event{ClassUID: class}).IsNetwork(); got != want { + t.Errorf("IsNetwork() for class %d = %v, want %v", class, got, want) + } + } +} diff --git a/internal/hydrator/fixtures_test.go b/internal/hydrator/fixtures_test.go new file mode 100644 index 0000000..b584968 --- /dev/null +++ b/internal/hydrator/fixtures_test.go @@ -0,0 +1,139 @@ +package hydrator + +import ( + "encoding/json" + "errors" + "io" + "strings" + "testing" +) + +// The two record examples from OpenShell's OCSF JSON export documentation, +// compacted to one line each as they appear in a JSONL file. +const ( + allowedExampleJSON = `{"class_uid":4001,"class_name":"Network Activity","category_uid":4,"category_name":"Network Activity","activity_id":1,"activity_name":"Open","severity_id":1,"severity":"Informational","status_id":1,"status":"Success","time":1775014138811,"message":"CONNECT allowed api.github.com:443","metadata":{"product":{"name":"OpenShell Sandbox Supervisor","vendor_name":"OpenShell","version":"0.1.4"},"version":"1.8.0"},"action_id":1,"action":"Allowed","disposition_id":1,"disposition":"Allowed","dst_endpoint":{"domain":"api.github.com","port":443},"src_endpoint":{"ip":"192.0.2.31","port":37494},"actor":{"process":{"name":"/usr/bin/curl","pid":57}},"firewall_rule":{"name":"github_api","type":"opa"}}` + deniedExampleJSON = `{"class_uid":4001,"class_name":"Network Activity","category_uid":4,"category_name":"Network Activity","activity_id":1,"activity_name":"Open","type_uid":400101,"type_name":"Network Activity: Open","time":1791996125789,"severity_id":3,"severity":"Medium","status_id":2,"status":"Failure","message":"Transparent TCP denied before relay: network connections not allowed by policy","status_detail":"transparent_tcp_policy_denied","metadata":{"version":"1.8.0","product":{"name":"OpenShell Sandbox Supervisor","vendor_name":"OpenShell","version":"0.1.4"},"profiles":["security_control","network_proxy","container","host"],"uid":"b885957d-124a-4af7-9eef-75cc410ff070"},"device":{"hostname":"docker-desktop","type_id":99,"type":"Sandbox","os":{"name":"Linux"}},"container":{"name":"docs-deny","uid":"3949f6fc-2b84-45b2-a631-46debe41d84f"},"dst_endpoint":{"domain":"github.com","ip":"198.18.0.2","port":443},"proxy_endpoint":{"ip":"127.0.0.1","port":3128},"actor":{"process":{"name":"/usr/bin/curl","pid":2187,"cmd_line":"curl https://github.com"}},"action_id":2,"action":"Denied","disposition_id":2,"disposition":"Blocked","is_src_dst_assignment_known":true}` + + deniedExampleUID = "3949f6fc-2b84-45b2-a631-46debe41d84f" + deniedExampleName = "docs-deny" + deniedExampleTime = int64(1791996125789) + allowedExampleTime = int64(1775014138811) + + stderrTimestamp = "2026-10-07T23:21:19.000Z" +) + +// netSpec describes a synthetic Network or HTTP Activity record. +type netSpec struct { + class int + uid string + name string + denied bool + statusID int + detail string + domain string + ip string + timeMs int64 +} + +func netLine(t *testing.T, s netSpec) string { + t.Helper() + fields := map[string]any{"class_uid": s.class} + if s.timeMs != 0 { + fields["time"] = s.timeMs + } + if s.uid != "" || s.name != "" { + fields["container"] = map[string]any{"uid": s.uid, "name": s.name} + } + if s.domain != "" || s.ip != "" { + fields["dst_endpoint"] = map[string]any{"domain": s.domain, "ip": s.ip, "port": 443} + } + switch { + case s.denied: + fields["action_id"] = 2 + fields["disposition_id"] = 2 + fields["status_id"] = 2 + fields["status_detail"] = s.detail + case s.statusID != 0: + fields["action_id"] = 1 + fields["disposition_id"] = 1 + fields["status_id"] = s.statusID + default: + fields["action_id"] = 1 + fields["disposition_id"] = 1 + fields["status_id"] = 1 + } + return jsonLine(t, fields) +} + +func findingLine(t *testing.T, uid string, severity int, timeMs int64) string { + t.Helper() + return jsonLine(t, map[string]any{ + "class_uid": ClassDetectionFinding, + "severity_id": severity, + "time": timeMs, + "container": map[string]any{"uid": uid}, + }) +} + +func stderrLine(payload string) string { + return stderrTimestamp + " OCSF-JSON " + payload +} + +func jsonLine(t *testing.T, fields map[string]any) string { + t.Helper() + b, err := json.Marshal(fields) + if err != nil { + t.Fatalf("marshal fixture: %v", err) + } + return string(b) +} + +func exampleFields(t *testing.T, example string) map[string]any { + t.Helper() + var fields map[string]any + if err := json.Unmarshal([]byte(example), &fields); err != nil { + t.Fatalf("unmarshal example: %v", err) + } + return fields +} + +func parseAll(t *testing.T, input string) ([]Event, ParseStats) { + t.Helper() + r := NewReader(strings.NewReader(input)) + var events []Event + for { + ev, err := r.Read() + if errors.Is(err, io.EOF) { + return events, r.Stats() + } + if err != nil { + t.Fatalf("Read() error: %v", err) + } + events = append(events, ev) + } +} + +func mustAgentMap(t *testing.T, mappings ...AgentMapping) AgentMap { + t.Helper() + m, err := NewAgentMap(mappings) + if err != nil { + t.Fatalf("NewAgentMap() error: %v", err) + } + return m +} + +func netEvent(class int, sandboxUID string, denied bool, detail, domain, ip string, timeMs int64) Event { + ev := Event{ClassUID: class, Time: timeMs, StatusDetail: detail, ActionID: 1, DispositionID: 1, StatusID: 1} + if sandboxUID != "" { + ev.Container = &Container{UID: sandboxUID} + } + if domain != "" || ip != "" { + ev.DstEndpoint = &Endpoint{Domain: domain, IP: ip} + } + if denied { + ev.ActionID = deniedID + ev.DispositionID = deniedID + ev.StatusID = deniedID + } + return ev +} diff --git a/internal/hydrator/observation.go b/internal/hydrator/observation.go new file mode 100644 index 0000000..fa2b851 --- /dev/null +++ b/internal/hydrator/observation.go @@ -0,0 +1,185 @@ +package hydrator + +import ( + "encoding/json" + "fmt" + "strconv" + "time" +) + +// Default Trust Index signal identifiers. They are not yet registered in the +// Trust Index; see the package documentation. +const ( + DefaultPolicyViolationRateSignal = "openshell.policyViolationRate" + DefaultProtocolAdherenceSignal = "openshell.protocolAdherence" +) + +// SignalConfig names the signals observations are posted under and selects +// which to emit. Empty identifiers take the defaults. +type SignalConfig struct { + PolicyViolationRateID string + ProtocolAdherenceID string + EmitPolicyViolationRate bool + EmitProtocolAdherence bool +} + +// DefaultSignalConfig emits both signals under their default identifiers. +func DefaultSignalConfig() SignalConfig { + return SignalConfig{ + PolicyViolationRateID: DefaultPolicyViolationRateSignal, + ProtocolAdherenceID: DefaultProtocolAdherenceSignal, + EmitPolicyViolationRate: true, + EmitProtocolAdherence: true, + } +} + +func (c SignalConfig) withDefaults() SignalConfig { + if c.PolicyViolationRateID == "" { + c.PolicyViolationRateID = DefaultPolicyViolationRateSignal + } + if c.ProtocolAdherenceID == "" { + c.ProtocolAdherenceID = DefaultProtocolAdherenceSignal + } + return c +} + +// Observation is one row of the Trust Index import request. +type Observation struct { + AgentID string `json:"agentId"` + SignalID string `json:"signalId"` + ObservedAt string `json:"observedAt"` + Value json.RawMessage `json:"value"` + Provenance *Provenance `json:"provenance,omitempty"` +} + +// Provenance identifies the producer of an observation. +type Provenance struct { + AIMID string `json:"aimId,omitempty"` + EvidenceURL string `json:"evidenceUrl,omitempty"` +} + +type importRequest struct { + Observations []Observation `json:"observations"` +} + +// PolicyViolationRateValue is the value of the policy violation rate signal: +// the share of network and HTTP events in the window that were denied, with +// the deny reasons, per-class counts, distinct destinations and detection +// findings by severity that explain it. +type PolicyViolationRateValue struct { + WindowStart string `json:"windowStart"` + WindowEnd string `json:"windowEnd"` + Total int `json:"total"` + Denied int `json:"denied"` + Rate float64 `json:"rate"` + DenyReasons map[string]int `json:"denyReasons"` + Classes map[string]int `json:"classes"` + Destinations int `json:"destinations"` + Findings map[string]int `json:"findings"` +} + +// ProtocolAdherenceValue is the value of the protocol adherence signal: one +// minus the policy violation rate, with the sample size it rests on. +type ProtocolAdherenceValue struct { + WindowStart string `json:"windowStart"` + WindowEnd string `json:"windowEnd"` + Total int `json:"total"` + Adherence float64 `json:"adherence"` + SampleSize int `json:"sampleSize"` +} + +func newProvenance(aimID, evidenceURL string) *Provenance { + if aimID == "" && evidenceURL == "" { + return nil + } + return &Provenance{AIMID: aimID, EvidenceURL: evidenceURL} +} + +type observationBuilder struct { + signals SignalConfig + provenance *Provenance + window Window +} + +// build returns the agent's observations for the window. An agent with no +// network or HTTP activity yields none: a rate over zero events would read as +// perfect adherence. +func (b observationBuilder) build(st *agentStats) ([]Observation, error) { + if st.total == 0 { + return nil, nil + } + var out []Observation + if b.signals.EmitPolicyViolationRate { + obs, err := b.observation(st.mapping.AgentID, b.signals.PolicyViolationRateID, b.policyViolationRate(st)) + if err != nil { + return nil, err + } + out = append(out, obs) + } + if b.signals.EmitProtocolAdherence { + obs, err := b.observation(st.mapping.AgentID, b.signals.ProtocolAdherenceID, b.protocolAdherence(st)) + if err != nil { + return nil, err + } + out = append(out, obs) + } + return out, nil +} + +func (b observationBuilder) observation(agentID, signalID string, value any) (Observation, error) { + raw, err := json.Marshal(value) + if err != nil { + return Observation{}, fmt.Errorf("hydrator: encode %s for agent %s: %w", signalID, agentID, err) + } + return Observation{ + AgentID: agentID, + SignalID: signalID, + ObservedAt: formatTime(b.window.End), + Value: raw, + Provenance: b.provenance, + }, nil +} + +func (b observationBuilder) policyViolationRate(st *agentStats) PolicyViolationRateValue { + return PolicyViolationRateValue{ + WindowStart: formatTime(b.window.Start), + WindowEnd: formatTime(b.window.End), + Total: st.total, + Denied: st.denied, + Rate: st.rate(), + DenyReasons: cloneCounts(st.denyReasons), + Classes: stringKeys(st.classes), + Destinations: len(st.destinations), + Findings: stringKeys(st.findings), + } +} + +func (b observationBuilder) protocolAdherence(st *agentStats) ProtocolAdherenceValue { + return ProtocolAdherenceValue{ + WindowStart: formatTime(b.window.Start), + WindowEnd: formatTime(b.window.End), + Total: st.total, + Adherence: 1 - st.rate(), + SampleSize: st.total, + } +} + +func formatTime(t time.Time) string { + return t.UTC().Format(time.RFC3339) +} + +func cloneCounts(in map[string]int) map[string]int { + out := make(map[string]int, len(in)) + for k, v := range in { + out[k] = v + } + return out +} + +func stringKeys(in map[int]int) map[string]int { + out := make(map[string]int, len(in)) + for k, v := range in { + out[strconv.Itoa(k)] = v + } + return out +} diff --git a/internal/hydrator/observation_test.go b/internal/hydrator/observation_test.go new file mode 100644 index 0000000..67a132b --- /dev/null +++ b/internal/hydrator/observation_test.go @@ -0,0 +1,219 @@ +package hydrator + +import ( + "encoding/json" + "reflect" + "testing" + "time" +) + +func decodeJSON(t *testing.T, raw []byte) map[string]any { + t.Helper() + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatalf("unmarshal %s: %v", raw, err) + } + return m +} + +func TestObservationValuesGolden(t *testing.T) { + t.Parallel() + agg := newAggregator(mustAgentMap(t, AgentMapping{AgentID: "agent-a", SandboxUIDs: []string{"sb"}}), nil) + for _, ev := range []Event{ + netEvent(ClassNetworkActivity, "sb", false, "", "api.github.com", "", 1000), + netEvent(ClassNetworkActivity, "sb", true, "transparent_tcp_policy_denied", "github.com", "", 2000), + {ClassUID: ClassHTTPActivity, Container: &Container{UID: "sb"}, StatusID: deniedID, DstEndpoint: &Endpoint{Domain: "api.github.com"}, Time: 3000}, + netEvent(ClassHTTPActivity, "sb", false, "", "", "198.18.0.2", 4000), + {ClassUID: ClassDetectionFinding, Container: &Container{UID: "sb"}, SeverityID: 3}, + {ClassUID: ClassDetectionFinding, Container: &Container{UID: "sb"}, SeverityID: 5}, + } { + agg.add(ev) + } + window := agg.resolveWindow(time.Time{}) + provenance := newProvenance("ans://v1.0.0.ops.example.com", "https://evidence.example/run/1") + b := observationBuilder{signals: DefaultSignalConfig(), provenance: provenance, window: window} + + obs, err := b.build(agg.stats["agent-a"]) + if err != nil { + t.Fatalf("build() error = %v", err) + } + if len(obs) != 2 { + t.Fatalf("len(obs) = %d, want 2", len(obs)) + } + for i, want := range []string{DefaultPolicyViolationRateSignal, DefaultProtocolAdherenceSignal} { + if obs[i].SignalID != want || obs[i].AgentID != "agent-a" || obs[i].ObservedAt != "1970-01-01T00:00:04Z" { + t.Fatalf("obs[%d] = %+v", i, obs[i]) + } + if obs[i].Provenance != provenance { + t.Fatalf("obs[%d].Provenance = %+v", i, obs[i].Provenance) + } + } + wantRate := map[string]any{ + "windowStart": "1970-01-01T00:00:01Z", + "windowEnd": "1970-01-01T00:00:04Z", + "total": 4.0, + "denied": 2.0, + "rate": 0.5, + "denyReasons": map[string]any{"transparent_tcp_policy_denied": 1.0, "unspecified": 1.0}, + "classes": map[string]any{"4001": 2.0, "4002": 2.0}, + "destinations": 3.0, + "findings": map[string]any{"3": 1.0, "5": 1.0}, + } + if got := decodeJSON(t, obs[0].Value); !reflect.DeepEqual(got, wantRate) { + t.Fatalf("policyViolationRate value = %v, want %v", got, wantRate) + } + wantAdherence := map[string]any{ + "windowStart": "1970-01-01T00:00:01Z", + "windowEnd": "1970-01-01T00:00:04Z", + "total": 4.0, + "adherence": 0.5, + "sampleSize": 4.0, + } + if got := decodeJSON(t, obs[1].Value); !reflect.DeepEqual(got, wantAdherence) { + t.Fatalf("protocolAdherence value = %v, want %v", got, wantAdherence) + } +} + +func TestObservationValuesWithoutTraffic(t *testing.T) { + t.Parallel() + st := newAgentStats(AgentMapping{AgentID: "agent-quiet"}) + st.events = 1 + st.findings[3]++ + b := observationBuilder{signals: DefaultSignalConfig(), window: Window{Start: time.Unix(1, 0), End: time.Unix(1, 0)}} + obs, err := b.build(st) + if err != nil { + t.Fatalf("build() error = %v", err) + } + if len(obs) != 0 { + t.Fatalf("build() emitted %d observations for an agent with no network or HTTP activity, want none", len(obs)) + } +} + +func TestObservationBuilderSignalSelection(t *testing.T) { + t.Parallel() + tests := []struct { + name string + signals SignalConfig + wantIDs []string + }{ + {name: "none", signals: SignalConfig{}}, + {name: "rate only, custom id", signals: SignalConfig{EmitPolicyViolationRate: true, PolicyViolationRateID: "custom.rate"}, wantIDs: []string{"custom.rate"}}, + {name: "adherence only, default id", signals: SignalConfig{EmitProtocolAdherence: true}, wantIDs: []string{DefaultProtocolAdherenceSignal}}, + {name: "both", signals: DefaultSignalConfig(), wantIDs: []string{DefaultPolicyViolationRateSignal, DefaultProtocolAdherenceSignal}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + st := newAgentStats(AgentMapping{AgentID: "a"}) + st.add(netEvent(ClassNetworkActivity, "", false, "", "h", "", 0)) + b := observationBuilder{signals: tt.signals.withDefaults()} + obs, err := b.build(st) + if err != nil { + t.Fatalf("build() error = %v", err) + } + var ids []string + for _, o := range obs { + ids = append(ids, o.SignalID) + } + if !reflect.DeepEqual(ids, tt.wantIDs) { + t.Fatalf("signal ids = %v, want %v", ids, tt.wantIDs) + } + }) + } +} + +func TestSignalConfigDefaults(t *testing.T) { + t.Parallel() + def := DefaultSignalConfig() + if !def.EmitPolicyViolationRate || !def.EmitProtocolAdherence { + t.Fatalf("DefaultSignalConfig() = %+v, want both signals emitted", def) + } + if def.PolicyViolationRateID != "openshell.policyViolationRate" || def.ProtocolAdherenceID != "openshell.protocolAdherence" { + t.Fatalf("DefaultSignalConfig() ids = %+v", def) + } + custom := SignalConfig{PolicyViolationRateID: "x", ProtocolAdherenceID: "y"}.withDefaults() + if custom.PolicyViolationRateID != "x" || custom.ProtocolAdherenceID != "y" { + t.Fatalf("withDefaults() overwrote custom ids: %+v", custom) + } + filled := SignalConfig{}.withDefaults() + if filled.PolicyViolationRateID != def.PolicyViolationRateID || filled.ProtocolAdherenceID != def.ProtocolAdherenceID { + t.Fatalf("withDefaults() = %+v", filled) + } +} + +func TestNewProvenance(t *testing.T) { + t.Parallel() + if p := newProvenance("", ""); p != nil { + t.Fatalf("newProvenance(\"\", \"\") = %+v, want nil", p) + } + if p := newProvenance("did:web:ops.example.com", ""); p == nil || p.AIMID != "did:web:ops.example.com" || p.EvidenceURL != "" { + t.Fatalf("newProvenance(aim) = %+v", p) + } + if p := newProvenance("", "https://e"); p == nil || p.EvidenceURL != "https://e" { + t.Fatalf("newProvenance(evidence) = %+v", p) + } +} + +func TestBatchBodyWireShape(t *testing.T) { + t.Parallel() + st := newAgentStats(AgentMapping{AgentID: "agent-a"}) + st.add(netEvent(ClassNetworkActivity, "", false, "", "h", "", 1000)) + window := Window{Start: time.UnixMilli(1000), End: time.UnixMilli(1000)} + + t.Run("with provenance", func(t *testing.T) { + t.Parallel() + b := observationBuilder{signals: SignalConfig{EmitPolicyViolationRate: true}.withDefaults(), provenance: newProvenance("aim", "https://e"), window: window} + obs, err := b.build(st) + if err != nil { + t.Fatalf("build() error = %v", err) + } + batch, err := newBatch(obs, Options{}, time.Time{}) + if err != nil { + t.Fatalf("newBatch() error = %v", err) + } + body := decodeJSON(t, batch.Body) + if len(body) != 1 { + t.Fatalf("body keys = %v, want only observations", body) + } + rows, ok := body["observations"].([]any) + if !ok || len(rows) != 1 { + t.Fatalf("observations = %v", body["observations"]) + } + row, _ := rows[0].(map[string]any) + wantKeys := map[string]bool{"agentId": true, "signalId": true, "observedAt": true, "value": true, "provenance": true} + for k := range row { + if !wantKeys[k] { + t.Fatalf("unexpected row key %q in %v", k, row) + } + } + if len(row) != len(wantKeys) { + t.Fatalf("row = %v, want keys %v", row, wantKeys) + } + if row["agentId"] != "agent-a" || row["signalId"] != DefaultPolicyViolationRateSignal || row["observedAt"] != "1970-01-01T00:00:01Z" { + t.Fatalf("row = %v", row) + } + if !reflect.DeepEqual(row["provenance"], map[string]any{"aimId": "aim", "evidenceUrl": "https://e"}) { + t.Fatalf("provenance = %v", row["provenance"]) + } + if batch.Signature != "" || len(batch.Observations) != 1 { + t.Fatalf("batch = %+v", batch) + } + }) + t.Run("without provenance", func(t *testing.T) { + t.Parallel() + b := observationBuilder{signals: DefaultSignalConfig(), window: window} + obs, err := b.build(st) + if err != nil { + t.Fatalf("build() error = %v", err) + } + batch, err := newBatch(obs, Options{}, time.Time{}) + if err != nil { + t.Fatalf("newBatch() error = %v", err) + } + rows, _ := decodeJSON(t, batch.Body)["observations"].([]any) + row, _ := rows[0].(map[string]any) + if _, present := row["provenance"]; present { + t.Fatalf("provenance present in %v", row) + } + }) +} diff --git a/internal/hydrator/run.go b/internal/hydrator/run.go new file mode 100644 index 0000000..9e5f739 --- /dev/null +++ b/internal/hydrator/run.go @@ -0,0 +1,186 @@ +package hydrator + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "maps" + "slices" + "time" + + "github.com/agentnameservice/ans-openshell/pkg/provenance" +) + +// Options configures Run. +type Options struct { + // Agents attributes sandboxes to agents. + Agents AgentMap + // Signals selects the signals to emit and names them. + Signals SignalConfig + // AIMID identifies the operator in provenance.aimId and in the batch + // signature, for example an ans:// name or a did:web. + AIMID string + // EvidenceURL is recorded in provenance.evidenceUrl when set. + EvidenceURL string + // Window overrides the observation period. When set, events outside it + // and events without a timestamp are excluded; when nil the window is the + // span of event times in the input. + Window *Window + // Signer signs the batch; nil posts it unsigned. + Signer provenance.BatchSigner + // Clock supplies the signing time, and the window when no event carries a + // timestamp; nil uses time.Now. + Clock func() time.Time +} + +// AgentSummary reports one agent's share of the input. +type AgentSummary struct { + AgentID string + AnsName string + Events int + Denied int + Observations int +} + +// Report describes one Run. +type Report struct { + Parse ParseStats + // Attributed counts events that resolved to a configured agent and fell + // inside the window, whatever their class. + Attributed int + // Unmapped counts events that resolved to no agent; NoContainer is the + // subset that carried no container at all, as downgraded records do. + Unmapped int + NoContainer int + // OutOfWindow counts events excluded by an explicit Options.Window. + OutOfWindow int + Window Window + Agents []AgentSummary + Observations int + Posted bool + Signed bool +} + +// Run reads every event from in, aggregates them per agent and posts one +// batch to sink. Without observations nothing is posted. The Report is +// populated as far as the run got, also when the error is non-nil. +func Run(ctx context.Context, in io.Reader, sink Sink, opts Options, logger *slog.Logger) (Report, error) { + if in == nil || sink == nil { + return Report{}, errors.New("hydrator: input and sink are required") + } + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + clock := opts.Clock + if clock == nil { + clock = time.Now + } + reader := NewReader(in) + agg := newAggregator(opts.Agents, opts.Window) + err := consume(ctx, reader, agg) + report := Report{ + Parse: reader.Stats(), + Attributed: agg.attributed, + Unmapped: agg.unmapped, + NoContainer: agg.noContainer, + OutOfWindow: agg.outOfWindow, + Window: agg.resolveWindow(clock()), + } + if err != nil { + return report, err + } + logger.DebugContext(ctx, "input parsed", + "lines", report.Parse.Lines, "events", report.Parse.Events, + "skipped", report.Parse.Skipped, "oversized", report.Parse.Oversized) + if report.Unmapped > 0 { + logger.WarnContext(ctx, "events without an agent mapping were skipped", + "unmapped", report.Unmapped, "no_container", report.NoContainer) + } + if report.Parse.Events > 0 && report.OutOfWindow == report.Parse.Events { + logger.WarnContext(ctx, "every event fell outside the window", + "events", report.Parse.Events, "window_start", report.Window.Start, "window_end", report.Window.End) + } + observations, err := buildObservations(agg, report.Window, opts, &report) + if err != nil { + return report, err + } + if len(observations) == 0 { + logger.InfoContext(ctx, "no observations to post", "events", report.Parse.Events) + return report, nil + } + batch, err := newBatch(observations, opts, clock()) + if err != nil { + return report, err + } + if err := sink.Post(ctx, batch); err != nil { + return report, err + } + report.Posted = true + report.Signed = batch.Signature != "" + logger.InfoContext(ctx, "observations posted", + "observations", len(observations), "agents", len(report.Agents), "signed", report.Signed) + return report, nil +} + +func consume(ctx context.Context, reader *Reader, agg *aggregator) error { + for { + if err := ctx.Err(); err != nil { + return fmt.Errorf("hydrator: %w", err) + } + ev, err := reader.Read() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return err + } + agg.add(ev) + } +} + +func buildObservations(agg *aggregator, window Window, opts Options, report *Report) ([]Observation, error) { + builder := observationBuilder{ + signals: opts.Signals.withDefaults(), + provenance: newProvenance(opts.AIMID, opts.EvidenceURL), + window: window, + } + ids := slices.Sorted(maps.Keys(agg.stats)) + observations := make([]Observation, 0, len(ids)) + report.Agents = make([]AgentSummary, 0, len(ids)) + for _, id := range ids { + st := agg.stats[id] + obs, err := builder.build(st) + if err != nil { + return nil, err + } + observations = append(observations, obs...) + report.Agents = append(report.Agents, AgentSummary{ + AgentID: id, + AnsName: st.mapping.AnsName, + Events: st.events, + Denied: st.denied, + Observations: len(obs), + }) + } + report.Observations = len(observations) + return observations, nil +} + +func newBatch(observations []Observation, opts Options, now time.Time) (Batch, error) { + body, err := json.Marshal(importRequest{Observations: observations}) + if err != nil { + return Batch{}, fmt.Errorf("hydrator: encode batch: %w", err) + } + batch := Batch{Observations: observations, Body: body} + if opts.Signer != nil { + signature, err := opts.Signer.Sign(body, now, len(observations), opts.AIMID) + if err != nil { + return Batch{}, err + } + batch.Signature = signature + } + return batch, nil +} diff --git a/internal/hydrator/run_test.go b/internal/hydrator/run_test.go new file mode 100644 index 0000000..313af44 --- /dev/null +++ b/internal/hydrator/run_test.go @@ -0,0 +1,398 @@ +package hydrator + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "log/slog" + "reflect" + "strings" + "testing" + "testing/iotest" + "time" + + "github.com/agentnameservice/ans-openshell/internal/testfixtures" + "github.com/agentnameservice/ans-openshell/pkg/provenance" +) + +type fakeSink struct { + batches []Batch + err error +} + +func (f *fakeSink) Post(_ context.Context, batch Batch) error { + if f.err != nil { + return f.err + } + f.batches = append(f.batches, batch) + return nil +} + +const ( + sandboxAUID = "3949f6fc-2b84-45b2-a631-46debe41d84f" + sandboxBName = "payments-sandbox" + sampleBaseMs = int64(1_791_996_120_000) + sampleAIMID = "ans://v1.0.0.ops.example.com" + sampleEvidURL = "https://evidence.example/exports/2026-10-07" +) + +func fixedClock() time.Time { + return time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) +} + +func sampleAgents(t *testing.T) AgentMap { + t.Helper() + return mustAgentMap(t, + AgentMapping{AgentID: "agent-a", AnsName: "ans://v1.0.0.a.example.com", SandboxUIDs: []string{sandboxAUID}}, + AgentMapping{AgentID: "agent-b", AnsName: "ans://v1.0.0.b.example.com", SandboxNames: []string{sandboxBName}}, + ) +} + +// sampleInput mixes supervisor stderr (agent-a) with plain JSONL (agent-b), +// an unmapped sandbox, a downgraded record, a blank line and garbage. +func sampleInput(t *testing.T) string { + t.Helper() + lines := []string{ + stderrTimestamp + " OCSF NET:OPEN [MED] DENIED github.com:443", + stderrLine(netLine(t, netSpec{class: ClassNetworkActivity, uid: sandboxAUID, name: "docs-deny", denied: true, detail: "transparent_tcp_policy_denied", domain: "github.com", timeMs: sampleBaseMs + 1000})), + stderrLine(netLine(t, netSpec{class: ClassNetworkActivity, uid: sandboxAUID, name: "docs-deny", domain: "api.github.com", timeMs: sampleBaseMs + 2000})), + netLine(t, netSpec{class: ClassHTTPActivity, name: sandboxBName, domain: "api.github.com", timeMs: sampleBaseMs + 3000}), + netLine(t, netSpec{class: ClassHTTPActivity, name: sandboxBName, statusID: 2, domain: "api.github.com", timeMs: sampleBaseMs + 4000}), + netLine(t, netSpec{class: ClassNetworkActivity, uid: "unknown-uid", name: "unknown", domain: "example.com", timeMs: sampleBaseMs + 5000}), + netLine(t, netSpec{class: ClassNetworkActivity, domain: "example.com", timeMs: sampleBaseMs + 6000}), + "", + "garbage", + findingLine(t, sandboxAUID, 3, sampleBaseMs+7000), + } + return strings.Join(lines, "\n") + "\n" +} + +func decodeBatch(t *testing.T, body []byte) []map[string]any { + t.Helper() + var req struct { + Observations []map[string]any `json:"observations"` + } + if err := json.Unmarshal(body, &req); err != nil { + t.Fatalf("body is not JSON: %v\n%s", err, body) + } + return req.Observations +} + +func TestRunEndToEnd(t *testing.T) { + t.Parallel() + sink := &fakeSink{} + opts := Options{ + Agents: sampleAgents(t), + Signals: DefaultSignalConfig(), + AIMID: sampleAIMID, + EvidenceURL: sampleEvidURL, + Clock: fixedClock, + } + var logs bytes.Buffer + logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) + + report, err := Run(t.Context(), strings.NewReader(sampleInput(t)), sink, opts, logger) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + wantWindow := Window{Start: time.UnixMilli(sampleBaseMs + 1000).UTC(), End: time.UnixMilli(sampleBaseMs + 7000).UTC()} + want := Report{ + Parse: ParseStats{Lines: 10, Events: 7, Skipped: 3}, + Attributed: 5, + Unmapped: 2, + NoContainer: 1, + Window: wantWindow, + Observations: 4, + Posted: true, + Agents: []AgentSummary{ + {AgentID: "agent-a", AnsName: "ans://v1.0.0.a.example.com", Events: 3, Denied: 1, Observations: 2}, + {AgentID: "agent-b", AnsName: "ans://v1.0.0.b.example.com", Events: 2, Denied: 1, Observations: 2}, + }, + } + if !reflect.DeepEqual(report, want) { + t.Fatalf("Report = %+v\nwant %+v", report, want) + } + if len(sink.batches) != 1 { + t.Fatalf("sink received %d batches, want 1", len(sink.batches)) + } + batch := sink.batches[0] + if batch.Signature != "" || len(batch.Observations) != 4 { + t.Fatalf("batch = %+v", batch) + } + rows := decodeBatch(t, batch.Body) + if len(rows) != 4 { + t.Fatalf("rows = %d, want 4", len(rows)) + } + wantObservedAt := wantWindow.End.Format(time.RFC3339) + wantRows := []struct{ agent, signal string }{ + {"agent-a", DefaultPolicyViolationRateSignal}, {"agent-a", DefaultProtocolAdherenceSignal}, + {"agent-b", DefaultPolicyViolationRateSignal}, {"agent-b", DefaultProtocolAdherenceSignal}, + } + for i, row := range rows { + if row["agentId"] != wantRows[i].agent || row["signalId"] != wantRows[i].signal || row["observedAt"] != wantObservedAt { + t.Fatalf("rows[%d] = %v, want %+v at %s", i, row, wantRows[i], wantObservedAt) + } + if !reflect.DeepEqual(row["provenance"], map[string]any{"aimId": sampleAIMID, "evidenceUrl": sampleEvidURL}) { + t.Fatalf("rows[%d].provenance = %v", i, row["provenance"]) + } + } + valueA, _ := rows[0]["value"].(map[string]any) + if valueA["total"] != 2.0 || valueA["denied"] != 1.0 || valueA["rate"] != 0.5 || valueA["destinations"] != 2.0 { + t.Fatalf("agent-a value = %v", valueA) + } + if !reflect.DeepEqual(valueA["findings"], map[string]any{"3": 1.0}) || !reflect.DeepEqual(valueA["denyReasons"], map[string]any{"transparent_tcp_policy_denied": 1.0}) { + t.Fatalf("agent-a value = %v", valueA) + } + valueB, _ := rows[2]["value"].(map[string]any) + if !reflect.DeepEqual(valueB["classes"], map[string]any{"4001": 0.0, "4002": 2.0}) || !reflect.DeepEqual(valueB["denyReasons"], map[string]any{"unspecified": 1.0}) { + t.Fatalf("agent-b value = %v", valueB) + } + for _, want := range []string{"input parsed", "events without an agent mapping were skipped", "unmapped=2", "no_container=1", "observations posted", "observations=4"} { + if !strings.Contains(logs.String(), want) { + t.Errorf("logs missing %q:\n%s", want, logs.String()) + } + } +} + +func TestRunSignsBatch(t *testing.T) { + t.Parallel() + id := testfixtures.MustNewIdentity("agent-signing", "ans://v1.0.0.platform.example.com") + signer, err := provenance.NewPEMSigner(id.KeyPEM, id.CertPEM) + if err != nil { + t.Fatalf("NewPEMSigner() error = %v", err) + } + sink := &fakeSink{} + opts := Options{Agents: sampleAgents(t), Signals: DefaultSignalConfig(), AIMID: sampleAIMID, Signer: signer, Clock: fixedClock} + + report, err := Run(t.Context(), strings.NewReader(sampleInput(t)), sink, opts, nil) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if !report.Signed || !report.Posted { + t.Fatalf("Report = %+v, want signed and posted", report) + } + batch := sink.batches[0] + claims, cert, err := provenance.Verify(batch.Signature, batch.Body) + if err != nil { + t.Fatalf("Verify() error = %v", err) + } + if claims.Count != 4 || claims.AIMID != sampleAIMID || claims.IssuedAt != fixedClock().Unix() { + t.Fatalf("claims = %+v", claims) + } + if !cert.Equal(signer.Certificate()) { + t.Fatal("signature carries a different certificate") + } + if _, _, err := provenance.Verify(batch.Signature, append(batch.Body, ' ')); !errors.Is(err, provenance.ErrBadSignature) { + t.Fatalf("Verify() over a modified body = %v, want ErrBadSignature", err) + } +} + +func TestRunWithoutObservations(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + agents AgentMap + signals SignalConfig + wantReport Report + }{ + { + name: "empty input", + agents: AgentMap{}, + signals: DefaultSignalConfig(), + wantReport: Report{Window: Window{Start: fixedClock(), End: fixedClock()}, Agents: []AgentSummary{}}, + }, + { + name: "only unmapped events", + input: allowedExampleJSON + "\n" + deniedExampleJSON + "\n", + agents: AgentMap{}, + signals: DefaultSignalConfig(), + wantReport: Report{ + Parse: ParseStats{Lines: 2, Events: 2}, + Unmapped: 2, + NoContainer: 1, + Window: Window{Start: time.UnixMilli(allowedExampleTime).UTC(), End: time.UnixMilli(deniedExampleTime).UTC()}, + Agents: []AgentSummary{}, + }, + }, + { + name: "mapped process event under the default signals", + input: `{"class_uid":1007,"container":{"uid":"sb"},"time":1000}` + "\n", + agents: mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), + signals: DefaultSignalConfig(), + wantReport: Report{ + Parse: ParseStats{Lines: 1, Events: 1}, + Attributed: 1, + Window: Window{Start: time.UnixMilli(1000).UTC(), End: time.UnixMilli(1000).UTC()}, + Agents: []AgentSummary{{AgentID: "a", Events: 1}}, + }, + }, + { + name: "no signals selected", + input: `{"class_uid":4001,"container":{"uid":"sb"}}` + "\n", + agents: mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), + signals: SignalConfig{}, + wantReport: Report{ + Parse: ParseStats{Lines: 1, Events: 1}, + Attributed: 1, + Window: Window{Start: fixedClock(), End: fixedClock()}, + Agents: []AgentSummary{{AgentID: "a", Events: 1}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sink := &fakeSink{} + report, err := Run(t.Context(), strings.NewReader(tt.input), sink, Options{Agents: tt.agents, Signals: tt.signals, Clock: fixedClock}, nil) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if !reflect.DeepEqual(report, tt.wantReport) { + t.Fatalf("Report = %+v\nwant %+v", report, tt.wantReport) + } + if len(sink.batches) != 0 { + t.Fatalf("sink received %d batches, want none", len(sink.batches)) + } + }) + } +} + +func TestRunMappedAgentWithoutObservationsStillSummarized(t *testing.T) { + t.Parallel() + sink := &fakeSink{} + opts := Options{Agents: mustAgentMap(t, AgentMapping{AgentID: "a", SandboxUIDs: []string{"sb"}}), Clock: fixedClock} + report, err := Run(t.Context(), strings.NewReader(`{"class_uid":4001,"container":{"uid":"sb"},"time":1000}`+"\n"), sink, opts, nil) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if report.Observations != 0 || report.Posted || report.Attributed != 1 || len(report.Agents) != 1 || report.Agents[0].Events != 1 || report.Agents[0].Observations != 0 { + t.Fatalf("Report = %+v", report) + } +} + +func TestRunExplicitWindow(t *testing.T) { + t.Parallel() + window := Window{Start: time.UnixMilli(sampleBaseMs + 2000).UTC(), End: time.UnixMilli(sampleBaseMs + 4000).UTC()} + sink := &fakeSink{} + opts := Options{Agents: sampleAgents(t), Signals: DefaultSignalConfig(), Window: &window, Clock: fixedClock} + + report, err := Run(t.Context(), strings.NewReader(sampleInput(t)), sink, opts, nil) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if report.OutOfWindow != 4 || report.Unmapped != 0 || report.Attributed != 3 || report.Window != window || report.Observations != 4 { + t.Fatalf("Report = %+v", report) + } + if report.Agents[0].Events != 1 || report.Agents[1].Events != 2 { + t.Fatalf("Agents = %+v", report.Agents) + } + rows := decodeBatch(t, sink.batches[0].Body) + if rows[0]["observedAt"] != window.End.Format(time.RFC3339) { + t.Fatalf("observedAt = %v, want window end", rows[0]["observedAt"]) + } + value, _ := rows[0]["value"].(map[string]any) + if value["windowStart"] != window.Start.Format(time.RFC3339) || value["windowEnd"] != window.End.Format(time.RFC3339) { + t.Fatalf("value window = %v/%v", value["windowStart"], value["windowEnd"]) + } +} + +func TestRunSignalSubset(t *testing.T) { + t.Parallel() + sink := &fakeSink{} + opts := Options{ + Agents: sampleAgents(t), + Signals: SignalConfig{EmitProtocolAdherence: true, ProtocolAdherenceID: "custom.adherence"}, + Clock: fixedClock, + } + report, err := Run(t.Context(), strings.NewReader(sampleInput(t)), sink, opts, nil) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if report.Observations != 2 { + t.Fatalf("Observations = %d, want 2", report.Observations) + } + for _, row := range decodeBatch(t, sink.batches[0].Body) { + if row["signalId"] != "custom.adherence" { + t.Fatalf("row = %v", row) + } + if _, present := row["provenance"]; present { + t.Fatalf("provenance present without aim id or evidence: %v", row) + } + } +} + +func TestRunFailures(t *testing.T) { + t.Parallel() + boom := errors.New("boom") + cancelled, cancel := context.WithCancel(t.Context()) + cancel() + tests := []struct { + name string + ctx context.Context + in io.Reader + sink Sink + wantErr error + wantMsg string + check func(t *testing.T, report Report) + wantPost bool + }{ + { + name: "sink error", ctx: t.Context(), in: strings.NewReader(sampleInput(t)), sink: &fakeSink{err: boom}, wantErr: boom, + check: func(t *testing.T, report Report) { + if report.Posted || report.Observations != 4 { + t.Fatalf("Report = %+v", report) + } + }, + }, + { + name: "reader error", ctx: t.Context(), in: io.MultiReader(strings.NewReader(sampleInput(t)), iotest.ErrReader(boom)), sink: &fakeSink{}, wantErr: boom, + check: func(t *testing.T, report Report) { + if report.Parse.Events != 7 || report.Posted || report.Observations != 0 { + t.Fatalf("Report = %+v", report) + } + }, + }, + {name: "context cancelled", ctx: cancelled, in: strings.NewReader(sampleInput(t)), sink: &fakeSink{}, wantErr: context.Canceled}, + {name: "nil input", ctx: t.Context(), sink: &fakeSink{}, wantMsg: "input and sink are required"}, + {name: "nil sink", ctx: t.Context(), in: strings.NewReader(""), wantMsg: "input and sink are required"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + report, err := Run(tt.ctx, tt.in, tt.sink, Options{Agents: sampleAgents(t), Signals: DefaultSignalConfig(), Clock: fixedClock}, nil) + if tt.wantErr != nil && !errors.Is(err, tt.wantErr) { + t.Fatalf("Run() error = %v, want %v", err, tt.wantErr) + } + if tt.wantMsg != "" && (err == nil || !strings.Contains(err.Error(), tt.wantMsg)) { + t.Fatalf("Run() error = %v, want containing %q", err, tt.wantMsg) + } + if fs, ok := tt.sink.(*fakeSink); ok && len(fs.batches) != 0 { + t.Fatalf("sink received %d batches, want none", len(fs.batches)) + } + if tt.check != nil { + tt.check(t, report) + } + }) + } +} + +func TestRunWarnsWhenEveryEventIsOutsideTheWindow(t *testing.T) { + t.Parallel() + window := Window{Start: time.UnixMilli(sampleBaseMs + 60_000).UTC(), End: time.UnixMilli(sampleBaseMs + 120_000).UTC()} + var logs bytes.Buffer + sink := &fakeSink{} + opts := Options{Agents: sampleAgents(t), Signals: DefaultSignalConfig(), Window: &window, Clock: fixedClock} + report, err := Run(t.Context(), strings.NewReader(sampleInput(t)), sink, opts, slog.New(slog.NewTextHandler(&logs, nil))) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if report.OutOfWindow != report.Parse.Events || report.Attributed != 0 || len(sink.batches) != 0 { + t.Fatalf("Report = %+v, batches = %d", report, len(sink.batches)) + } + if !strings.Contains(logs.String(), "every event fell outside the window") { + t.Fatalf("missing out-of-window warning:\n%s", logs.String()) + } +} diff --git a/internal/hydrator/sink.go b/internal/hydrator/sink.go new file mode 100644 index 0000000..4f55e87 --- /dev/null +++ b/internal/hydrator/sink.go @@ -0,0 +1,174 @@ +package hydrator + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +// ImportPath is the Trust Index observation import endpoint. +const ImportPath = "/v1/internal/observations/import" + +// BatchSignatureHeader carries the compact JWS over the request body. HTTP +// header names are case-insensitive; this is the canonical form net/http +// emits for X-ANS-Batch-Signature. +const BatchSignatureHeader = "X-Ans-Batch-Signature" + +// DefaultHTTPTimeout bounds one import request when no client is supplied. +const DefaultHTTPTimeout = 10 * time.Second + +const ( + maxResponseBytes = 64 << 10 + maxErrorBodyBytes = 256 +) + +// Batch is one import request: the observations, the exact body bytes that +// are posted and the signature over them when the batch is signed. +type Batch struct { + Observations []Observation + Body []byte + Signature string +} + +// Sink delivers a batch. +type Sink interface { + Post(ctx context.Context, batch Batch) error +} + +// ImportError is a non-2xx answer from the import API. Title, Detail and Code +// come from the RFC 7807 problem body when there is one; otherwise Title is +// the status text and Detail a bounded, quoted excerpt of the body. +type ImportError struct { + Status int + Title string + Detail string + Code string +} + +func newImportError(status int, body []byte) *ImportError { + ie := &ImportError{Status: status} + var problem struct { + Title string `json:"title"` + Detail string `json:"detail"` + Code string `json:"code"` + } + if err := json.Unmarshal(body, &problem); err == nil && (problem.Title != "" || problem.Detail != "" || problem.Code != "") { + ie.Title, ie.Detail, ie.Code = problem.Title, problem.Detail, problem.Code + return ie + } + ie.Title = http.StatusText(status) + if excerpt := bytes.TrimSpace(body); len(excerpt) > 0 { + ie.Detail = strconv.Quote(string(excerpt[:min(len(excerpt), maxErrorBodyBytes)])) + } + return ie +} + +// Error implements error. +func (e *ImportError) Error() string { + var b strings.Builder + b.WriteString("hydrator: import returned HTTP ") + b.WriteString(strconv.Itoa(e.Status)) + if e.Title != "" { + b.WriteString(" " + e.Title) + } + if e.Code != "" { + b.WriteString(" (" + e.Code + ")") + } + if e.Detail != "" { + b.WriteString(": " + e.Detail) + } + return b.String() +} + +// HTTPSink posts batches to the Trust Index import API. +type HTTPSink struct { + endpoint string + adminKey string + client *http.Client + // UserAgent identifies the producer to the Trust Index when set, for + // example "ans-ocsf-hydrator/0.1.0". + UserAgent string +} + +// NewHTTPSink targets the Trust Index at baseURL. adminKey is sent as +// "Authorization: Bearer " when non-empty, which is how the import +// API's admin middleware expects it; leave it empty for a deployment whose +// import routes run unauthenticated. A nil client uses one with +// DefaultHTTPTimeout. +func NewHTTPSink(baseURL, adminKey string, client *http.Client) (*HTTPSink, error) { + u, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("hydrator: base url: %w", err) + } + if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return nil, fmt.Errorf("hydrator: base url %q must be http(s) with a host", baseURL) + } + if client == nil { + client = &http.Client{Timeout: DefaultHTTPTimeout} + } + return &HTTPSink{endpoint: u.JoinPath(ImportPath).String(), adminKey: adminKey, client: client}, nil +} + +// Post sends the batch and returns an *ImportError for a non-2xx answer. +func (s *HTTPSink) Post(ctx context.Context, batch Batch) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, bytes.NewReader(batch.Body)) + if err != nil { + return fmt.Errorf("hydrator: build import request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if s.UserAgent != "" { + req.Header.Set("User-Agent", s.UserAgent) + } + if s.adminKey != "" { + req.Header.Set("Authorization", "Bearer "+s.adminKey) + } + if batch.Signature != "" { + req.Header.Set(BatchSignatureHeader, batch.Signature) + } + resp, err := s.client.Do(req) //nolint:gosec // G704: the endpoint is operator configuration validated in NewHTTPSink, never request input + if err != nil { + return fmt.Errorf("hydrator: post import: %w", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes)) + if err != nil { + return fmt.Errorf("hydrator: read import response: %w", err) + } + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + return nil + } + return newImportError(resp.StatusCode, body) +} + +// WriterSink writes the request body to w for dry runs: the body on one line, +// followed by the compact JWS on its own line when the batch is signed. +type WriterSink struct { + w io.Writer +} + +// NewWriterSink returns a WriterSink over w. +func NewWriterSink(w io.Writer) *WriterSink { + return &WriterSink{w: w} +} + +// Post writes the batch. +func (s *WriterSink) Post(_ context.Context, batch Batch) error { + var buf bytes.Buffer + buf.Write(batch.Body) + buf.WriteByte('\n') + if batch.Signature != "" { + buf.WriteString(batch.Signature) + buf.WriteByte('\n') + } + if _, err := s.w.Write(buf.Bytes()); err != nil { + return fmt.Errorf("hydrator: write batch: %w", err) + } + return nil +} diff --git a/internal/hydrator/sink_test.go b/internal/hydrator/sink_test.go new file mode 100644 index 0000000..1c7d583 --- /dev/null +++ b/internal/hydrator/sink_test.go @@ -0,0 +1,292 @@ +package hydrator + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type capturedRequest struct { + method string + path string + header http.Header + body []byte +} + +func captureServer(t *testing.T, status int, response string) (*httptest.Server, <-chan capturedRequest) { + t.Helper() + captured := make(chan capturedRequest, 1) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + captured <- capturedRequest{method: r.Method, path: r.URL.Path, header: r.Header.Clone(), body: body} + if strings.HasPrefix(response, "{") { + w.Header().Set("Content-Type", "application/problem+json") + } + w.WriteHeader(status) + _, _ = io.WriteString(w, response) + })) + t.Cleanup(srv.Close) + return srv, captured +} + +func TestNewHTTPSink(t *testing.T) { + t.Parallel() + tests := []struct { + name string + baseURL string + wantEndpoint string + wantErr bool + }{ + {name: "host only", baseURL: "https://ti.example", wantEndpoint: "https://ti.example/v1/internal/observations/import"}, + {name: "trailing slash", baseURL: "http://localhost:8080/", wantEndpoint: "http://localhost:8080/v1/internal/observations/import"}, + {name: "path prefix", baseURL: "https://ti.example/trust/", wantEndpoint: "https://ti.example/trust/v1/internal/observations/import"}, + {name: "unparseable", baseURL: "::bad", wantErr: true}, + {name: "wrong scheme", baseURL: "ftp://ti.example", wantErr: true}, + {name: "no host", baseURL: "https://", wantErr: true}, + {name: "relative", baseURL: "/v1", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s, err := NewHTTPSink(tt.baseURL, "", nil) + if (err != nil) != tt.wantErr { + t.Fatalf("NewHTTPSink() error = %v, wantErr %v", err, tt.wantErr) + } + if err != nil { + return + } + if s.endpoint != tt.wantEndpoint { + t.Fatalf("endpoint = %q, want %q", s.endpoint, tt.wantEndpoint) + } + if s.client == nil || s.client.Timeout != DefaultHTTPTimeout { + t.Fatalf("client = %+v, want default with %v timeout", s.client, DefaultHTTPTimeout) + } + }) + } +} + +func TestHTTPSinkPostRequest(t *testing.T) { + t.Parallel() + body := []byte(`{"observations":[]}`) + + t.Run("authenticated and signed", func(t *testing.T) { + t.Parallel() + srv, captured := captureServer(t, http.StatusOK, `{"imported":2}`) + sink, err := NewHTTPSink(srv.URL, "admin-key", srv.Client()) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + if err := sink.Post(t.Context(), Batch{Body: body, Signature: "h.p.s"}); err != nil { + t.Fatalf("Post() error = %v", err) + } + got := <-captured + if got.method != http.MethodPost || got.path != ImportPath { + t.Fatalf("request = %s %s", got.method, got.path) + } + if ct := got.header.Get("Content-Type"); ct != "application/json" { + t.Fatalf("Content-Type = %q", ct) + } + if auth := got.header.Get("Authorization"); auth != "Bearer admin-key" { + t.Fatalf("Authorization = %q", auth) + } + if sig := got.header.Get(BatchSignatureHeader); sig != "h.p.s" { + t.Fatalf("%s = %q", BatchSignatureHeader, sig) + } + if !bytes.Equal(got.body, body) { + t.Fatalf("body = %s", got.body) + } + }) + t.Run("unauthenticated and unsigned", func(t *testing.T) { + t.Parallel() + srv, captured := captureServer(t, http.StatusCreated, "") + sink, err := NewHTTPSink(srv.URL, "", nil) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + if err := sink.Post(t.Context(), Batch{Body: body}); err != nil { + t.Fatalf("Post() error = %v", err) + } + got := <-captured + if _, present := got.header["Authorization"]; present { + t.Fatalf("Authorization sent without an admin key: %v", got.header) + } + if _, present := got.header[BatchSignatureHeader]; present { + t.Fatalf("signature header sent for an unsigned batch: %v", got.header) + } + }) +} + +func TestHTTPSinkPostErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + status int + response string + wantErr *ImportError + wantString string + }{ + { + name: "422 problem", + status: http.StatusUnprocessableEntity, + response: `{"type":"about:blank","title":"Unprocessable Entity","status":422,"detail":"signal \"x\" is not registered in this binary","code":"UNKNOWN_SIGNAL"}`, + wantErr: &ImportError{ + Status: http.StatusUnprocessableEntity, Title: "Unprocessable Entity", + Detail: `signal "x" is not registered in this binary`, Code: "UNKNOWN_SIGNAL", + }, + wantString: `hydrator: import returned HTTP 422 Unprocessable Entity (UNKNOWN_SIGNAL): signal "x" is not registered in this binary`, + }, + { + name: "400 problem without code", + status: http.StatusBadRequest, + response: `{"title":"Bad Request","status":400,"detail":"observations: at least one observation is required"}`, + wantErr: &ImportError{Status: http.StatusBadRequest, Title: "Bad Request", Detail: "observations: at least one observation is required"}, + wantString: "hydrator: import returned HTTP 400 Bad Request: observations: at least one observation is required", + }, + { + name: "401 problem", + status: http.StatusUnauthorized, + response: `{"title":"Unauthorized","status":401,"detail":"missing or invalid admin bearer key","code":"UNAUTHORIZED"}`, + wantErr: &ImportError{Status: http.StatusUnauthorized, Title: "Unauthorized", Detail: "missing or invalid admin bearer key", Code: "UNAUTHORIZED"}, + wantString: "hydrator: import returned HTTP 401 Unauthorized (UNAUTHORIZED): missing or invalid admin bearer key", + }, + { + name: "500 plain text", + status: http.StatusInternalServerError, + response: "upstream exploded\n", + wantErr: &ImportError{Status: http.StatusInternalServerError, Title: "Internal Server Error", Detail: `"upstream exploded"`}, + wantString: `hydrator: import returned HTTP 500 Internal Server Error: "upstream exploded"`, + }, + { + name: "503 empty body", + status: http.StatusServiceUnavailable, + wantErr: &ImportError{Status: http.StatusServiceUnavailable, Title: "Service Unavailable"}, + wantString: "hydrator: import returned HTTP 503 Service Unavailable", + }, + { + name: "json without problem fields", + status: http.StatusBadGateway, + response: `{"error":"nope"}`, + wantErr: &ImportError{Status: http.StatusBadGateway, Title: "Bad Gateway", Detail: `"{\"error\":\"nope\"}"`}, + wantString: `hydrator: import returned HTTP 502 Bad Gateway: "{\"error\":\"nope\"}"`, + }, + { + name: "long body is truncated", + status: http.StatusInternalServerError, + response: strings.Repeat("e", 2*maxErrorBodyBytes), + wantErr: &ImportError{Status: http.StatusInternalServerError, Title: "Internal Server Error", Detail: `"` + strings.Repeat("e", maxErrorBodyBytes) + `"`}, + }, + { + name: "control characters are quoted", + status: http.StatusInternalServerError, + response: "line1\x1b[31mred", + wantErr: &ImportError{Status: http.StatusInternalServerError, Title: "Internal Server Error", Detail: `"line1\x1b[31mred"`}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + srv, _ := captureServer(t, tt.status, tt.response) + sink, err := NewHTTPSink(srv.URL, "k", nil) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + err = sink.Post(t.Context(), Batch{Body: []byte("{}")}) + var ie *ImportError + if !errors.As(err, &ie) { + t.Fatalf("Post() error = %v, want *ImportError", err) + } + if *ie != *tt.wantErr { + t.Fatalf("ImportError = %+v, want %+v", *ie, *tt.wantErr) + } + if tt.wantString != "" && err.Error() != tt.wantString { + t.Fatalf("Error() = %q, want %q", err.Error(), tt.wantString) + } + }) + } +} + +func TestHTTPSinkPostTransportFailures(t *testing.T) { + t.Parallel() + + t.Run("connection refused", func(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.NotFoundHandler()) + srv.Close() + sink, err := NewHTTPSink(srv.URL, "", nil) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + err = sink.Post(t.Context(), Batch{Body: []byte("{}")}) + var ie *ImportError + if err == nil || errors.As(err, &ie) || !strings.Contains(err.Error(), "post import") { + t.Fatalf("Post() error = %v, want a transport error", err) + } + }) + t.Run("context cancelled", func(t *testing.T) { + t.Parallel() + srv, _ := captureServer(t, http.StatusOK, "") + sink, err := NewHTTPSink(srv.URL, "", nil) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if err := sink.Post(ctx, Batch{Body: []byte("{}")}); !errors.Is(err, context.Canceled) { + t.Fatalf("Post() error = %v, want context.Canceled", err) + } + }) + t.Run("nil context", func(t *testing.T) { + t.Parallel() + srv, _ := captureServer(t, http.StatusOK, "") + sink, err := NewHTTPSink(srv.URL, "", nil) + if err != nil { + t.Fatalf("NewHTTPSink() error = %v", err) + } + var ctx context.Context + if err := sink.Post(ctx, Batch{Body: []byte("{}")}); err == nil || !strings.Contains(err.Error(), "build import request") { + t.Fatalf("Post() error = %v, want request build failure", err) + } + }) +} + +type failingWriter struct{} + +func (failingWriter) Write([]byte) (int, error) { return 0, errors.New("disk full") } + +func TestWriterSink(t *testing.T) { + t.Parallel() + body := []byte(`{"observations":[]}`) + + t.Run("unsigned", func(t *testing.T) { + t.Parallel() + var out bytes.Buffer + if err := NewWriterSink(&out).Post(t.Context(), Batch{Body: body}); err != nil { + t.Fatalf("Post() error = %v", err) + } + if out.String() != string(body)+"\n" { + t.Fatalf("output = %q", out.String()) + } + }) + t.Run("signed", func(t *testing.T) { + t.Parallel() + var out bytes.Buffer + if err := NewWriterSink(&out).Post(t.Context(), Batch{Body: body, Signature: "h.p.s"}); err != nil { + t.Fatalf("Post() error = %v", err) + } + if out.String() != string(body)+"\nh.p.s\n" { + t.Fatalf("output = %q", out.String()) + } + }) + t.Run("write failure", func(t *testing.T) { + t.Parallel() + err := NewWriterSink(failingWriter{}).Post(t.Context(), Batch{Body: body}) + if err == nil || !strings.Contains(err.Error(), "disk full") { + t.Fatalf("Post() error = %v, want write failure", err) + } + }) +} diff --git a/internal/identity/artifacts.go b/internal/identity/artifacts.go new file mode 100644 index 0000000..329dac6 --- /dev/null +++ b/internal/identity/artifacts.go @@ -0,0 +1,131 @@ +package identity + +import ( + "context" + "crypto/x509" + "fmt" + "os" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" +) + +// Artifacts are the SCITT headers a signed request carries, base64-encoded as +// they travel: the receipt (X-SCITT-Receipt) and the status token +// (X-ANS-Status-Token). +type Artifacts struct { + ReceiptBase64 string + StatusTokenBase64 string +} + +// complete reports whether both artifacts are present. +func (a Artifacts) complete() bool { return a.ReceiptBase64 != "" && a.StatusTokenBase64 != "" } + +// ArtifactSource supplies the current artifacts of one identity. +type ArtifactSource interface { + // Current returns the artifacts to attach now. An empty field means that + // artifact is unavailable, missing or expired, and the request must be + // denied. + Current() Artifacts + // Healthy reports whether a complete, unexpired set of artifacts is held. + // A failed background refresh does not make a source unhealthy while the + // artifacts it holds are still valid. + Healthy() bool +} + +type certificateSource struct { + ArtifactSource + cert *x509.Certificate + now func() time.Time +} + +func (s *certificateSource) Current() Artifacts { + now := s.now() + if now.Before(s.cert.NotBefore) || now.After(s.cert.NotAfter) { + return Artifacts{} + } + return s.ArtifactSource.Current() +} + +func (s *certificateSource) Healthy() bool { return s.Current().complete() } + +// SupplierSource keeps artifacts fresh with the SDK's scitt.HeaderSupplier, +// which refreshes them in the background at half the status token's TTL and +// withholds an expired token. +type SupplierSource struct { + supplier *scitt.HeaderSupplier + cancel context.CancelFunc +} + +// NewSupplierSource primes supplier with one fetch and starts its background +// refresh, which runs until ctx is canceled or Close is called. The first fetch +// happens here so a log that is unreachable or does not vouch for the agent +// fails at startup rather than on the request path. +func NewSupplierSource(ctx context.Context, supplier *scitt.HeaderSupplier) (*SupplierSource, error) { + supplier.CurrentHeaders() + if !supplier.Healthy() { + return nil, fmt.Errorf("fetch artifacts: %w", supplier.LastError()) + } + return &SupplierSource{supplier: supplier, cancel: supplier.StartAutoRefresh(ctx)}, nil +} + +// Current implements ArtifactSource. +func (s *SupplierSource) Current() Artifacts { + h := s.supplier.CurrentHeaders() + return Artifacts{ReceiptBase64: h.ReceiptBase64, StatusTokenBase64: h.StatusTokenBase64} +} + +// Healthy implements ArtifactSource: the supplier withholds an expired token, +// so a complete set means the artifacts are usable now. +func (s *SupplierSource) Healthy() bool { return s.Current().complete() } + +// Close stops the background refresh. +func (s *SupplierSource) Close() { s.cancel() } + +// StaticSource serves one fixed set of artifacts, optionally until an expiry +// after which the status token is withheld the way the SDK supplier withholds +// an expired one. +type StaticSource struct { + artifacts Artifacts + expiresAt time.Time + now func() time.Time +} + +// NewStaticSource returns a source that always serves a. +func NewStaticSource(a Artifacts) *StaticSource { + return &StaticSource{artifacts: a, now: time.Now} +} + +// NewStaticSourceUntil returns a source that serves a until exp, the status +// token's expiry, and only the receipt from then on. +func NewStaticSourceUntil(a Artifacts, exp time.Time) *StaticSource { + return newStaticSourceUntil(a, exp, time.Now) +} + +func newStaticSourceUntil(a Artifacts, exp time.Time, now func() time.Time) *StaticSource { + return &StaticSource{artifacts: a, expiresAt: exp, now: now} +} + +// Current implements ArtifactSource. +func (s *StaticSource) Current() Artifacts { + if !s.expiresAt.IsZero() && !s.now().Before(s.expiresAt) { + return Artifacts{ReceiptBase64: s.artifacts.ReceiptBase64} + } + return s.artifacts +} + +// Healthy implements ArtifactSource. +func (s *StaticSource) Healthy() bool { return s.Current().complete() } + +// readArtifactFiles loads a raw COSE_Sign1 receipt and status token. +func readArtifactFiles(receiptFile, statusTokenFile string) ([]byte, []byte, error) { + receipt, err := os.ReadFile(receiptFile) + if err != nil { + return nil, nil, fmt.Errorf("read receipt_file: %w", err) + } + token, err := os.ReadFile(statusTokenFile) + if err != nil { + return nil, nil, fmt.Errorf("read status_token_file: %w", err) + } + return receipt, token, nil +} diff --git a/internal/identity/artifacts_test.go b/internal/identity/artifacts_test.go new file mode 100644 index 0000000..c17532c --- /dev/null +++ b/internal/identity/artifacts_test.go @@ -0,0 +1,164 @@ +package identity + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func TestCertificateSource(t *testing.T) { + t.Parallel() + now := time.Now() + cert := &x509.Certificate{NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour)} + full := Artifacts{ReceiptBase64: "receipt", StatusTokenBase64: "status"} + for _, tt := range []struct { + name string + now time.Time + artifacts Artifacts + wantHealthy bool + }{ + {name: "before certificate validity", now: cert.NotBefore.Add(-time.Nanosecond), artifacts: full}, + {name: "at not before", now: cert.NotBefore, artifacts: full, wantHealthy: true}, + {name: "valid", now: now, artifacts: full, wantHealthy: true}, + {name: "at not after", now: cert.NotAfter, artifacts: full, wantHealthy: true}, + {name: "expired", now: cert.NotAfter.Add(time.Nanosecond), artifacts: full}, + {name: "artifacts unavailable", now: now}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s := &certificateSource{ArtifactSource: NewStaticSource(tt.artifacts), cert: cert, now: func() time.Time { return tt.now }} + if s.Healthy() != tt.wantHealthy || s.Current().complete() != tt.wantHealthy { + t.Fatalf("healthy=%v artifacts=%+v", s.Healthy(), s.Current()) + } + }) + } +} + +func TestStaticSource(t *testing.T) { + t.Parallel() + full := Artifacts{ReceiptBase64: "r", StatusTokenBase64: "t"} + exp := time.Unix(1_700_000_000, 0) + tests := []struct { + name string + src *StaticSource + wantCurrent Artifacts + wantHealthy bool + }{ + {name: "no expiry", src: NewStaticSource(full), wantCurrent: full, wantHealthy: true}, + {name: "before expiry", src: newStaticSourceUntil(full, exp, func() time.Time { return exp.Add(-time.Second) }), + wantCurrent: full, wantHealthy: true}, + {name: "at expiry withholds the token", src: newStaticSourceUntil(full, exp, func() time.Time { return exp }), + wantCurrent: Artifacts{ReceiptBase64: "r"}}, + {name: "after expiry", src: newStaticSourceUntil(full, exp, func() time.Time { return exp.Add(time.Hour) }), + wantCurrent: Artifacts{ReceiptBase64: "r"}}, + {name: "receipt only", src: NewStaticSource(Artifacts{ReceiptBase64: "r"}), wantCurrent: Artifacts{ReceiptBase64: "r"}}, + {name: "token only", src: NewStaticSource(Artifacts{StatusTokenBase64: "t"}), wantCurrent: Artifacts{StatusTokenBase64: "t"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := tt.src.Current(); got != tt.wantCurrent { + t.Fatalf("Current() = %+v, want %+v", got, tt.wantCurrent) + } + if got := tt.src.Healthy(); got != tt.wantHealthy { + t.Fatalf("Healthy() = %v, want %v", got, tt.wantHealthy) + } + }) + } + if NewStaticSourceUntil(full, time.Now().Add(time.Hour)).Current() != full { + t.Fatal("NewStaticSourceUntil withheld an unexpired token") + } +} + +func TestReadArtifactFiles(t *testing.T) { + t.Parallel() + dir := t.TempDir() + receiptFile := filepath.Join(dir, "receipt.cose") + tokenFile := filepath.Join(dir, "token.cose") + if err := os.WriteFile(receiptFile, []byte("receipt"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(tokenFile, []byte("token"), 0o600); err != nil { + t.Fatal(err) + } + receipt, token, err := readArtifactFiles(receiptFile, tokenFile) + if err != nil || string(receipt) != "receipt" || string(token) != "token" { + t.Fatalf("readArtifactFiles() = %q, %q, %v", receipt, token, err) + } + if _, _, err := readArtifactFiles(filepath.Join(dir, "missing"), tokenFile); err == nil { + t.Fatal("accepted a missing receipt file") + } + if _, _, err := readArtifactFiles(receiptFile, filepath.Join(dir, "missing")); err == nil { + t.Fatal("accepted a missing token file") + } +} + +func TestSupplierSource(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + receipt := tl.Receipt(id.AgentID, id.AnsName) + + t.Run("serves verified artifacts and refreshes", func(t *testing.T) { + t.Parallel() + client := scitt.NewMockClient().WithReceipt(id.AgentID, receipt).WithStatusToken(id.AgentID, tl.FreshStatusToken(id)) + supplier := scitt.NewHeaderSupplier(id.AgentID, client, scitt.NewRefreshableKeyStore(tl.KeyStore(), nil)) + src, err := NewSupplierSource(t.Context(), supplier) + if err != nil { + t.Fatalf("NewSupplierSource: %v", err) + } + defer src.Close() + got := src.Current() + if got.ReceiptBase64 != base64.StdEncoding.EncodeToString(receipt) || got.StatusTokenBase64 == "" { + t.Fatalf("Current() = %+v", got) + } + if !src.Healthy() { + t.Fatal("Healthy() = false after a successful fetch") + } + }) + + t.Run("fails closed when the log does not know the agent", func(t *testing.T) { + t.Parallel() + supplier := scitt.NewHeaderSupplier(id.AgentID, scitt.NewMockClient(), scitt.NewRefreshableKeyStore(tl.KeyStore(), nil)) + if _, err := NewSupplierSource(t.Context(), supplier); err == nil { + t.Fatal("NewSupplierSource succeeded without artifacts") + } + }) + + t.Run("fails closed when the token is signed by an untrusted log", func(t *testing.T) { + t.Parallel() + rogue := testfixtures.MustNewTL() + client := scitt.NewMockClient().WithReceipt(id.AgentID, receipt).WithStatusToken(id.AgentID, rogue.FreshStatusToken(id)) + supplier := scitt.NewHeaderSupplier(id.AgentID, client, scitt.NewRefreshableKeyStore(tl.KeyStore(), nil)) + _, err := NewSupplierSource(t.Context(), supplier) + var sigErr *scitt.SignatureError + if !errors.As(err, &sigErr) { + t.Fatalf("NewSupplierSource error = %v, want a signature error", err) + } + }) + + t.Run("withholds an expired token", func(t *testing.T) { + t.Parallel() + now := time.Now().Unix() + expired := tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, now-3600, now-10, id.Fingerprint) + client := scitt.NewMockClient().WithReceipt(id.AgentID, receipt).WithStatusToken(id.AgentID, expired) + supplier := scitt.NewHeaderSupplier(id.AgentID, client, scitt.NewRefreshableKeyStore(tl.KeyStore(), nil), + scitt.WithSupplierClockSkew(time.Minute)) + src, err := NewSupplierSource(t.Context(), supplier) + if err != nil { + t.Fatalf("NewSupplierSource: %v", err) + } + defer src.Close() + if got := src.Current(); got.StatusTokenBase64 != "" || got.ReceiptBase64 == "" { + t.Fatalf("Current() = %+v, want receipt only", got) + } + }) +} diff --git a/internal/identity/client.go b/internal/identity/client.go new file mode 100644 index 0000000..f420808 --- /dev/null +++ b/internal/identity/client.go @@ -0,0 +1,73 @@ +package identity + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" +) + +type keyRefresher struct { + refresh func(context.Context) (bool, error) + now func() time.Time + + mu sync.Mutex + nextAttempt time.Time +} + +func (r *keyRefresher) verify(ctx context.Context, validate func() error) error { + err := validate() + var sigErr *scitt.SignatureError + if !errors.As(err, &sigErr) || sigErr.Type != scitt.SigErrUnknownKeyID { + return err + } + r.mu.Lock() + defer r.mu.Unlock() + if err = validate(); err == nil { + return nil + } + now := r.now() + if now.Before(r.nextAttempt) { + return err + } + // The SDK cools down successful refreshes; failed attempts need the same bound. + r.nextAttempt = now.Add(scitt.DefaultOnDemandCooldown) + if _, err := r.refresh(ctx); err != nil { + return fmt.Errorf("refresh transparency log keys: %w", err) + } + return validate() +} + +type validatingClient struct { + scitt.Client + validator *artifactValidator + keys *keyRefresher +} + +func (c *validatingClient) FetchReceipt(ctx context.Context, agentID string) ([]byte, error) { + raw, err := c.Client.FetchReceipt(ctx, agentID) + if err != nil { + return nil, err + } + if err := c.keys.verify(ctx, func() error { return c.validator.receipt(raw) }); err != nil { + return nil, err + } + return raw, nil +} + +func (c *validatingClient) FetchStatusToken(ctx context.Context, agentID string) ([]byte, error) { + raw, err := c.Client.FetchStatusToken(ctx, agentID) + if err != nil { + return nil, err + } + if err := c.keys.verify(ctx, func() error { + _, err := c.validator.status(raw) + return err + }); err != nil { + return nil, err + } + return raw, nil +} diff --git a/internal/identity/client_test.go b/internal/identity/client_test.go new file mode 100644 index 0000000..c6d440e --- /dev/null +++ b/internal/identity/client_test.go @@ -0,0 +1,163 @@ +package identity + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func TestKeyRefresher(t *testing.T) { + t.Parallel() + unknown := &scitt.SignatureError{Type: scitt.SigErrUnknownKeyID} + badSignature := &scitt.SignatureError{Type: scitt.SigErrSignatureInvalid} + for _, tt := range []struct { + name string + initial, afterRefresh, refreshErr error + wantCalls int + wantErr bool + }{ + {name: "known key"}, + {name: "malformed artifact", initial: errors.New("invalid CBOR"), wantErr: true}, + {name: "bad signature", initial: badSignature, wantErr: true}, + {name: "new key", initial: unknown, wantCalls: 1}, + {name: "key still absent", initial: unknown, afterRefresh: unknown, wantCalls: 1, wantErr: true}, + {name: "refresh unavailable", initial: unknown, refreshErr: errors.New("offline"), wantCalls: 1, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + current, calls := tt.initial, 0 + r := &keyRefresher{now: time.Now, refresh: func(context.Context) (bool, error) { + calls++ + current = tt.afterRefresh + return true, tt.refreshErr + }} + err := r.verify(t.Context(), func() error { return current }) + if (err != nil) != tt.wantErr || calls != tt.wantCalls { + t.Fatalf("verify() = %v, refresh calls=%d; wantErr=%v calls=%d", err, calls, tt.wantErr, tt.wantCalls) + } + }) + } +} + +func TestKeyRefresherFailureCooldown(t *testing.T) { + t.Parallel() + now := time.Now() + calls := 0 + r := &keyRefresher{now: func() time.Time { return now }, refresh: func(context.Context) (bool, error) { + calls++ + return false, context.DeadlineExceeded + }} + validate := func() error { return &scitt.SignatureError{Type: scitt.SigErrUnknownKeyID} } + for range 2 { + if err := r.verify(t.Context(), validate); err == nil { + t.Fatal("unknown key accepted") + } + } + if calls != 1 { + t.Fatalf("refreshes within cooldown=%d", calls) + } + now = now.Add(scitt.DefaultOnDemandCooldown) + if err := r.verify(t.Context(), validate); !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("retry=%v", err) + } + if calls != 2 { + t.Fatalf("refreshes after cooldown=%d", calls) + } +} + +func TestKeyRefresherConcurrentMisses(t *testing.T) { + t.Parallel() + var valid atomic.Bool + var calls atomic.Int32 + arrived := make(chan struct{}, 2) + release := make(chan struct{}) + r := &keyRefresher{now: time.Now, refresh: func(context.Context) (bool, error) { + calls.Add(1) + valid.Store(true) + return true, nil + }} + var wg sync.WaitGroup + for range 2 { + wg.Go(func() { + first := true + err := r.verify(t.Context(), func() error { + if first { + first = false + arrived <- struct{}{} + <-release + return &scitt.SignatureError{Type: scitt.SigErrUnknownKeyID} + } + if valid.Load() { + return nil + } + return &scitt.SignatureError{Type: scitt.SigErrUnknownKeyID} + }) + if err != nil { + t.Errorf("verify: %v", err) + } + }) + } + <-arrived + <-arrived + close(release) + wg.Wait() + if calls.Load() != 1 { + t.Fatalf("refresh calls=%d", calls.Load()) + } +} + +func TestValidatingClientRotation(t *testing.T) { + t.Parallel() + oldTL, newTL := testfixtures.MustNewTL(), testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + upstream := scitt.NewMockClient().WithReceipt(id.AgentID, newTL.Receipt(id.AgentID, id.AnsName)).WithStatusToken(id.AgentID, newTL.FreshStatusToken(id)).WithRootKeys([]string{oldTL.RootKeyC2SP(), newTL.RootKeyC2SP()}) + keys := scitt.NewRefreshableKeyStore(oldTL.KeyStore(), upstream) + client := &validatingClient{Client: upstream, validator: testValidator(t, keys, id, time.Now), keys: &keyRefresher{refresh: keys.RefreshIfCooldownElapsed, now: time.Now}} + supplier := scitt.NewHeaderSupplier(id.AgentID, client, keys) + source, err := NewSupplierSource(t.Context(), supplier) + if err != nil { + t.Fatal(err) + } + defer source.Close() + if !source.Healthy() || keys.Len() != 2 { + t.Fatalf("healthy=%v keys=%d", source.Healthy(), keys.Len()) + } + before := source.Current() + other := testfixtures.MustNewIdentity("other", "ans://v1.0.0.other.example") + upstream.WithReceipt(id.AgentID, newTL.Receipt(other.AgentID, other.AnsName)) + if err := supplier.RefreshNow(t.Context()); err == nil { + t.Fatal("accepted wrong-agent receipt") + } + if source.Current() != before { + t.Fatal("bad receipt replaced validated snapshot") + } + upstream.WithReceipt(id.AgentID, newTL.Receipt(id.AgentID, id.AnsName)).WithStatusToken(id.AgentID, newTL.FreshStatusToken(other)) + if err := supplier.RefreshNow(t.Context()); err == nil { + t.Fatal("accepted wrong-agent status") + } + if source.Current() != before { + t.Fatal("bad status replaced validated snapshot") + } +} + +func TestValidatingClientFetchFailures(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + upstream := scitt.NewMockClient().WithError(id.AgentID, context.Canceled) + keys := scitt.NewRefreshableKeyStore(tl.KeyStore(), upstream) + client := &validatingClient{Client: upstream, validator: testValidator(t, keys, id, time.Now), keys: &keyRefresher{refresh: keys.RefreshIfCooldownElapsed, now: time.Now}} + if _, err := client.FetchReceipt(t.Context(), id.AgentID); !errors.Is(err, context.Canceled) { + t.Fatalf("receipt: %v", err) + } + if _, err := client.FetchStatusToken(t.Context(), id.AgentID); !errors.Is(err, context.Canceled) { + t.Fatalf("status: %v", err) + } +} diff --git a/internal/identity/registry.go b/internal/identity/registry.go new file mode 100644 index 0000000..a072782 --- /dev/null +++ b/internal/identity/registry.go @@ -0,0 +1,390 @@ +package identity + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "maps" + "os" + "slices" + "strings" + "sync" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +// Config is one identities[] block of the service configuration. +type Config struct { + // AnsName is the registered ans:// name; it must equal the certificate's + // ans:// URI SAN. + AnsName string `yaml:"ans_name"` + // AgentID is the registration's agent id, the key under which the + // transparency log serves the receipt and status token. + AgentID string `yaml:"agent_id"` + // KeyFile holds the PEM-encoded P-256 private key (PKCS#8 or SEC1). + KeyFile string `yaml:"key_file"` + // CertFile holds the PEM-encoded ANS identity certificate. + CertFile string `yaml:"cert_file"` + // ReceiptFile and StatusTokenFile pin raw COSE_Sign1 artifacts instead of + // fetching them from the transparency log. Both or neither must be set. + ReceiptFile string `yaml:"receipt_file"` + StatusTokenFile string `yaml:"status_token_file"` +} + +// Identity is one ANS identity the middleware can sign as. +type Identity struct { + name string + ansName string + agentID string + signer Signer + artifacts ArtifactSource +} + +// NewIdentity binds a signer and an artifact source to the registration they +// prove. ansName must parse as an ans:// name and agentID must be set. +func NewIdentity(name, ansName, agentID string, signer Signer, artifacts ArtifactSource) (*Identity, error) { + if name == "" { + return nil, errors.New("identity name must not be empty") + } + if _, err := verify.ParseAnsName(ansName); err != nil { + return nil, fmt.Errorf("identity %q: ans_name: %w", name, err) + } + if agentID == "" { + return nil, fmt.Errorf("identity %q: agent_id must not be empty", name) + } + if signer == nil { + return nil, fmt.Errorf("identity %q: signer must not be nil", name) + } + if artifacts == nil { + return nil, fmt.Errorf("identity %q: artifact source must not be nil", name) + } + return &Identity{name: name, ansName: ansName, agentID: agentID, signer: signer, artifacts: artifacts}, nil +} + +// Name is the configuration name a policy selects the identity by. +func (i *Identity) Name() string { return i.name } + +// AnsName is the registered ans:// name. +func (i *Identity) AnsName() string { return i.ansName } + +// AgentID is the registration's agent id. +func (i *Identity) AgentID() string { return i.agentID } + +// Signer mints the identity's DPoP proofs. +func (i *Identity) Signer() Signer { return i.signer } + +// Artifacts supplies the identity's receipt and status token. +func (i *Identity) Artifacts() ArtifactSource { return i.artifacts } + +// Registry is the set of identities the service holds, by configuration name. +type Registry struct { + ids map[string]*Identity + closers []func() + closeOnce sync.Once +} + +// NewRegistryFromIdentities builds a Registry from identities constructed in +// memory, for tests and demos. Names must be unique. +func NewRegistryFromIdentities(ids ...*Identity) (*Registry, error) { + r := &Registry{ids: make(map[string]*Identity, len(ids))} + for _, id := range ids { + if id == nil { + return nil, errors.New("nil identity") + } + if _, dup := r.ids[id.name]; dup { + return nil, fmt.Errorf("duplicate identity %q", id.name) + } + r.ids[id.name] = id + } + return r, nil +} + +// NewRegistry loads every configured identity and fails closed on the first it +// cannot prove: an ans_name that does not parse, a certificate whose ans:// SAN +// differs from it, an unusable key, or artifacts that cannot be fetched and +// verified against the pinned transparency-log root keys. Identities that pin +// receipt_file and status_token_file never touch the log; the others share one +// log client whose root keys refresh in the background. Close releases the +// background work. +func NewRegistry(ctx context.Context, identities map[string]Config, tl config.TransparencyLog, + logger *slog.Logger) (*Registry, error) { + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + r := &Registry{ids: make(map[string]*Identity, len(identities))} + l := &loader{tl: tl, log: logger, registry: r} + for _, name := range slices.Sorted(maps.Keys(identities)) { + id, err := l.identity(ctx, name, identities[name]) + if err != nil { + r.Close() + return nil, err + } + r.ids[name] = id + } + return r, nil +} + +// Identity returns the identity registered under name. +func (r *Registry) Identity(name string) (*Identity, bool) { + id, ok := r.ids[name] + return id, ok +} + +// Names lists the identity names in sorted order. +func (r *Registry) Names() []string { + return slices.Sorted(maps.Keys(r.ids)) +} + +// Healthy reports whether every identity's artifact source is healthy. +func (r *Registry) Healthy() bool { + return len(r.Unhealthy()) == 0 +} + +// Unhealthy lists, in sorted order, the identities whose artifact source +// cannot currently supply a complete, unexpired set of artifacts. +func (r *Registry) Unhealthy() []string { + var names []string + for name, id := range r.ids { + if !id.artifacts.Healthy() { + names = append(names, name) + } + } + slices.Sort(names) + return names +} + +// Close stops the background refresh of artifacts and root keys. It is safe to +// call more than once. +func (r *Registry) Close() { + r.closeOnce.Do(func() { + for _, c := range r.closers { + c() + } + }) +} + +// loader builds identities from configuration, sharing one transparency-log +// client across those that fetch their artifacts. +type loader struct { + tl config.TransparencyLog + log *slog.Logger + registry *Registry + store *scitt.KeyStore + client *scitt.HTTPClient + keys *scitt.RefreshableKeyStore + refresh *keyRefresher +} + +func (l *loader) identity(ctx context.Context, name string, cfg Config) (*Identity, error) { + ansName, err := verify.ParseAnsName(cfg.AnsName) + if err != nil { + return nil, fmt.Errorf("identity %q: ans_name: %w", name, err) + } + if cfg.AgentID == "" { + return nil, fmt.Errorf("identity %q: agent_id must not be empty", name) + } + signer, cert, err := loadSigner(cfg) + if err != nil { + return nil, fmt.Errorf("identity %q: %w", name, err) + } + if err := checkCertAnsName(cert, ansName); err != nil { + return nil, fmt.Errorf("identity %q: %w", name, err) + } + artifacts, kind, err := l.artifacts(ctx, cfg, cert, ansName) + if err != nil { + return nil, fmt.Errorf("identity %q: %w", name, err) + } + artifacts = &certificateSource{ArtifactSource: artifacts, cert: cert, now: time.Now} + l.log.InfoContext(ctx, "identity loaded", + "identity", name, "ans_name", cfg.AnsName, "agent_id", cfg.AgentID, "artifacts", kind) + return NewIdentity(name, cfg.AnsName, cfg.AgentID, signer, artifacts) +} + +func loadSigner(cfg Config) (Signer, *x509.Certificate, error) { + keyPEM, err := os.ReadFile(cfg.KeyFile) + if err != nil { + return nil, nil, fmt.Errorf("read key_file: %w", err) + } + certPEM, err := os.ReadFile(cfg.CertFile) + if err != nil { + return nil, nil, fmt.Errorf("read cert_file: %w", err) + } + return NewPEMSigner(keyPEM, certPEM) +} + +// checkCertAnsName requires the certificate's ans:// URI SAN to name the same +// version and host as the configured ans_name. +func checkCertAnsName(cert *x509.Certificate, want *verify.AnsName) error { + got := verify.CertIdentityFromX509(cert).AnsName() + if got == nil { + return errors.New("certificate carries no ans:// URI SAN") + } + if canonicalAnsName(got) != canonicalAnsName(want) { + return fmt.Errorf("certificate ans:// SAN %s does not match ans_name %s", got, want) + } + return nil +} + +// canonicalAnsName renders an ans:// name on version and host alone, the +// comparison the SDK's verifier applies between certificate and status token. +func canonicalAnsName(a *verify.AnsName) string { + return "ans://" + a.Version.String() + "." + a.Host +} + +// artifacts returns the identity's artifact source and a label for logging. +// Whatever the source, the status token in hand is verified against the pinned +// root keys and must vouch for this certificate, agent id and ans_name: a +// misconfigured identity fails here instead of at every callee. +func (l *loader) artifacts(ctx context.Context, cfg Config, cert *x509.Certificate, + ansName *verify.AnsName) (ArtifactSource, string, error) { + if cfg.ReceiptFile != "" || cfg.StatusTokenFile != "" { + return l.staticArtifacts(cfg, cert, ansName) + } + if err := l.connect(ctx); err != nil { + return nil, "", err + } + client := &validatingClient{ + Client: l.client, + validator: &artifactValidator{keys: l.keys, cert: cert, cfg: cfg, name: ansName, now: time.Now}, + keys: l.refresh, + } + supplier := scitt.NewHeaderSupplier(cfg.AgentID, client, l.keys, scitt.WithSupplierLogger(l.log)) + src, err := NewSupplierSource(ctx, supplier) + if err != nil { + return nil, "", err + } + l.registry.closers = append(l.registry.closers, src.Close) + return src, "transparency_log", nil +} + +func (l *loader) staticArtifacts(cfg Config, cert *x509.Certificate, ansName *verify.AnsName) (ArtifactSource, string, error) { + if cfg.ReceiptFile == "" || cfg.StatusTokenFile == "" { + return nil, "", errors.New("receipt_file and status_token_file must both be set") + } + receipt, token, err := readArtifactFiles(cfg.ReceiptFile, cfg.StatusTokenFile) + if err != nil { + return nil, "", err + } + store, err := l.rootKeyStore() + if err != nil { + return nil, "", err + } + validator := &artifactValidator{keys: store, cert: cert, cfg: cfg, name: ansName, now: time.Now} + if err := validator.receipt(receipt); err != nil { + return nil, "", err + } + vt, err := validator.status(token) + if err != nil { + return nil, "", err + } + src := NewStaticSourceUntil(Artifacts{ + ReceiptBase64: base64.StdEncoding.EncodeToString(receipt), + StatusTokenBase64: base64.StdEncoding.EncodeToString(token), + }, time.Unix(vt.Payload.Exp, 0)) + return src, "static", nil +} + +// verifyStatusToken checks that token is a valid status token under keys and +// that it vouches for exactly the identity being configured: the certificate's +// fingerprint is among its valid identity certificates, and its agent id and +// ans:// name match. The certificate must also be inside its validity period. +func verifyStatusToken(token []byte, keys scitt.KeyLookup, cert *x509.Certificate, cfg Config, + ansName *verify.AnsName, now time.Time) (*scitt.VerifiedStatusToken, error) { + vt, err := scitt.VerifyStatusTokenAt(token, keys, scitt.MaxClockSkew, now.Unix()) + if err != nil { + return nil, fmt.Errorf("status token: %w", err) + } + if !scitt.MatchesIdentityCert(&vt.Payload, sha256.Sum256(cert.Raw)) { + return nil, errors.New("status token does not vouch for the configured certificate") + } + if vt.Payload.AgentID != cfg.AgentID { + return nil, fmt.Errorf("status token names agent %q, configured agent_id is %q", vt.Payload.AgentID, cfg.AgentID) + } + tokenName, err := verify.ParseAnsName(vt.Payload.AnsName) + if err != nil { + return nil, fmt.Errorf("status token ans name: %w", err) + } + if canonicalAnsName(tokenName) != canonicalAnsName(ansName) { + return nil, fmt.Errorf("status token names %s, configured ans_name is %s", vt.Payload.AnsName, cfg.AnsName) + } + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + return nil, fmt.Errorf("certificate is outside its validity period (%s to %s)", + cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339)) + } + return vt, nil +} + +// connect builds the shared transparency-log client and root-key store on +// first use. At least one root key must be pinned in configuration: the log's +// own /root-keys endpoint serves rotation, not first trust. +func (l *loader) connect(ctx context.Context) error { + if l.client != nil { + return nil + } + if err := l.tl.Validate(); err != nil { + return err + } + store, err := l.rootKeyStore() + if err != nil { + return err + } + var opts []scitt.HTTPClientOption + if l.tl.AllowInsecureTransport { + opts = append(opts, scitt.WithAllowInsecureTransport()) + } + client, err := scitt.NewHTTPClient(l.tl.BaseURL, opts...) + if err != nil { + return fmt.Errorf("transparency_log: %w", err) + } + l.client = client + l.keys = scitt.NewRefreshableKeyStore(store, client, scitt.WithRefreshLogger(l.log)) + l.refresh = &keyRefresher{refresh: l.keys.RefreshIfCooldownElapsed, now: time.Now} + l.registry.closers = append(l.registry.closers, l.keys.StartBackgroundRefreshDefault(ctx)) + return nil +} + +// rootKeyStore parses the pinned root keys once. +func (l *loader) rootKeyStore() (*scitt.KeyStore, error) { + if l.store != nil { + return l.store, nil + } + rootKeys, err := l.rootKeys() + if err != nil { + return nil, err + } + store, err := scitt.NewKeyStore(rootKeys) + if err != nil { + return nil, fmt.Errorf("transparency_log: root keys: %w", err) + } + l.store = store + return store, nil +} + +// rootKeys reads root_keys and root_keys_file; the file holds one C2SP key per +// line, with blank lines and '#' comments ignored. +func (l *loader) rootKeys() ([]string, error) { + keys := slices.Clone(l.tl.RootKeys) + if l.tl.RootKeysFile != "" { + b, err := os.ReadFile(l.tl.RootKeysFile) + if err != nil { + return nil, fmt.Errorf("transparency_log: read root_keys_file: %w", err) + } + for line := range strings.SplitSeq(string(b), "\n") { + if line = strings.TrimSpace(line); line != "" && !strings.HasPrefix(line, "#") { + keys = append(keys, line) + } + } + } + if len(keys) == 0 { + return nil, errors.New("transparency_log: at least one root key is required (root_keys or root_keys_file)") + } + return keys, nil +} diff --git a/internal/identity/registry_test.go b/internal/identity/registry_test.go new file mode 100644 index 0000000..4103ad4 --- /dev/null +++ b/internal/identity/registry_test.go @@ -0,0 +1,265 @@ +package identity + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +// writeIdentityFiles writes id's key and certificate PEMs into dir. +func writeIdentityFiles(t *testing.T, dir, name string, id *testfixtures.Identity) (string, string) { + t.Helper() + keyFile := filepath.Join(dir, name+"-key.pem") + certFile := filepath.Join(dir, name+"-cert.pem") + if err := os.WriteFile(keyFile, id.KeyPEM, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(certFile, id.CertPEM, 0o600); err != nil { + t.Fatal(err) + } + return keyFile, certFile +} + +func writeFile(t *testing.T, path string, b []byte) string { + t.Helper() + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatal(err) + } + return path +} + +type stubSigner struct{} + +func (stubSigner) Sign(context.Context, string, string, [32]byte) (string, error) { + return "proof", nil +} + +func TestNewIdentityValidation(t *testing.T) { + t.Parallel() + src := NewStaticSource(Artifacts{ReceiptBase64: "r", StatusTokenBase64: "t"}) + tests := []struct { + name string + idName string + ansName string + agentID string + signer Signer + artifacts ArtifactSource + wantErr string + }{ + {name: "ok", idName: "a", ansName: testAnsName, agentID: testAgentID, signer: stubSigner{}, artifacts: src}, + {name: "empty name", ansName: testAnsName, agentID: testAgentID, signer: stubSigner{}, artifacts: src, wantErr: "name"}, + {name: "bad ans name", idName: "a", ansName: "https://example.com", agentID: testAgentID, signer: stubSigner{}, artifacts: src, wantErr: "ans_name"}, + {name: "empty agent id", idName: "a", ansName: testAnsName, signer: stubSigner{}, artifacts: src, wantErr: "agent_id"}, + {name: "nil signer", idName: "a", ansName: testAnsName, agentID: testAgentID, artifacts: src, wantErr: "signer"}, + {name: "nil artifacts", idName: "a", ansName: testAnsName, agentID: testAgentID, signer: stubSigner{}, wantErr: "artifact source"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + id, err := NewIdentity(tt.idName, tt.ansName, tt.agentID, tt.signer, tt.artifacts) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewIdentity() error = %v, want containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("NewIdentity() error = %v", err) + } + if id.Name() != tt.idName || id.AnsName() != tt.ansName || id.AgentID() != tt.agentID || + id.Signer() != tt.signer || id.Artifacts() != tt.artifacts { + t.Fatalf("accessors do not return the constructor arguments: %+v", id) + } + }) + } +} + +func TestNewRegistryFromIdentities(t *testing.T) { + t.Parallel() + src := NewStaticSource(Artifacts{ReceiptBase64: "r", StatusTokenBase64: "t"}) + a, err := NewIdentity("a", testAnsName, testAgentID, stubSigner{}, src) + if err != nil { + t.Fatal(err) + } + b, err := NewIdentity("b", testAnsName, "agent-b", stubSigner{}, NewStaticSource(Artifacts{})) + if err != nil { + t.Fatal(err) + } + reg, err := NewRegistryFromIdentities(b, a) + if err != nil { + t.Fatalf("NewRegistryFromIdentities: %v", err) + } + defer reg.Close() + if got := reg.Names(); !slices.Equal(got, []string{"a", "b"}) { + t.Fatalf("Names() = %v", got) + } + if got, ok := reg.Identity("a"); !ok || got != a { + t.Fatalf("Identity(a) = %v, %v", got, ok) + } + if _, ok := reg.Identity("zzz"); ok { + t.Fatal("Identity(zzz) found") + } + if reg.Healthy() { + t.Fatal("Healthy() = true with an identity lacking artifacts") + } + if _, err := NewRegistryFromIdentities(a, a); err == nil { + t.Fatal("duplicate identity accepted") + } + if _, err := NewRegistryFromIdentities(a, nil); err == nil { + t.Fatal("nil identity accepted") + } + reg.Close() +} + +func TestNewRegistryFromTransparencyLog(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + payments := testfixtures.MustNewIdentity(testAgentID, testAnsName) + search := testfixtures.MustNewIdentity("agent-search", "ans://v2.1.0.search.example.com") + pinned := testfixtures.MustNewIdentity("agent-pinned", "ans://v1.0.0.pinned.example.com") + srv := testfixtures.FakeTLServer(tl, payments, search) + defer srv.Close() + + dir := t.TempDir() + payKey, payCert := writeIdentityFiles(t, dir, "payments", payments) + searchKey, searchCert := writeIdentityFiles(t, dir, "search", search) + pinnedKey, pinnedCert := writeIdentityFiles(t, dir, "pinned", pinned) + receiptFile := writeFile(t, filepath.Join(dir, "receipt.cose"), tl.Receipt(pinned.AgentID, pinned.AnsName)) + tokenFile := writeFile(t, filepath.Join(dir, "token.cose"), tl.FreshStatusToken(pinned)) + rootKeysFile := writeFile(t, filepath.Join(dir, "root-keys.txt"), []byte("\n "+tl.RootKeyC2SP()+" \n\n")) + + identities := map[string]Config{ + "payments-agent": {AnsName: testAnsName, AgentID: payments.AgentID, KeyFile: payKey, CertFile: payCert}, + "search-agent": {AnsName: search.AnsName, AgentID: search.AgentID, KeyFile: searchKey, CertFile: searchCert}, + "pinned-agent": {AnsName: pinned.AnsName, AgentID: pinned.AgentID, KeyFile: pinnedKey, CertFile: pinnedCert, + ReceiptFile: receiptFile, StatusTokenFile: tokenFile}, + } + tlCfg := config.TransparencyLog{BaseURL: srv.URL, RootKeysFile: rootKeysFile, AllowInsecureTransport: true} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) + + reg, err := NewRegistry(t.Context(), identities, tlCfg, logger) + if err != nil { + t.Fatalf("NewRegistry: %v", err) + } + defer reg.Close() + if got := reg.Names(); !slices.Equal(got, []string{"payments-agent", "pinned-agent", "search-agent"}) { + t.Fatalf("Names() = %v", got) + } + if !reg.Healthy() { + t.Fatal("Healthy() = false") + } + for _, name := range reg.Names() { + id, ok := reg.Identity(name) + if !ok { + t.Fatalf("Identity(%q) missing", name) + } + arts := id.Artifacts().Current() + if arts.ReceiptBase64 == "" || arts.StatusTokenBase64 == "" { + t.Fatalf("%s: artifacts = %+v", name, arts) + } + if _, err := id.Signer().Sign(t.Context(), "GET", "https://tools.example.com/", [32]byte{}); err != nil { + t.Fatalf("%s: Sign: %v", name, err) + } + } + if id, _ := reg.Identity("payments-agent"); id.AnsName() != testAnsName || id.AgentID() != payments.AgentID { + t.Fatalf("payments-agent = %s %s", id.AnsName(), id.AgentID()) + } + reg.Close() +} + +func TestNewRegistryFailsClosed(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + unregistered := testfixtures.MustNewIdentity("agent-unregistered", "ans://v1.0.0.ghost.example.com") + noSAN := testfixtures.MustNewIdentity("agent-nosan", "https://not-an-ans-name.example.com/") + otherVersion := testfixtures.MustNewIdentity(testAgentID, "ans://v2.0.0.payments.example.com") + srv := testfixtures.FakeTLServer(tl, id) + t.Cleanup(srv.Close) + down := testfixtures.FakeTLServer(tl, id) + down.Close() + + dir := t.TempDir() + keyFile, certFile := writeIdentityFiles(t, dir, "id", id) + _, unregCert := writeIdentityFiles(t, dir, "unreg", unregistered) + noSANKey, noSANCert := writeIdentityFiles(t, dir, "nosan", noSAN) + otherKey, otherCert := writeIdentityFiles(t, dir, "other", otherVersion) + receiptFile := writeFile(t, filepath.Join(dir, "receipt.cose"), tl.Receipt(id.AgentID, id.AnsName)) + badKeyFile := writeFile(t, filepath.Join(dir, "bad-key.pem"), []byte("not a key")) + missing := filepath.Join(dir, "missing") + + good := Config{AnsName: testAnsName, AgentID: id.AgentID, KeyFile: keyFile, CertFile: certFile} + goodTL := config.TransparencyLog{BaseURL: srv.URL, RootKeys: []string{tl.RootKeyC2SP()}, AllowInsecureTransport: true} + with := func(mod func(c *Config)) Config { + c := good + mod(&c) + return c + } + + tests := []struct { + name string + cfg Config + tl config.TransparencyLog + wantErr string + }{ + {name: "bad ans_name", cfg: with(func(c *Config) { c.AnsName = "payments.example.com" }), tl: goodTL, wantErr: "ans_name"}, + {name: "empty agent_id", cfg: with(func(c *Config) { c.AgentID = "" }), tl: goodTL, wantErr: "agent_id"}, + {name: "missing key file", cfg: with(func(c *Config) { c.KeyFile = missing }), tl: goodTL, wantErr: "read key_file"}, + {name: "missing cert file", cfg: with(func(c *Config) { c.CertFile = missing }), tl: goodTL, wantErr: "read cert_file"}, + {name: "unusable key", cfg: with(func(c *Config) { c.KeyFile = badKeyFile }), tl: goodTL, wantErr: "private key"}, + {name: "cert for another key", cfg: with(func(c *Config) { c.CertFile = unregCert }), tl: goodTL, wantErr: "signer"}, + {name: "cert without ans SAN", cfg: with(func(c *Config) { c.KeyFile, c.CertFile = noSANKey, noSANCert }), tl: goodTL, + wantErr: "no ans:// URI SAN"}, + {name: "cert SAN version differs", cfg: with(func(c *Config) { c.KeyFile, c.CertFile = otherKey, otherCert }), tl: goodTL, + wantErr: "does not match ans_name"}, + {name: "only receipt_file", cfg: with(func(c *Config) { c.ReceiptFile = receiptFile }), tl: goodTL, wantErr: "must both be set"}, + {name: "missing static token", cfg: with(func(c *Config) { c.ReceiptFile, c.StatusTokenFile = receiptFile, missing }), tl: goodTL, + wantErr: "read status_token_file"}, + {name: "no base_url", cfg: good, tl: config.TransparencyLog{RootKeys: goodTL.RootKeys}, wantErr: "base_url"}, + {name: "no root keys", cfg: good, tl: config.TransparencyLog{BaseURL: srv.URL, AllowInsecureTransport: true}, wantErr: "root key"}, + {name: "unparseable root key", cfg: good, tl: config.TransparencyLog{BaseURL: srv.URL, RootKeys: []string{"junk"}, AllowInsecureTransport: true}, + wantErr: "root keys"}, + {name: "missing root_keys_file", cfg: good, tl: config.TransparencyLog{BaseURL: srv.URL, RootKeysFile: missing, AllowInsecureTransport: true}, + wantErr: "read root_keys_file"}, + {name: "http without insecure opt-in", cfg: good, tl: config.TransparencyLog{BaseURL: srv.URL, RootKeys: goodTL.RootKeys}, + wantErr: "https"}, + {name: "log does not know the agent", cfg: with(func(c *Config) { c.AgentID = "someone-else" }), tl: goodTL, wantErr: "fetch artifacts"}, + {name: "log unreachable", cfg: good, tl: config.TransparencyLog{BaseURL: down.URL, RootKeys: goodTL.RootKeys, AllowInsecureTransport: true}, + wantErr: "fetch artifacts"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + defer cancel() + reg, err := NewRegistry(ctx, map[string]Config{"id": tt.cfg}, tt.tl, nil) + if err == nil { + reg.Close() + t.Fatal("NewRegistry() succeeded, want error") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewRegistry() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +func TestNewRegistryEmpty(t *testing.T) { + t.Parallel() + reg, err := NewRegistry(t.Context(), nil, config.TransparencyLog{}, nil) + if err != nil { + t.Fatalf("NewRegistry: %v", err) + } + defer reg.Close() + if len(reg.Names()) != 0 || !reg.Healthy() { + t.Fatalf("empty registry: names %v healthy %v", reg.Names(), reg.Healthy()) + } +} diff --git a/internal/identity/registry_verify_test.go b/internal/identity/registry_verify_test.go new file mode 100644 index 0000000..fa913e6 --- /dev/null +++ b/internal/identity/registry_verify_test.go @@ -0,0 +1,156 @@ +package identity + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/config" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func TestNewRegistryRejectsInvalidStaticReceipts(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + dir := t.TempDir() + key, cert := writeIdentityFiles(t, dir, "id", id) + token := writeFile(t, filepath.Join(dir, "status.cose"), tl.FreshStatusToken(id)) + for _, tt := range []struct { + name string + receipt []byte + wantErr bool + }{ + {name: "valid", receipt: tl.Receipt(id.AgentID, id.AnsName)}, + {name: "malformed", receipt: []byte("bad COSE"), wantErr: true}, + {name: "wrong agent", receipt: tl.Receipt("other", id.AnsName), wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + file := writeFile(t, filepath.Join(t.TempDir(), "receipt.cose"), tt.receipt) + cfg := Config{AnsName: id.AnsName, AgentID: id.AgentID, KeyFile: key, CertFile: cert, ReceiptFile: file, StatusTokenFile: token} + r, err := NewRegistry(t.Context(), map[string]Config{"id": cfg}, config.TransparencyLog{RootKeys: []string{tl.RootKeyC2SP()}}, nil) + if r != nil { + defer r.Close() + } + if (err != nil) != tt.wantErr { + t.Fatalf("NewRegistry()=%v wantErr=%v", err, tt.wantErr) + } + if err == nil { + loaded, _ := r.Identity("id") + source, ok := loaded.Artifacts().(*certificateSource) + if !ok { + t.Fatalf("source=%T, want certificate validity bound", loaded.Artifacts()) + } + source.now = func() time.Time { return source.cert.NotAfter.Add(time.Second) } + if r.Healthy() || loaded.Artifacts().Current().complete() { + t.Fatal("expired identity remains usable") + } + } + }) + } +} + +func TestNewRegistryRefreshesPublishedKey(t *testing.T) { + t.Parallel() + oldTL, newTL := testfixtures.MustNewTL(), testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + srv := testfixtures.FakeTLServer(newTL, id) + t.Cleanup(srv.Close) + key, cert := writeIdentityFiles(t, t.TempDir(), "id", id) + cfg := Config{AnsName: id.AnsName, AgentID: id.AgentID, KeyFile: key, CertFile: cert} + r, err := NewRegistry(t.Context(), map[string]Config{"id": cfg}, config.TransparencyLog{ + BaseURL: srv.URL, RootKeys: []string{oldTL.RootKeyC2SP()}, AllowInsecureTransport: true, + }, nil) + if err != nil { + t.Fatal(err) + } + defer r.Close() + if !r.Healthy() { + t.Fatal("identity unhealthy after refreshing the published signing key") + } +} + +// TestNewRegistryVerifiesArtifacts covers the startup binding between the +// configured certificate and the status token the log (or a pinned file) +// vouches with, for both artifact sources. +func TestNewRegistryVerifiesArtifacts(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + otherKey := testfixtures.MustNewIdentity(testAgentID, testAnsName) + now := time.Now() + expiredCert, err := testfixtures.NewIdentityValid(testAgentID, testAnsName, now.Add(-2*time.Hour), now.Add(-time.Hour)) + if err != nil { + t.Fatal(err) + } + srv := testfixtures.FakeTLServer(tl, id, expiredCert) + t.Cleanup(srv.Close) + + dir := t.TempDir() + keyFile, certFile := writeIdentityFiles(t, dir, "id", id) + otherKeyFile, otherCertFile := writeIdentityFiles(t, dir, "other", otherKey) + expiredKeyFile, expiredCertFile := writeIdentityFiles(t, dir, "expired", expiredCert) + receiptFile := writeFile(t, filepath.Join(dir, "receipt.cose"), tl.Receipt(id.AgentID, id.AnsName)) + freshToken := writeFile(t, filepath.Join(dir, "fresh.cose"), tl.FreshStatusToken(id)) + otherToken := writeFile(t, filepath.Join(dir, "other.cose"), tl.FreshStatusToken(otherKey)) + expiredToken := writeFile(t, filepath.Join(dir, "expired.cose"), tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, + now.Add(-2*time.Hour).Unix(), now.Add(-time.Hour).Unix(), id.Fingerprint)) + wrongAgentToken := writeFile(t, filepath.Join(dir, "wrong-agent.cose"), tl.StatusToken("someone-else", id.AnsName, scitt.StatusActive, + now.Add(-time.Minute).Unix(), now.Add(time.Hour).Unix(), id.Fingerprint)) + wrongNameToken := writeFile(t, filepath.Join(dir, "wrong-name.cose"), tl.StatusToken(id.AgentID, "ans://v9.0.0.other.example.com", + scitt.StatusActive, now.Add(-time.Minute).Unix(), now.Add(time.Hour).Unix(), id.Fingerprint)) + rootKeysFile := writeFile(t, filepath.Join(dir, "root-keys.txt"), []byte("# fixture log\n"+tl.RootKeyC2SP()+"\n\n# end\n")) + tlCfg := config.TransparencyLog{BaseURL: srv.URL, RootKeysFile: rootKeysFile, AllowInsecureTransport: true} + base := Config{AnsName: testAnsName, AgentID: id.AgentID, KeyFile: keyFile, CertFile: certFile} + static := func(token string) Config { + c := base + c.ReceiptFile, c.StatusTokenFile = receiptFile, token + return c + } + + tests := []struct { + name string + cfg Config + wantErr string + }{ + {name: "static fresh token", cfg: static(freshToken)}, + {name: "static token for another certificate", cfg: static(otherToken), wantErr: "does not vouch"}, + {name: "static expired token", cfg: static(expiredToken), wantErr: "status token"}, + {name: "static token for another agent", cfg: static(wrongAgentToken), wantErr: "names agent"}, + {name: "static token for another name", cfg: static(wrongNameToken), wantErr: "configured ans_name"}, + {name: "log vouches for a different key", cfg: Config{AnsName: testAnsName, AgentID: id.AgentID, KeyFile: otherKeyFile, CertFile: otherCertFile}, + wantErr: "does not vouch"}, + {name: "certificate outside validity", cfg: Config{AnsName: testAnsName, AgentID: expiredCert.AgentID, KeyFile: expiredKeyFile, CertFile: expiredCertFile}, + wantErr: "validity period"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + defer cancel() + reg, err := NewRegistry(ctx, map[string]Config{"id": tt.cfg}, tlCfg, nil) + if tt.wantErr == "" { + if err != nil { + t.Fatalf("NewRegistry() error = %v", err) + } + defer reg.Close() + if !reg.Healthy() { + t.Fatal("Healthy() = false") + } + return + } + if err == nil { + reg.Close() + t.Fatal("NewRegistry() succeeded, want error") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewRegistry() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} diff --git a/internal/identity/signer.go b/internal/identity/signer.go new file mode 100644 index 0000000..7495d3e --- /dev/null +++ b/internal/identity/signer.go @@ -0,0 +1,68 @@ +// Package identity holds the ANS identities the middleware signs as: a private +// key and identity certificate behind a Signer, and the receipt and status +// token an ArtifactSource keeps current. A Registry loads them from +// configuration and fails closed on anything it cannot prove at startup. +package identity + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "crypto/x509" + "fmt" + + "github.com/agentnameservice/ans-sdk-go/pop" + + "github.com/agentnameservice/ans-openshell/internal/pemkeys" +) + +// Signer mints the DPoP proof for one outbound request. It is the seam for a +// key held outside the process (Vault Transit, an HSM). +type Signer interface { + // Sign returns a compact DPoP proof binding the HTTP method, the target + // URL (normalized to htu by the signer) and the SHA-256 digest of the + // request content, which is the digest of the empty string for a bodyless + // request. + Sign(ctx context.Context, method, rawURL string, contentDigest [sha256.Size]byte) (string, error) +} + +// popSigner adapts the SDK's pop.Signer to Signer. +type popSigner struct { + signer *pop.Signer +} + +func (p popSigner) Sign(ctx context.Context, method, rawURL string, contentDigest [sha256.Size]byte) (string, error) { + return p.signer.Sign(ctx, method, rawURL, pop.WithContentDigest(contentDigest)) +} + +const ( + pemTypePKCS8 = "PRIVATE KEY" + pemTypeSEC1 = "EC PRIVATE KEY" +) + +// NewPEMSigner builds a Signer from a PEM-encoded P-256 private key (PKCS#8 or +// SEC1) and the PEM-encoded identity certificate that binds its public key. The +// parsed certificate is returned so the caller can check its ans:// URI SAN. +func NewPEMSigner(keyPEM, certPEM []byte) (Signer, *x509.Certificate, error) { + key, err := parsePrivateKeyPEM(keyPEM) + if err != nil { + return nil, nil, fmt.Errorf("private key: %w", err) + } + cert, err := parseCertificatePEM(certPEM) + if err != nil { + return nil, nil, fmt.Errorf("certificate: %w", err) + } + signer, err := pop.NewSigner(key, cert.Raw) + if err != nil { + return nil, nil, fmt.Errorf("signer: %w", err) + } + return popSigner{signer: signer}, cert, nil +} + +func parsePrivateKeyPEM(b []byte) (*ecdsa.PrivateKey, error) { + return pemkeys.ParseP256PrivateKey(b) +} + +func parseCertificatePEM(b []byte) (*x509.Certificate, error) { + return pemkeys.ParseCertificate(b) +} diff --git a/internal/identity/signer_test.go b/internal/identity/signer_test.go new file mode 100644 index 0000000..c70c264 --- /dev/null +++ b/internal/identity/signer_test.go @@ -0,0 +1,132 @@ +package identity + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "net/http" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + + "github.com/agentnameservice/ans-openshell/internal/pemkeys" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +const ( + testAgentID = "7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07" + testAnsName = "ans://v1.0.0.payments.example.com" +) + +func sec1PEM(t *testing.T, key *ecdsa.PrivateKey) []byte { + t.Helper() + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatalf("MarshalECPrivateKey: %v", err) + } + return pem.EncodeToMemory(&pem.Block{Type: pemTypeSEC1, Bytes: der}) +} + +func pkcs8PEM(t *testing.T, key any) []byte { + t.Helper() + der, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("MarshalPKCS8PrivateKey: %v", err) + } + return pem.EncodeToMemory(&pem.Block{Type: pemTypePKCS8, Bytes: der}) +} + +func TestNewPEMSigner(t *testing.T) { + t.Parallel() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + other := testfixtures.MustNewIdentity("other", testAnsName) + p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + t.Fatalf("P-384 key: %v", err) + } + _, edKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519 key: %v", err) + } + + tests := []struct { + name string + keyPEM []byte + certPEM []byte + wantErr string + }{ + {name: "pkcs8", keyPEM: id.KeyPEM, certPEM: id.CertPEM}, + {name: "sec1", keyPEM: sec1PEM(t, id.Key), certPEM: id.CertPEM}, + {name: "key not pem", keyPEM: []byte("not pem"), certPEM: id.CertPEM, wantErr: "private key: no PEM block"}, + {name: "key wrong block type", keyPEM: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: []byte{1}}), + certPEM: id.CertPEM, wantErr: `unsupported PEM block "RSA PRIVATE KEY"`}, + {name: "pkcs8 garbage", keyPEM: pem.EncodeToMemory(&pem.Block{Type: pemTypePKCS8, Bytes: []byte{1, 2}}), + certPEM: id.CertPEM, wantErr: "parse PRIVATE KEY"}, + {name: "pkcs8 not ecdsa", keyPEM: pkcs8PEM(t, edKey), certPEM: id.CertPEM, wantErr: "want an ECDSA P-256 key"}, + {name: "sec1 wrong curve", keyPEM: sec1PEM(t, p384), certPEM: id.CertPEM, wantErr: "key curve is P-384"}, + {name: "cert not pem", keyPEM: id.KeyPEM, certPEM: []byte("nope"), wantErr: "certificate: no PEM block"}, + {name: "cert wrong block type", keyPEM: id.KeyPEM, + certPEM: pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: id.CertDER}), wantErr: `unsupported PEM block "PUBLIC KEY"`}, + {name: "cert garbage", keyPEM: id.KeyPEM, + certPEM: pem.EncodeToMemory(&pem.Block{Type: pemkeys.TypeCertificate, Bytes: []byte{1, 2, 3}}), wantErr: "parse certificate"}, + {name: "cert key mismatch", keyPEM: id.KeyPEM, certPEM: other.CertPEM, wantErr: "signer:"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + signer, cert, err := NewPEMSigner(tt.keyPEM, tt.certPEM) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewPEMSigner() error = %v, want containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("NewPEMSigner() error = %v", err) + } + if signer == nil || cert == nil || string(cert.Raw) != string(id.CertDER) { + t.Fatal("NewPEMSigner() returned no signer or the wrong certificate") + } + }) + } +} + +func TestSignerBindsMethodURLAndContent(t *testing.T) { + t.Parallel() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + signer, _, err := NewPEMSigner(id.KeyPEM, id.CertPEM) + if err != nil { + t.Fatalf("NewPEMSigner: %v", err) + } + const method, target = http.MethodPost, "https://tools.example.com:443/v1/do" + body := []byte(`{"a":1}`) + proof, err := signer.Sign(t.Context(), method, target, sha256.Sum256(body)) + if err != nil { + t.Fatalf("Sign: %v", err) + } + replay := pop.NewMemoryReplayCache(t.Context(), 0) + defer replay.Close() + content := func() ([]byte, error) { return body, nil } + res, err := pop.VerifyProof(t.Context(), proof, method, "https://tools.example.com/v1/do", time.Now(), 0, replay, + pop.WithReceivedContent(content)) + if err != nil { + t.Fatalf("VerifyProof: %v", err) + } + if res.Fingerprint != id.Fingerprint { + t.Fatal("proof certificate is not the identity certificate") + } + tampered := func() ([]byte, error) { return []byte(`{"a":2}`), nil } + if _, err := pop.VerifyProof(t.Context(), proof, method, target, time.Now(), 0, replay, + pop.WithReceivedContent(tampered)); err == nil { + t.Fatal("proof verified against different content") + } + if _, err := signer.Sign(t.Context(), method, "not a url", sha256.Sum256(nil)); err == nil { + t.Fatal("Sign accepted a relative URL") + } +} diff --git a/internal/identity/validation.go b/internal/identity/validation.go new file mode 100644 index 0000000..77a1835 --- /dev/null +++ b/internal/identity/validation.go @@ -0,0 +1,76 @@ +package identity + +import ( + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" +) + +type artifactValidator struct { + keys scitt.KeyLookup + cert *x509.Certificate + cfg Config + name *verify.AnsName + now func() time.Time +} + +func (v *artifactValidator) receipt(raw []byte) error { + receipt, err := scitt.VerifyReceipt(raw, v.keys) + if err != nil { + return fmt.Errorf("receipt: %w", err) + } + return v.receiptIdentity(receipt.EventBytes) +} + +func (v *artifactValidator) receiptIdentity(raw []byte) error { + var leaf struct { + Payload struct { + Producer struct { + Event struct { + AnsID string `json:"ansId"` + AgentID string `json:"agentId"` + AnsName string `json:"ansName"` + } `json:"event"` + } `json:"producer"` + } `json:"payload"` + } + if err := json.Unmarshal(raw, &leaf); err != nil { + return fmt.Errorf("receipt event: %w", err) + } + event := leaf.Payload.Producer.Event + if event.AnsID != "" && event.AgentID != "" && event.AnsID != event.AgentID { + return errors.New("receipt event has conflicting agent identifiers") + } + agentID := event.AnsID + if agentID == "" { + agentID = event.AgentID + } + if agentID != v.cfg.AgentID { + return fmt.Errorf("receipt names agent %q, configured agent_id is %q", agentID, v.cfg.AgentID) + } + name, err := verify.ParseAnsName(event.AnsName) + if err != nil { + return fmt.Errorf("receipt ans name: %w", err) + } + if canonicalAnsName(name) != canonicalAnsName(v.name) { + return fmt.Errorf("receipt names %s, configured ans_name is %s", event.AnsName, v.cfg.AnsName) + } + return nil +} + +func (v *artifactValidator) status(raw []byte) (*scitt.VerifiedStatusToken, error) { + now := v.now() + token, err := verifyStatusToken(raw, v.keys, v.cert, v.cfg, v.name, now) + if err != nil { + return nil, err + } + if now.Unix() >= token.Payload.Exp { + return nil, errors.New("status token has expired") + } + return token, nil +} diff --git a/internal/identity/validation_test.go b/internal/identity/validation_test.go new file mode 100644 index 0000000..05a8c94 --- /dev/null +++ b/internal/identity/validation_test.go @@ -0,0 +1,103 @@ +package identity + +import ( + "crypto/x509" + "fmt" + "strings" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func testValidator(t *testing.T, keys scitt.KeyLookup, id *testfixtures.Identity, now func() time.Time) *artifactValidator { + t.Helper() + cert, err := x509.ParseCertificate(id.CertDER) + if err != nil { + t.Fatal(err) + } + name, err := verify.ParseAnsName(id.AnsName) + if err != nil { + t.Fatal(err) + } + return &artifactValidator{keys: keys, cert: cert, name: name, now: now, cfg: Config{AnsName: id.AnsName, AgentID: id.AgentID}} +} + +func TestArtifactValidatorReceipt(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + v := testValidator(t, tl.KeyStore(), id, time.Now) + for _, tt := range []struct { + name string + raw []byte + wantErr string + }{ + {name: "valid", raw: tl.Receipt(id.AgentID, id.AnsName)}, + {name: "malformed", raw: []byte("not a receipt"), wantErr: "receipt:"}, + {name: "untrusted signer", raw: testfixtures.MustNewTL().Receipt(id.AgentID, id.AnsName), wantErr: "unknown key"}, + {name: "another agent", raw: tl.Receipt("other-agent", id.AnsName), wantErr: "receipt names agent"}, + {name: "another name", raw: tl.Receipt(id.AgentID, "ans://v2.0.0.other.example"), wantErr: "configured ans_name"}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := v.receipt(tt.raw) + if tt.wantErr == "" && err != nil || tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) { + t.Fatalf("receipt() = %v, want %q", err, tt.wantErr) + } + }) + } +} + +func TestArtifactValidatorReceiptIdentity(t *testing.T) { + t.Parallel() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + v := testValidator(t, testfixtures.MustNewTL().KeyStore(), id, time.Now) + for _, tt := range []struct { + name, event string + wantErr bool + }{ + {name: "current schema", event: fmt.Sprintf(`{"ansId":%q,"ansName":%q}`, id.AgentID, id.AnsName)}, + {name: "legacy schema", event: fmt.Sprintf(`{"agentId":%q,"ansName":%q}`, id.AgentID, id.AnsName)}, + {name: "matching identifiers", event: fmt.Sprintf(`{"ansId":%q,"agentId":%q,"ansName":%q}`, id.AgentID, id.AgentID, id.AnsName)}, + {name: "case insensitive host", event: fmt.Sprintf(`{"ansId":%q,"ansName":"ans://v1.0.0.PAYMENTS.EXAMPLE.COM"}`, id.AgentID)}, + {name: "conflicting identifiers", event: fmt.Sprintf(`{"ansId":%q,"agentId":"other","ansName":%q}`, id.AgentID, id.AnsName), wantErr: true}, + {name: "missing identifier", event: fmt.Sprintf(`{"ansName":%q}`, id.AnsName), wantErr: true}, + {name: "missing name", event: fmt.Sprintf(`{"ansId":%q}`, id.AgentID), wantErr: true}, + {name: "invalid name", event: fmt.Sprintf(`{"ansId":%q,"ansName":"https://example.com"}`, id.AgentID), wantErr: true}, + {name: "different version", event: fmt.Sprintf(`{"ansId":%q,"ansName":"ans://v2.0.0.payments.example.com"}`, id.AgentID), wantErr: true}, + {name: "wrong field type", event: `{"ansId":12}`, wantErr: true}, + {name: "malformed JSON", event: `{`, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + raw := []byte(`{"payload":{"producer":{"event":` + tt.event + `}}}`) + if err := v.receiptIdentity(raw); (err != nil) != tt.wantErr { + t.Fatalf("receiptIdentity() = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestArtifactValidatorStatus(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + id := testfixtures.MustNewIdentity(testAgentID, testAnsName) + now := time.Now() + v := testValidator(t, tl.KeyStore(), id, func() time.Time { return now }) + if _, err := v.status(tl.FreshStatusToken(id)); err != nil { + t.Fatalf("valid status: %v", err) + } + for _, exp := range []int64{now.Unix(), now.Add(-time.Second).Unix()} { + if _, err := v.status(tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, now.Add(-time.Hour).Unix(), exp, id.Fingerprint)); err == nil { + t.Fatal("accepted expired status within clock skew") + } + } + now = v.cert.NotAfter.Add(time.Second) + if _, err := v.status(tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, now.Add(-time.Minute).Unix(), now.Add(time.Hour).Unix(), id.Fingerprint)); err == nil { + t.Fatal("accepted expired certificate with a fresh status token") + } +} diff --git a/internal/interceptor/grpc_test.go b/internal/interceptor/grpc_test.go new file mode 100644 index 0000000..5d34bd6 --- /dev/null +++ b/internal/interceptor/grpc_test.go @@ -0,0 +1,77 @@ +package interceptor + +import ( + "context" + "net" + "strings" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +func startServer(t *testing.T, svc *Service) interceptorv1.GatewayInterceptorClient { + t.Helper() + lis := bufconn.Listen(1 << 20) + srv := grpc.NewServer() + interceptorv1.RegisterGatewayInterceptorServer(srv, svc) + go func() { _ = srv.Serve(lis) }() + t.Cleanup(srv.Stop) + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + return interceptorv1.NewGatewayInterceptorClient(conn) +} + +func TestContractOverBufconn(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: map[string]vetting.Verdict{ + "api.example.com": registered(), + "evil.example.com": withStatus(vetting.StatusUnregistered), + }} + svc := NewService(fake, Config{Audience: testAudience, Mode: vetting.ModeRequire, ImplementationVersion: "v0.1.0-test"}, discardLogger()) + client := startServer(t, svc) + ctx := t.Context() + + manifest, err := client.Describe(ctx, &interceptorv1.DescribeRequest{Gateway: gatewayPeer(1, 0, contractCapability)}) + if err != nil { + t.Fatalf("Describe() error = %v", err) + } + checkManifest(t, manifest) + + if _, err = client.Describe(ctx, &interceptorv1.DescribeRequest{Gateway: gatewayPeer(2, 0)}); status.Code(err) != codes.FailedPrecondition { + t.Fatalf("Describe(major 2) error = %v, want FailedPrecondition", err) + } + + policy := map[string]*sandboxv1.NetworkPolicyRule{"tools": rule("api.example.com", "evil.example.com")} + res, err := client.Evaluate(ctx, validateEval(methodCreate, toStruct(t, createSandbox(policy)))) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() || res.GetStatusCode() != "PERMISSION_DENIED" || !strings.Contains(res.GetReason(), "evil.example.com ans_destination_unregistered") { + t.Fatalf("Evaluate() = %v, want a denial naming evil.example.com", res) + } + if res.GetLogAnnotations()["ans.vetting.verdicts"] != "api.example.com=registered,evil.example.com=unregistered" { + t.Fatalf("annotations = %v", res.GetLogAnnotations()) + } + + res, err = client.Evaluate(ctx, validateEval(methodCreate, toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"tools": rule("api.example.com")})))) + if err != nil || !res.GetAllowed() { + t.Fatalf("Evaluate(registered only) = %v, %v; want allowed", res, err) + } + + if _, err := client.SnapshotProviderProfiles(ctx, &interceptorv1.ProviderProfileSnapshotRequest{}); status.Code(err) != codes.Unimplemented { + t.Fatalf("SnapshotProviderProfiles() error = %v, want Unimplemented", err) + } +} diff --git a/internal/interceptor/helpers_test.go b/internal/interceptor/helpers_test.go new file mode 100644 index 0000000..46b4e71 --- /dev/null +++ b/internal/interceptor/helpers_test.go @@ -0,0 +1,211 @@ +package interceptor + +import ( + "context" + "log/slog" + "sync" + "testing" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/openshellv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +const ( + testAudience = "urn:openshell:extension:interceptor:ans" + openShellService = "openshell.v1.OpenShell" + methodCreate = "CreateSandbox" + methodSubmit = "SubmitPolicyAnalysis" + methodEdit = "EditDraftChunk" + methodImport = "ImportProviderProfiles" + methodUpdate = "UpdateProviderProfiles" + methodApprove = "ApproveDraftChunk" + methodApproveAll = "ApproveAllDraftChunks" +) + +type lookupCall struct { + host string + wait time.Duration +} + +// fakeVerifier answers scripted verdicts and records every Lookup. With +// coldFirst set, a zero-wait Lookup answers StatusUnknown so the bounded +// second pass runs for every host. +type fakeVerifier struct { + mu sync.Mutex + verdicts map[string]vetting.Verdict + calls []lookupCall + coldFirst bool +} + +func (f *fakeVerifier) Lookup(_ context.Context, host string, wait time.Duration) vetting.Verdict { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = append(f.calls, lookupCall{host: host, wait: wait}) + v, ok := f.verdicts[host] + if !ok || (f.coldFirst && wait == 0) { + return vetting.Verdict{Host: host, Status: vetting.StatusUnknown} + } + v.Host = host + return v +} + +func (f *fakeVerifier) recorded() []lookupCall { + f.mu.Lock() + defer f.mu.Unlock() + return append([]lookupCall(nil), f.calls...) +} + +// hosts returns the distinct hosts looked up, in first-call order. +func (f *fakeVerifier) hosts() []string { + var out []string + seen := map[string]bool{} + for _, c := range f.recorded() { + if !seen[c.host] { + seen[c.host] = true + out = append(out, c.host) + } + } + return out +} + +func registered() vetting.Verdict { + return vetting.Verdict{Status: vetting.StatusRegistered, AnsName: "ans://v1.0.0.tools.example.com", BadgeStatus: "ACTIVE"} +} + +func withStatus(s vetting.Status) vetting.Verdict { return vetting.Verdict{Status: s} } + +func toStruct(t *testing.T, msg proto.Message) *structpb.Struct { + t.Helper() + raw, err := protojson.Marshal(msg) + if err != nil { + t.Fatalf("protojson.Marshal: %v", err) + } + var st structpb.Struct + if err := protojson.Unmarshal(raw, &st); err != nil { + t.Fatalf("protojson.Unmarshal into Struct: %v", err) + } + return &st +} + +func mustStruct(t *testing.T, v map[string]any) *structpb.Struct { + t.Helper() + st, err := structpb.NewStruct(v) + if err != nil { + t.Fatalf("structpb.NewStruct: %v", err) + } + return st +} + +func rule(hosts ...string) *sandboxv1.NetworkPolicyRule { + endpoints := make([]*sandboxv1.NetworkEndpoint, 0, len(hosts)) + for _, h := range hosts { + endpoints = append(endpoints, endpoint(h)) + } + return &sandboxv1.NetworkPolicyRule{Name: "rule", Endpoints: endpoints} +} + +func createSandbox(rules map[string]*sandboxv1.NetworkPolicyRule) *openshellv1.CreateSandboxRequest { + return &openshellv1.CreateSandboxRequest{ + Name: "sb-1", + Spec: &openshellv1.SandboxSpec{Policy: &sandboxv1.SandboxPolicy{Version: 1, NetworkPolicies: rules}}, + } +} + +func submitPolicyAnalysis(rules ...*sandboxv1.NetworkPolicyRule) *openshellv1.SubmitPolicyAnalysisRequest { + chunks := make([]*openshellv1.PolicyChunk, 0, len(rules)) + for i, r := range rules { + chunks = append(chunks, &openshellv1.PolicyChunk{Id: "chunk-" + string(rune('a'+i)), RuleName: "rule", ProposedRule: r}) + } + return &openshellv1.SubmitPolicyAnalysisRequest{Name: "sb-1", AnalysisMode: "agent_authored", ProposedChunks: chunks} +} + +func editDraftChunk(r *sandboxv1.NetworkPolicyRule) *openshellv1.EditDraftChunkRequest { + return &openshellv1.EditDraftChunkRequest{Sandbox: "sb-1", ChunkId: "chunk-a", ProposedRule: r} +} + +func validateEval(method string, op *structpb.Struct) *interceptorv1.InterceptorEvaluation { + return validateEvalWithState(method, op, nil) +} + +func validateEvalWithState(method string, op, state *structpb.Struct) *interceptorv1.InterceptorEvaluation { + return &interceptorv1.InterceptorEvaluation{ + InterceptorName: "ans", + BindingId: "test-binding", + Service: openShellService, + Method: method, + Phase: &interceptorv1.InterceptorEvaluation_Validate{Validate: &interceptorv1.ValidateEvaluation{ + ProposedOperation: op, CurrentState: state}}, + } +} + +func withService(e *interceptorv1.InterceptorEvaluation, service string) *interceptorv1.InterceptorEvaluation { + e.Service = service + return e +} + +func gatewayPeer(major, minor uint32, required ...string) *extensionv1.PeerMetadata { + return &extensionv1.PeerMetadata{ + ProtocolVersion: &extensionv1.ProtocolVersion{Major: major, Minor: minor}, + ImplementationName: "openshell/test", + ImplementationVersion: "0.0.0-test", + SupportedCapabilities: []string{contractCapability}, + RequiredCapabilities: required, + } +} + +func discardLogger() *slog.Logger { return slog.New(slog.DiscardHandler) } + +func endpoint(host string) *sandboxv1.NetworkEndpoint { + return &sandboxv1.NetworkEndpoint{Host: host, Port: 443, Protocol: "rest"} +} + +func providerProfile(id string, endpoints ...*sandboxv1.NetworkEndpoint) *openshellv1.ProviderProfile { + return &openshellv1.ProviderProfile{Id: id, DisplayName: id, Endpoints: endpoints} +} + +func importProviderProfiles(profiles ...*openshellv1.ProviderProfile) *openshellv1.ImportProviderProfilesRequest { + items := make([]*openshellv1.ProviderProfileImportItem, 0, len(profiles)) + for _, p := range profiles { + items = append(items, &openshellv1.ProviderProfileImportItem{Profile: p, Source: p.GetId() + ".yaml"}) + } + return &openshellv1.ImportProviderProfilesRequest{Profiles: items} +} + +func updateProviderProfiles(p *openshellv1.ProviderProfile) *openshellv1.UpdateProviderProfilesRequest { + req := &openshellv1.UpdateProviderProfilesRequest{Id: p.GetId(), ExpectedResourceVersion: 1} + if p != nil { + req.Profile = &openshellv1.ProviderProfileImportItem{Profile: p} + } + return req +} + +func approveDraftChunk(chunkID string) *openshellv1.ApproveDraftChunkRequest { + return &openshellv1.ApproveDraftChunkRequest{Sandbox: "sb-1", ChunkId: chunkID, ReviewToken: "tok-" + chunkID} +} + +func approveAllDraftChunks(includeFlagged bool, chunkIDs ...string) *openshellv1.ApproveAllDraftChunksRequest { + req := &openshellv1.ApproveAllDraftChunksRequest{Sandbox: "sb-1", IncludeSecurityFlagged: includeFlagged} + for _, id := range chunkIDs { + req.Approvals = append(req.Approvals, &openshellv1.DraftChunkApproval{ChunkId: id, ReviewToken: "tok-" + id}) + } + return req +} + +func draftChunk(id, status, securityNotes string, r *sandboxv1.NetworkPolicyRule) *openshellv1.PolicyChunk { + return &openshellv1.PolicyChunk{Id: id, Status: status, RuleName: "rule-" + id, SecurityNotes: securityNotes, ProposedRule: r} +} + +// draftState is the gateway state the approval bindings resolve stored drafts +// from: the sandbox's draft policy in GetDraftPolicyResponse shape. +func draftState(t *testing.T, chunks ...*openshellv1.PolicyChunk) *structpb.Struct { + t.Helper() + return toStruct(t, &openshellv1.GetDraftPolicyResponse{Chunks: chunks, DraftVersion: 3}) +} diff --git a/internal/interceptor/hosts.go b/internal/interceptor/hosts.go new file mode 100644 index 0000000..248c8cd --- /dev/null +++ b/internal/interceptor/hosts.go @@ -0,0 +1,276 @@ +package interceptor + +import ( + "errors" + "fmt" + "slices" + "strings" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/openshellv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" +) + +// hostExtractor decodes a proposed operation, with the read-only gateway state +// the evaluation carries, and returns every destination host the operation +// would admit. +type hostExtractor func(op, state *structpb.Struct) ([]string, error) + +// errDraftUnresolved reports an approval whose stored draft rules the gateway +// did not supply, so the destinations it would install cannot be vetted. +var errDraftUnresolved = errors.New("stored draft rules could not be resolved from current_state") + +// The gateway installs only pending chunks, and the state it loads for an +// approval must carry the draft policy's chunks for them to be resolvable. +const ( + draftStatusPending = "pending" + draftChunksField = "chunks" +) + +type binding struct { + id string + rpc string + extract hostExtractor +} + +func newBinding(id, fullMethod string, extract hostExtractor) binding { + return binding{id: id, rpc: strings.TrimPrefix(fullMethod, "/"), extract: extract} +} + +func newBindings() []binding { + return []binding{ + newBinding("create-sandbox", openshellv1.OpenShell_CreateSandbox_FullMethodName, createSandboxHosts), + newBinding("update-config", openshellv1.OpenShell_UpdateConfig_FullMethodName, updateConfigHosts), + newBinding("submit-policy-analysis", openshellv1.OpenShell_SubmitPolicyAnalysis_FullMethodName, submitPolicyAnalysisHosts), + newBinding("edit-draft-chunk", openshellv1.OpenShell_EditDraftChunk_FullMethodName, editDraftChunkHosts), + newBinding("import-provider-profiles", openshellv1.OpenShell_ImportProviderProfiles_FullMethodName, importProviderProfilesHosts), + newBinding("update-provider-profiles", openshellv1.OpenShell_UpdateProviderProfiles_FullMethodName, updateProviderProfilesHosts), + newBinding("approve-draft-chunk", openshellv1.OpenShell_ApproveDraftChunk_FullMethodName, approveDraftChunkHosts), + newBinding("approve-all-draft-chunks", openshellv1.OpenShell_ApproveAllDraftChunks_FullMethodName, approveAllDraftChunksHosts), + } +} + +// updateConfigHosts covers both ways UpdateConfig admits destinations: a +// replacement policy and merge operations that add rules or allow-rule +// targets. Removals and deny rules grant nothing and are not vetted. +func updateConfigHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.UpdateConfigRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + var hosts []string + for _, rule := range req.GetPolicy().GetNetworkPolicies() { + hosts = append(hosts, ruleHosts(rule)...) + } + for _, m := range req.GetMergeOperations() { + if add := m.GetAddRule(); add != nil { + hosts = append(hosts, ruleHosts(add.GetRule())...) + } + if allow := m.GetAddAllowRules(); allow != nil && allow.GetTarget().GetHost() != "" { + hosts = append(hosts, allow.GetTarget().GetHost()) + } + } + return hosts, nil +} + +func createSandboxHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.CreateSandboxRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + var hosts []string + for _, rule := range req.GetSpec().GetPolicy().GetNetworkPolicies() { + hosts = append(hosts, ruleHosts(rule)...) + } + return hosts, nil +} + +func submitPolicyAnalysisHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.SubmitPolicyAnalysisRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + var hosts []string + for _, chunk := range req.GetProposedChunks() { + hosts = append(hosts, ruleHosts(chunk.GetProposedRule())...) + } + return hosts, nil +} + +func editDraftChunkHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.EditDraftChunkRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + return ruleHosts(req.GetProposedRule()), nil +} + +// Provider profiles carry endpoints the gateway composes into every attached +// sandbox's effective policy, so a custom profile admits destinations the same +// way a policy rule does. Built-in profiles never pass through these RPCs. +func importProviderProfilesHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.ImportProviderProfilesRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + var hosts []string + for _, item := range req.GetProfiles() { + hosts = append(hosts, endpointHosts(item.GetProfile().GetEndpoints())...) + } + return hosts, nil +} + +func updateProviderProfilesHosts(op, _ *structpb.Struct) ([]string, error) { + var req openshellv1.UpdateProviderProfilesRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + return endpointHosts(req.GetProfile().GetProfile().GetEndpoints()), nil +} + +// Approving a draft installs a stored rule the request does not carry, so the +// rule is resolved from the sandbox's draft policy in current_state, in +// GetDraftPolicyResponse shape. The gateway only reliably sends that state +// once it populates method-specific state; until then every approval is +// unresolved and the service decides per mode. +func approveDraftChunkHosts(op, state *structpb.Struct) ([]string, error) { + var req openshellv1.ApproveDraftChunkRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + return draftHosts(state, []string{req.GetChunkId()}, true) +} + +func approveAllDraftChunksHosts(op, state *structpb.Struct) ([]string, error) { + var req openshellv1.ApproveAllDraftChunksRequest + if err := decodeOperation(op, &req); err != nil { + return nil, err + } + ids := make([]string, 0, len(req.GetApprovals())) + for _, a := range req.GetApprovals() { + ids = append(ids, a.GetChunkId()) + } + return draftHosts(state, ids, req.GetIncludeSecurityFlagged()) +} + +// draftHosts lists the destinations the named stored chunks would install, the +// way the gateway installs them: pending chunks only, and security-flagged ones +// only when includeFlagged. Without ids every installable chunk counts. State +// without the draft policy's chunks, or a named chunk the state does not carry, +// cannot be vetted and is unresolved. +func draftHosts(state *structpb.Struct, ids []string, includeFlagged bool) ([]string, error) { + if state == nil || state.GetFields()[draftChunksField] == nil { + return nil, fmt.Errorf("%w: the state carries no draft chunks", errDraftUnresolved) + } + var draft openshellv1.GetDraftPolicyResponse + if err := decodeState(state, &draft); err != nil { + return nil, fmt.Errorf("%w: %w", errDraftUnresolved, err) + } + installable := func(c *openshellv1.PolicyChunk) bool { + return c.GetStatus() == draftStatusPending && (includeFlagged || c.GetSecurityNotes() == "") + } + var hosts []string + if len(ids) == 0 { + for _, c := range draft.GetChunks() { + if installable(c) { + hosts = append(hosts, ruleHosts(c.GetProposedRule())...) + } + } + return hosts, nil + } + byID := make(map[string]*openshellv1.PolicyChunk, len(draft.GetChunks())) + for _, c := range draft.GetChunks() { + byID[c.GetId()] = c + } + var missing []string + for _, id := range ids { + c, ok := byID[id] + if !ok { + missing = append(missing, clip(id)) + continue + } + if installable(c) { + hosts = append(hosts, ruleHosts(c.GetProposedRule())...) + } + } + if len(missing) > 0 { + return nil, fmt.Errorf("%w: unknown chunk ids %s", errDraftUnresolved, capped(missing)) + } + return hosts, nil +} + +// capped joins caller-supplied identifiers for a diagnostic, showing at most +// maxReasonHosts of them. +func capped(items []string) string { + shown := items + if len(shown) > maxReasonHosts { + shown = shown[:maxReasonHosts] + } + out := strings.Join(shown, ", ") + if extra := len(items) - len(shown); extra > 0 { + out += fmt.Sprintf(" (+%d more)", extra) + } + return out +} + +func ruleHosts(rule *sandboxv1.NetworkPolicyRule) []string { return endpointHosts(rule.GetEndpoints()) } + +// endpointHosts lists every destination a set of endpoints admits. An endpoint +// without a host admits whatever resolves into its allowed_ips, so those +// addresses stand in for the host and are vetted as IP destinations. +func endpointHosts(endpoints []*sandboxv1.NetworkEndpoint) []string { + hosts := make([]string, 0, len(endpoints)) + for _, ep := range endpoints { + if ep.GetHost() == "" { + hosts = append(hosts, ep.GetAllowedIps()...) + continue + } + hosts = append(hosts, ep.GetHost()) + } + return hosts +} + +// decodeOperation converts the gateway's protobuf-JSON operation into msg, +// ignoring fields this build of the contract does not know. +func decodeOperation(op *structpb.Struct, msg proto.Message) error { + return decodeStruct(op, msg, "proposed_operation") +} + +// decodeState does the same for the read-only state loaded for the evaluation. +func decodeState(state *structpb.Struct, msg proto.Message) error { + return decodeStruct(state, msg, "current_state") +} + +func decodeStruct(st *structpb.Struct, msg proto.Message, what string) error { + if st == nil { + return errors.New(what + " is required") + } + raw, err := protojson.Marshal(st) + if err != nil { + return fmt.Errorf("encode %s: %w", what, err) + } + if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, msg); err != nil { + return fmt.Errorf("decode %s: %w", what, err) + } + return nil +} + +// normalizeHosts lowercases, trims, drops trailing dots and empties, dedupes +// and sorts so evaluation and annotations are deterministic. +func normalizeHosts(hosts []string) []string { + seen := make(map[string]struct{}, len(hosts)) + out := make([]string, 0, len(hosts)) + for _, h := range hosts { + h = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(h)), ".") + if _, dup := seen[h]; h == "" || dup { + continue + } + seen[h] = struct{}{} + out = append(out, h) + } + slices.Sort(out) + return out +} diff --git a/internal/interceptor/hosts_test.go b/internal/interceptor/hosts_test.go new file mode 100644 index 0000000..7b4a54c --- /dev/null +++ b/internal/interceptor/hosts_test.go @@ -0,0 +1,168 @@ +package interceptor + +import ( + "errors" + "slices" + "strconv" + "strings" + "testing" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" +) + +func TestNormalizeHosts(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in []string + want []string + }{ + {name: "nil", in: nil, want: []string{}}, + {name: "lowercase trim trailing dot", in: []string{" API.Example.COM. "}, want: []string{"api.example.com"}}, + {name: "dedupe after normalization", in: []string{"a.example.com", "A.EXAMPLE.COM.", "a.example.com"}, want: []string{"a.example.com"}}, + {name: "drop empty", in: []string{"", " ", ".", "b.example.com"}, want: []string{"b.example.com"}}, + {name: "sorted", in: []string{"z.example.com", "a.example.com", "m.example.com"}, want: []string{"a.example.com", "m.example.com", "z.example.com"}}, + {name: "keeps globs and ip literals", in: []string{"*.example.com", "192.0.2.1", "[::1]"}, want: []string{"*.example.com", "192.0.2.1", "[::1]"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := normalizeHosts(tt.in); !slices.Equal(got, tt.want) { + t.Fatalf("normalizeHosts(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestNewBindings(t *testing.T) { + t.Parallel() + got := newBindings() + want := []struct{ id, rpc string }{ + {"create-sandbox", "openshell.v1.OpenShell/CreateSandbox"}, + {"update-config", "openshell.v1.OpenShell/UpdateConfig"}, + {"submit-policy-analysis", "openshell.v1.OpenShell/SubmitPolicyAnalysis"}, + {"edit-draft-chunk", "openshell.v1.OpenShell/EditDraftChunk"}, + {"import-provider-profiles", "openshell.v1.OpenShell/ImportProviderProfiles"}, + {"update-provider-profiles", "openshell.v1.OpenShell/UpdateProviderProfiles"}, + {"approve-draft-chunk", "openshell.v1.OpenShell/ApproveDraftChunk"}, + {"approve-all-draft-chunks", "openshell.v1.OpenShell/ApproveAllDraftChunks"}, + } + if len(got) != len(want) { + t.Fatalf("newBindings() returned %d bindings, want %d", len(got), len(want)) + } + for i, w := range want { + if got[i].id != w.id || got[i].rpc != w.rpc || got[i].extract == nil { + t.Errorf("binding %d = %+v, want %+v with an extractor", i, got[i], w) + } + } +} + +func TestExtractors(t *testing.T) { + t.Parallel() + bindings := newBindings() + extractor := func(method string) hostExtractor { + for _, b := range bindings { + if b.rpc == openShellService+"/"+method { + return b.extract + } + } + t.Fatalf("no binding for %q", method) + return nil + } + pending := draftState(t, + draftChunk("c1", "pending", "", rule("a.example.com")), + draftChunk("c2", "pending", "resolves to a private range", rule("b.example.com")), + draftChunk("c3", "approved", "", rule("c.example.com")), + ) + tests := []struct { + name string + method string + op *structpb.Struct + state *structpb.Struct + want []string + wantErr bool + wantUnresolved bool + wantErrText string + }{ + { + name: "create sandbox collects every rule", method: methodCreate, + op: toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{ + "tools": rule("api.example.com", "api.example.com"), "git": rule("github.example.com"), "empty": rule(), + })), + want: []string{"api.example.com", "api.example.com", "github.example.com"}, + }, + {name: "create sandbox without policy", method: methodCreate, op: toStruct(t, createSandbox(nil)), want: nil}, + { + name: "submit policy analysis collects every chunk", method: methodSubmit, + op: toStruct(t, submitPolicyAnalysis(rule("a.example.com"), rule("b.example.com", "c.example.com"))), + want: []string{"a.example.com", "b.example.com", "c.example.com"}, + }, + {name: "edit draft chunk", method: methodEdit, op: toStruct(t, editDraftChunk(rule("new.example.com"))), want: []string{"new.example.com"}}, + {name: "edit draft chunk without rule", method: methodEdit, op: toStruct(t, editDraftChunk(nil)), want: []string{}}, + { + name: "import provider profiles collects every profile endpoint", method: methodImport, + op: toStruct(t, importProviderProfiles( + providerProfile("crm", endpoint("crm.example.com"), endpoint("api.example.com")), + providerProfile("metrics", &sandboxv1.NetworkEndpoint{AllowedIps: []string{"198.51.100.0/24"}, Port: 443}), + )), + want: []string{"198.51.100.0/24", "api.example.com", "crm.example.com"}, + }, + {name: "import provider profiles without profiles", method: methodImport, op: toStruct(t, importProviderProfiles()), want: nil}, + {name: "update provider profile", method: methodUpdate, op: toStruct(t, updateProviderProfiles(providerProfile("crm", endpoint("new.example.com")))), want: []string{"new.example.com"}}, + {name: "update provider profile without payload", method: methodUpdate, op: toStruct(t, updateProviderProfiles(nil)), want: []string{}}, + {name: "import provider profiles nil operation", method: methodImport, op: nil, wantErr: true}, + {name: "update provider profile wrong field type", method: methodUpdate, op: mustStruct(t, map[string]any{"profile": "nope"}), wantErr: true}, + {name: "approve draft chunk resolves the stored rule", method: methodApprove, op: toStruct(t, approveDraftChunk("c1")), state: pending, want: []string{"a.example.com"}}, + {name: "approve draft chunk missing from state", method: methodApprove, op: toStruct(t, approveDraftChunk("c9")), state: pending, wantErr: true, wantUnresolved: true}, + {name: "approve draft chunk without state", method: methodApprove, op: toStruct(t, approveDraftChunk("c1")), wantErr: true, wantUnresolved: true}, + {name: "approve draft chunk wrong field type", method: methodApprove, op: mustStruct(t, map[string]any{"chunkId": 5}), state: pending, wantErr: true}, + {name: "approve draft chunk undecodable state", method: methodApprove, op: toStruct(t, approveDraftChunk("c1")), state: mustStruct(t, map[string]any{"chunks": "nope"}), wantErr: true, wantUnresolved: true}, + {name: "approve all explicit approvals", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false, "c1", "c3")), state: pending, want: []string{"a.example.com"}}, + {name: "approve all explicit flagged chunk is skipped like the gateway does", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false, "c2")), state: pending, want: nil}, + {name: "approve all explicit flagged chunk included on request", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(true, "c2")), state: pending, want: []string{"b.example.com"}}, + {name: "approve all with many unknown chunk ids caps the diagnostic", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false, manyChunkIDs(12)...)), state: pending, wantErr: true, wantUnresolved: true, wantErrText: "(+2 more)"}, + {name: "approve all against state without draft chunks", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false)), state: mustStruct(t, map[string]any{"resourceVersion": "7"}), wantErr: true, wantUnresolved: true}, + {name: "approve all against an empty draft", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false)), state: mustStruct(t, map[string]any{"chunks": []any{}}), want: nil}, + {name: "approve draft chunk that is already approved installs nothing", method: methodApprove, op: toStruct(t, approveDraftChunk("c3")), state: pending, want: nil}, + {name: "approve all explicit approval missing from state", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false, "c1", "c9")), state: pending, wantErr: true, wantUnresolved: true}, + {name: "approve all pending skips flagged chunks", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false)), state: pending, want: []string{"a.example.com"}}, + {name: "approve all pending including flagged chunks", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(true)), state: pending, want: []string{"a.example.com", "b.example.com"}}, + {name: "approve all without state", method: methodApproveAll, op: toStruct(t, approveAllDraftChunks(false)), wantErr: true, wantUnresolved: true}, + {name: "approve all wrong field type", method: methodApproveAll, op: mustStruct(t, map[string]any{"approvals": "nope"}), state: pending, wantErr: true}, + {name: "nil operation", method: methodCreate, op: nil, wantErr: true}, + {name: "wrong field type", method: methodCreate, op: mustStruct(t, map[string]any{"spec": "nope"}), wantErr: true}, + {name: "wrong nested type", method: methodSubmit, op: mustStruct(t, map[string]any{"proposedChunks": map[string]any{"a": 1}}), wantErr: true}, + {name: "unknown fields discarded", method: methodEdit, op: mustStruct(t, map[string]any{ + "future": true, "proposedRule": map[string]any{"endpoints": []any{map[string]any{"host": "x.example.com", "futureFlag": 1}}}, + }), want: []string{"x.example.com"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := extractor(tt.method)(tt.op, tt.state) + if (err != nil) != tt.wantErr { + t.Fatalf("extract() error = %v, wantErr %v", err, tt.wantErr) + } + if errors.Is(err, errDraftUnresolved) != tt.wantUnresolved { + t.Fatalf("extract() error = %v, want unresolved draft %v", err, tt.wantUnresolved) + } + if tt.wantErrText != "" && !strings.Contains(err.Error(), tt.wantErrText) { + t.Fatalf("extract() error = %v, want it to contain %q", err, tt.wantErrText) + } + slices.Sort(got) + if !tt.wantErr && !slices.Equal(got, tt.want) { + t.Fatalf("extract() = %q, want %q", got, tt.want) + } + }) + } +} + +func manyChunkIDs(n int) []string { + ids := make([]string, 0, n) + for i := range n { + ids = append(ids, "missing-"+strconv.Itoa(i)) + } + return ids +} diff --git a/internal/interceptor/review_fixes_test.go b/internal/interceptor/review_fixes_test.go new file mode 100644 index 0000000..761bc46 --- /dev/null +++ b/internal/interceptor/review_fixes_test.go @@ -0,0 +1,267 @@ +package interceptor + +import ( + "strconv" + "strings" + "testing" + "time" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/openshellv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// policyOf builds a CreateSandbox operation whose single rule admits hosts. +func policyOf(t *testing.T, hosts ...string) *structpb.Struct { + t.Helper() + return toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"r": rule(hosts...)})) +} + +func TestUpdateConfigHosts(t *testing.T) { + t.Parallel() + req := &openshellv1.UpdateConfigRequest{ + Sandbox: "sb-1", + Policy: &sandboxv1.SandboxPolicy{NetworkPolicies: map[string]*sandboxv1.NetworkPolicyRule{"r": rule("policy.example.com")}}, + MergeOperations: []*openshellv1.PolicyMergeOperation{ + {Operation: &openshellv1.PolicyMergeOperation_AddRule{AddRule: &openshellv1.AddNetworkRule{RuleName: "added", Rule: rule("added.example.com")}}}, + {Operation: &openshellv1.PolicyMergeOperation_AddAllowRules{AddAllowRules: &openshellv1.AddAllowRules{ + Target: &openshellv1.L7RuleTarget{RuleName: "r", Host: "allow.example.com"}}}}, + {Operation: &openshellv1.PolicyMergeOperation_AddDenyRules{AddDenyRules: &openshellv1.AddDenyRules{ + Target: &openshellv1.L7RuleTarget{RuleName: "r", Host: "deny.example.com"}}}}, + {Operation: &openshellv1.PolicyMergeOperation_RemoveRule{RemoveRule: &openshellv1.RemoveNetworkRule{RuleName: "old"}}}, + }, + } + hosts, err := updateConfigHosts(toStruct(t, req), nil) + if err != nil { + t.Fatalf("updateConfigHosts() error = %v", err) + } + want := []string{"policy.example.com", "added.example.com", "allow.example.com"} + if strings.Join(hosts, ",") != strings.Join(want, ",") { + t.Fatalf("updateConfigHosts() = %v, want %v", hosts, want) + } + if _, err := updateConfigHosts(nil, nil); err == nil { + t.Fatal("updateConfigHosts(nil) = nil error") + } +} + +func TestRuleHostsIncludesAllowedIPs(t *testing.T) { + t.Parallel() + r := &sandboxv1.NetworkPolicyRule{Endpoints: []*sandboxv1.NetworkEndpoint{ + {Host: "named.example.com", AllowedIps: []string{"192.0.2.1"}}, + {AllowedIps: []string{"203.0.113.5", "198.51.100.0/24"}}, + {Host: "", AllowedIps: nil}, + }} + got := ruleHosts(r) + want := []string{"named.example.com", "203.0.113.5", "198.51.100.0/24"} + if strings.Join(got, ",") != strings.Join(want, ",") { + t.Fatalf("ruleHosts() = %v, want %v", got, want) + } +} + +func TestEvaluateUpdateConfigIsVetted(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: map[string]vetting.Verdict{"evil.example.com": withStatus(vetting.StatusUnregistered)}} + svc := NewService(fake, Config{Mode: vetting.ModeRequire}, discardLogger()) + req := &openshellv1.UpdateConfigRequest{Sandbox: "sb-1", MergeOperations: []*openshellv1.PolicyMergeOperation{ + {Operation: &openshellv1.PolicyMergeOperation_AddRule{AddRule: &openshellv1.AddNetworkRule{Rule: rule("evil.example.com")}}}, + }} + res, err := svc.Evaluate(t.Context(), validateEval("UpdateConfig", toStruct(t, req))) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() || res.GetStatusCode() != statusPermissionDenied || !strings.Contains(res.GetReason(), "evil.example.com") { + t.Fatalf("UpdateConfig with an unregistered host = %v, want a denial naming it", res) + } +} + +func TestEvaluateCallerKind(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: map[string]vetting.Verdict{"api.example.com": registered()}} + svc := NewService(fake, Config{Mode: vetting.ModeRequire}, discardLogger()) + eval := validateEval(methodCreate, policyOf(t, "api.example.com")) + + gateway := extauth.ContextWithCaller(t.Context(), extauth.Caller{Kind: extauth.CallerGateway}) + res, err := svc.Evaluate(gateway, eval) + if err != nil || !res.GetAllowed() { + t.Fatalf("gateway caller = %v, %v; want allowed", res, err) + } + supervisor := extauth.ContextWithCaller(t.Context(), extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: "sb-1"}) + res, err = svc.Evaluate(supervisor, eval) + if err != nil || res.GetAllowed() || res.GetLogAnnotations()["ans.vetting"] != "caller_denied" { + t.Fatalf("supervisor caller = %v, %v; want denied with caller_denied", res, err) + } +} + +func TestEvaluateModeOffSkipsDecoding(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{} + svc := NewService(fake, Config{Mode: vetting.ModeOff}, discardLogger()) + garbage, _ := structpb.NewStruct(map[string]any{"spec": "not an object"}) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, garbage)) + if err != nil || !res.GetAllowed() || res.GetLogAnnotations()["ans.vetting"] != "vetting_off" { + t.Fatalf("mode off = %v, %v; want allowed with vetting_off", res, err) + } + if len(fake.recorded()) != 0 { + t.Fatalf("mode off performed lookups: %v", fake.recorded()) + } +} + +func TestEvaluateTooManyHosts(t *testing.T) { + t.Parallel() + hosts := make([]string, 0, maxHosts+1) + for i := range maxHosts + 1 { + hosts = append(hosts, "h"+strconv.Itoa(i)+".example.com") + } + verdicts := make(map[string]vetting.Verdict, len(hosts)) + for _, h := range hosts { + verdicts[h] = registered() + } + for _, tt := range []struct { + mode vetting.Mode + wantAllowed bool + }{{mode: vetting.ModeRequire, wantAllowed: false}, {mode: vetting.ModeObserve, wantAllowed: true}} { + fake := &fakeVerifier{verdicts: verdicts} + svc := NewService(fake, Config{Mode: tt.mode}, discardLogger()) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, policyOf(t, hosts...))) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() != tt.wantAllowed || res.GetLogAnnotations()["ans.vetting"] != "too_many_hosts" { + t.Fatalf("%s: result = %v, want allowed=%v with too_many_hosts", tt.mode, res, tt.wantAllowed) + } + if n := len(fake.hosts()); n != maxHosts { + t.Fatalf("%s: looked up %d hosts, want %d", tt.mode, n, maxHosts) + } + if !tt.wantAllowed && !strings.Contains(res.GetReason(), "more than") { + t.Fatalf("%s: reason %q does not explain the cap", tt.mode, res.GetReason()) + } + } +} + +func TestEvaluateObserveNeverWaits(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{coldFirst: true, verdicts: map[string]vetting.Verdict{"cold.example.com": registered()}} + svc := NewService(fake, Config{Mode: vetting.ModeObserve}, discardLogger()) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, policyOf(t, "cold.example.com"))) + if err != nil || !res.GetAllowed() { + t.Fatalf("observe = %v, %v; want allowed", res, err) + } + for _, c := range fake.recorded() { + if c.wait != 0 { + t.Fatalf("observe waited %v for %s", c.wait, c.host) + } + } + if ann := res.GetLogAnnotations()["ans.vetting.verdicts"]; ann != "cold.example.com=unknown" { + t.Fatalf("verdicts annotation = %q, want the cold host reported unknown", ann) + } +} + +// callsWithin compares recorded lookups, allowing a bounded wait to fall short +// of the requested budget by up to tol: the wait is the budget's remainder. +func callsWithin(got, want []lookupCall, tol time.Duration) bool { + if len(got) != len(want) { + return false + } + for i := range got { + if got[i].host != want[i].host { + return false + } + if want[i].wait == 0 && got[i].wait != 0 { + return false + } + if want[i].wait != 0 && (got[i].wait > want[i].wait || got[i].wait < want[i].wait-tol) { + return false + } + } + return true +} + +func TestEvaluateProviderProfilesAreVetted(t *testing.T) { + t.Parallel() + verdicts := map[string]vetting.Verdict{"evil.example.com": withStatus(vetting.StatusUnregistered), "crm.example.com": registered()} + tests := []struct { + name string + method string + op *structpb.Struct + }{ + {name: "import", method: methodImport, op: toStruct(t, importProviderProfiles( + providerProfile("crm", endpoint("crm.example.com")), providerProfile("evil", endpoint("evil.example.com"))))}, + {name: "update", method: methodUpdate, op: toStruct(t, updateProviderProfiles(providerProfile("crm", endpoint("evil.example.com"))))}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require := NewService(&fakeVerifier{verdicts: verdicts}, Config{Mode: vetting.ModeRequire}, discardLogger()) + res, err := require.Evaluate(t.Context(), validateEval(tt.method, tt.op)) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() || res.GetStatusCode() != statusPermissionDenied || !strings.Contains(res.GetReason(), "evil.example.com") { + t.Fatalf("%s with an unregistered profile endpoint = %v, want a denial naming it", tt.method, res) + } + observe := NewService(&fakeVerifier{verdicts: verdicts}, Config{Mode: vetting.ModeObserve}, discardLogger()) + res, err = observe.Evaluate(t.Context(), validateEval(tt.method, tt.op)) + if err != nil || !res.GetAllowed() || !strings.Contains(res.GetLogAnnotations()["ans.vetting.verdicts"], "evil.example.com=unregistered") { + t.Fatalf("observe %s = %v, %v; want allowed with the unregistered endpoint annotated", tt.method, res, err) + } + }) + } +} + +func TestEvaluateDraftApprovals(t *testing.T) { + t.Parallel() + verdicts := map[string]vetting.Verdict{"evil.example.com": withStatus(vetting.StatusUnregistered), "crm.example.com": registered()} + stored := func(host string) *structpb.Struct { + return draftState(t, draftChunk("c1", "pending", "", rule(host))) + } + operations := map[string]*structpb.Struct{ + methodApprove: toStruct(t, approveDraftChunk("c1")), + methodApproveAll: toStruct(t, approveAllDraftChunks(false, "c1")), + } + tests := []struct { + name string + mode vetting.Mode + state *structpb.Struct + wantAllowed bool + wantOutcome string + wantReason string + }{ + {name: "require denies an unregistered stored rule", mode: vetting.ModeRequire, state: stored("evil.example.com"), wantReason: "evil.example.com"}, + {name: "require allows a registered stored rule", mode: vetting.ModeRequire, state: stored("crm.example.com"), wantAllowed: true}, + {name: "require refuses an approval it cannot resolve", mode: vetting.ModeRequire, wantOutcome: "draft_unresolved", wantReason: "current_state"}, + {name: "observe lets an unresolved approval through and records it", mode: vetting.ModeObserve, wantAllowed: true, wantOutcome: "draft_unresolved"}, + {name: "require refuses state without draft chunks", mode: vetting.ModeRequire, state: mustStruct(t, map[string]any{"resourceVersion": "7"}), wantOutcome: "draft_unresolved", wantReason: "no draft chunks"}, + {name: "require refuses an undecodable state", mode: vetting.ModeRequire, state: mustStruct(t, map[string]any{"chunks": "nope"}), wantOutcome: "draft_unresolved", wantReason: "current_state"}, + {name: "observe lets an undecodable state through and records it", mode: vetting.ModeObserve, state: mustStruct(t, map[string]any{"chunks": "nope"}), wantAllowed: true, wantOutcome: "draft_unresolved"}, + } + for method, op := range operations { + for _, tt := range tests { + t.Run(method+"/"+tt.name, func(t *testing.T) { + t.Parallel() + svc := NewService(&fakeVerifier{verdicts: verdicts}, Config{Mode: tt.mode}, discardLogger()) + res, err := svc.Evaluate(t.Context(), validateEvalWithState(method, op, tt.state)) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() != tt.wantAllowed { + t.Fatalf("allowed = %v, want %v (%v)", res.GetAllowed(), tt.wantAllowed, res) + } + if !tt.wantAllowed && res.GetStatusCode() != statusPermissionDenied { + t.Fatalf("status = %q, want %q", res.GetStatusCode(), statusPermissionDenied) + } + if tt.wantOutcome != "" && res.GetLogAnnotations()["ans.vetting"] != tt.wantOutcome { + t.Fatalf("outcome = %q, want %q", res.GetLogAnnotations()["ans.vetting"], tt.wantOutcome) + } + if got := res.GetLogAnnotations()["ans.vetting.denied"]; got != strconv.FormatBool(!tt.wantAllowed) { + t.Fatalf("denied annotation = %q, want %v", got, !tt.wantAllowed) + } + if tt.wantReason != "" && !strings.Contains(res.GetReason(), tt.wantReason) { + t.Fatalf("reason %q does not mention %q", res.GetReason(), tt.wantReason) + } + }) + } + } +} diff --git a/internal/interceptor/service.go b/internal/interceptor/service.go new file mode 100644 index 0000000..f93f63a --- /dev/null +++ b/internal/interceptor/service.go @@ -0,0 +1,356 @@ +// Package interceptor implements the OpenShell gateway interceptor that vets +// the destination hosts in proposed sandbox policies and Advisor proposals +// against ANS registration before the gateway commits them. +package interceptor + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +const ( + manifestName = "ans-openshell-interceptor" + implementationName = "agentnameservice/ans-openshell" + contractCapability = "openshell.gateway-interceptor.contract" + failClosed = "fail_closed" + protocolMajor = 1 + protocolMinor = 0 + + statusPermissionDenied = "PERMISSION_DENIED" + statusFailedPrecondition = "FAILED_PRECONDITION" + + // defaultLookupWait stays under OpenShell's default 500 ms interceptor + // timeout; the gateway does not propagate its deadline on the RPC. + defaultLookupWait = 300 * time.Millisecond + deadlineMargin = 50 * time.Millisecond + maxReasonHosts = 10 + maxVerdictsBytes = 1024 + maxHostLogBytes = 64 + // maxHosts bounds the destinations one operation may name; beyond it the + // operation is denied under require_registered rather than vetted + // piecemeal, and only the first maxHosts are vetted under observe. + maxHosts = 64 + + annotationOutcome = "ans.vetting" + annotationMode = "ans.vetting.mode" + annotationVerdicts = "ans.vetting.verdicts" + annotationDenied = "ans.vetting.denied" + + outcomeSkippedPhase = "skipped_phase" + outcomeUnboundMethod = "unbound_method" + outcomeDecodeError = "decode_error" + outcomeVettingOff = "vetting_off" + outcomeCallerDenied = "caller_denied" + outcomeTooManyHosts = "too_many_hosts" + // outcomeDraftUnresolved marks an approval whose stored rules the gateway + // did not supply in current_state. + outcomeDraftUnresolved = "draft_unresolved" +) + +// Config selects the vetting policy the interceptor applies. +type Config struct { + // Audience is the exact JWT audience this service verifies; it is returned + // as expected_audience so a mis-registered gateway refuses to start. + Audience string + // Mode is the destination policy: off, observe or require_registered. + Mode vetting.Mode + // ImplementationVersion is reported to the gateway for diagnostics. + ImplementationVersion string + // LookupWait bounds how long one evaluation waits, in total, for cold + // lookups under require_registered; observe never waits. Zero selects + // 300 ms. It must stay below the registration's timeout in the gateway + // configuration, which the gateway does not propagate as a deadline. + LookupWait time.Duration +} + +// Service implements openshell.gateway_interceptor.v1.GatewayInterceptor. +type Service struct { + interceptorv1.UnimplementedGatewayInterceptorServer + vet vetting.Verifier + cfg Config + logger *slog.Logger + bindings []binding +} + +// NewService builds the interceptor around vet. +func NewService(vet vetting.Verifier, cfg Config, logger *slog.Logger) *Service { + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + if cfg.LookupWait <= 0 { + cfg.LookupWait = defaultLookupWait + } + return &Service{ + vet: vet, + cfg: cfg, + logger: logger, + bindings: newBindings(), + } +} + +// Describe negotiates the protocol and returns the manifest. +func (s *Service) Describe(ctx context.Context, req *interceptorv1.DescribeRequest) (*interceptorv1.InterceptorManifest, error) { + gw := req.GetGateway() + if gw == nil { + return nil, status.Error(codes.FailedPrecondition, "gateway protocol metadata is required") + } + version := gw.GetProtocolVersion() + if version.GetMajor() != protocolMajor { + return nil, status.Errorf(codes.FailedPrecondition, + "unsupported gateway-interceptor protocol major %d, want %d", version.GetMajor(), protocolMajor) + } + for _, capability := range gw.GetRequiredCapabilities() { + if capability != contractCapability { + return nil, status.Errorf(codes.FailedPrecondition, "gateway requires unsupported capability %q", capability) + } + } + s.logger.InfoContext(ctx, "gateway negotiated", + "implementation", gw.GetImplementationName(), + "version", gw.GetImplementationVersion(), + "protocol", fmt.Sprintf("%d.%d", version.GetMajor(), version.GetMinor())) + return s.manifest(), nil +} + +// Evaluate vets the hosts of a validate-phase operation. Other phases, unbound +// methods and a service with vetting off are allowed untouched; an undecodable +// operation is denied because the binding is fail-closed, and only the gateway +// may evaluate. A draft approval whose stored rules are not in current_state is +// refused under require_registered and allowed with an annotation under observe. +func (s *Service) Evaluate(ctx context.Context, req *interceptorv1.InterceptorEvaluation) (*interceptorv1.InterceptorResult, error) { + started := time.Now() + if c, ok := extauth.CallerFromContext(ctx); ok && c.Kind != extauth.CallerGateway { + s.logger.WarnContext(ctx, "denied evaluation from a non-gateway caller", "caller_kind", string(c.Kind), "method", req.GetMethod()) + return &interceptorv1.InterceptorResult{ + StatusCode: statusPermissionDenied, + Reason: "ans destination vetting: only the gateway may evaluate operations", + LogAnnotations: map[string]string{annotationOutcome: outcomeCallerDenied}, + }, nil + } + validate := req.GetValidate() + if validate == nil { + return s.skipped(ctx, req, outcomeSkippedPhase), nil + } + b, ok := s.binding(req.GetService(), req.GetMethod()) + if !ok { + return s.skipped(ctx, req, outcomeUnboundMethod), nil + } + if s.cfg.Mode == vetting.ModeOff { + return s.skipped(ctx, req, outcomeVettingOff), nil + } + hosts, err := b.extract(validate.GetProposedOperation(), validate.GetCurrentState()) + if errors.Is(err, errDraftUnresolved) { + return s.draftUnresolved(ctx, req, err), nil + } + if err != nil { + s.logger.WarnContext(ctx, "denied undecodable operation", + "method", req.GetMethod(), "binding", req.GetBindingId(), "err", err) + return &interceptorv1.InterceptorResult{ + StatusCode: statusFailedPrecondition, + Reason: "ans destination vetting: proposed " + req.GetMethod() + " operation could not be decoded", + LogAnnotations: map[string]string{annotationOutcome: outcomeDecodeError}, + }, nil + } + hosts = normalizeHosts(hosts) + tooMany := len(hosts) > maxHosts + if tooMany { + hosts = hosts[:maxHosts] + } + verdicts := s.vetAll(ctx, hosts) + denied := s.denials(hosts, verdicts) + if tooMany && s.cfg.Mode == vetting.ModeRequire { + denied = append(denied, fmt.Sprintf("operation names more than %d destinations", maxHosts)) + } + result := &interceptorv1.InterceptorResult{ + Allowed: len(denied) == 0, + LogAnnotations: map[string]string{ + annotationMode: s.cfg.Mode.String(), + annotationVerdicts: verdictsAnnotation(hosts, verdicts), + annotationDenied: strconv.FormatBool(len(denied) > 0), + }, + } + if tooMany { + result.LogAnnotations[annotationOutcome] = outcomeTooManyHosts + } + if len(denied) > 0 { + result.StatusCode = statusPermissionDenied + result.Reason = denialReason(denied) + } + s.logger.InfoContext(ctx, "destination vetting evaluated", + "method", req.GetMethod(), "binding", req.GetBindingId(), "mode", s.cfg.Mode.String(), + "hosts", len(hosts), "denied", len(denied), "denied_hosts", denialReason(denied), + "duration_ms", time.Since(started).Milliseconds()) + return result, nil +} + +func (s *Service) skipped(ctx context.Context, req *interceptorv1.InterceptorEvaluation, outcome string) *interceptorv1.InterceptorResult { + s.logger.InfoContext(ctx, "operation not vetted", + "method", req.GetMethod(), "service", req.GetService(), "binding", req.GetBindingId(), "outcome", outcome) + return &interceptorv1.InterceptorResult{ + Allowed: true, + LogAnnotations: map[string]string{annotationOutcome: outcome}, + } +} + +// draftUnresolved handles an approval whose stored rules the gateway did not +// supply: require_registered refuses to install destinations it has not seen, +// observe lets the approval through and records the gap. +func (s *Service) draftUnresolved(ctx context.Context, req *interceptorv1.InterceptorEvaluation, err error) *interceptorv1.InterceptorResult { + allowed := s.cfg.Mode != vetting.ModeRequire + level := slog.LevelWarn + if allowed { + level = slog.LevelInfo + } + s.logger.LogAttrs(ctx, level, "draft approval could not be vetted", + slog.String("method", req.GetMethod()), slog.String("binding", req.GetBindingId()), + slog.String("mode", s.cfg.Mode.String()), slog.String("err", err.Error())) + result := &interceptorv1.InterceptorResult{ + Allowed: allowed, + LogAnnotations: map[string]string{ + annotationMode: s.cfg.Mode.String(), + annotationOutcome: outcomeDraftUnresolved, + annotationDenied: strconv.FormatBool(!allowed), + }, + } + if !result.GetAllowed() { + result.StatusCode = statusPermissionDenied + result.Reason = "ans destination vetting: " + err.Error() + } + return result +} + +// binding selects the binding whose rpc selector names the evaluated +// service and method. +func (s *Service) binding(service, method string) (binding, bool) { + if service == "" || method == "" { + return binding{}, false + } + rpc := service + "/" + method + for _, b := range s.bindings { + if b.rpc == rpc { + return b, true + } + } + return binding{}, false +} + +func (s *Service) manifest() *interceptorv1.InterceptorManifest { + bindings := make([]*interceptorv1.InterceptorBinding, 0, len(s.bindings)) + for _, b := range s.bindings { + bindings = append(bindings, &interceptorv1.InterceptorBinding{ + Id: b.id, + Selector: &interceptorv1.InterceptorSelector{Rpc: b.rpc}, + Phases: []interceptorv1.GatewayInterceptorPhase{interceptorv1.GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_VALIDATE}, + }) + } + return &interceptorv1.InterceptorManifest{ + Name: manifestName, + Bindings: bindings, + FailurePolicy: failClosed, + ProviderProfiles: false, + ExpectedAudience: s.cfg.Audience, + Extension: &extensionv1.PeerMetadata{ + ProtocolVersion: &extensionv1.ProtocolVersion{Major: protocolMajor, Minor: protocolMinor}, + ImplementationName: implementationName, + ImplementationVersion: s.cfg.ImplementationVersion, + SupportedCapabilities: []string{contractCapability}, + }, + } +} + +// vetAll starts every cold lookup with a zero wait so they run concurrently. +// Under require_registered it then waits for the ones still unknown within one +// shared LookupWait budget; observe returns at once and the background lookups +// warm the cache for the next evaluation. +func (s *Service) vetAll(ctx context.Context, hosts []string) []vetting.Verdict { + verdicts := make([]vetting.Verdict, len(hosts)) + for i, host := range hosts { + verdicts[i] = s.vet.Lookup(ctx, host, 0) + } + if s.cfg.Mode != vetting.ModeRequire { + return verdicts + } + deadline := time.Now().Add(s.lookupWait(ctx)) + for i, host := range hosts { + if verdicts[i].Status != vetting.StatusUnknown { + continue + } + if wait := time.Until(deadline); wait > 0 { + verdicts[i] = s.vet.Lookup(ctx, host, wait) + } + } + return verdicts +} + +func (s *Service) lookupWait(ctx context.Context) time.Duration { + wait := s.cfg.LookupWait + if deadline, ok := ctx.Deadline(); ok { + wait = min(wait, time.Until(deadline)-deadlineMargin) + } + return max(wait, 0) +} + +func (s *Service) denials(hosts []string, verdicts []vetting.Verdict) []string { + var denied []string + for i, v := range verdicts { + if d := vetting.Decide(s.cfg.Mode, v); d.Deny { + denied = append(denied, clip(hosts[i])+" "+d.ReasonCode) + } + } + return denied +} + +func denialReason(denied []string) string { + if len(denied) == 0 { + return "" + } + shown := denied + if len(shown) > maxReasonHosts { + shown = shown[:maxReasonHosts] + } + reason := "ans destination vetting: " + strings.Join(shown, ", ") + if extra := len(denied) - len(shown); extra > 0 { + reason += fmt.Sprintf(" (+%d more)", extra) + } + return reason +} + +func verdictsAnnotation(hosts []string, verdicts []vetting.Verdict) string { + var b strings.Builder + for i, v := range verdicts { + entry := clip(hosts[i]) + "=" + v.Status.String() + if b.Len() > 0 { + entry = "," + entry + } + if b.Len()+len(entry) > maxVerdictsBytes { + if b.Len() > 0 { + b.WriteString(",") + } + fmt.Fprintf(&b, "+%d more", len(verdicts)-i) + break + } + b.WriteString(entry) + } + return b.String() +} + +// clip bounds a host string in diagnostics; hosts are operator or agent +// authored and may be arbitrarily long. +func clip(host string) string { + if len(host) <= maxHostLogBytes { + return host + } + return host[:maxHostLogBytes] + "..." +} diff --git a/internal/interceptor/service_test.go b/internal/interceptor/service_test.go new file mode 100644 index 0000000..37e804e --- /dev/null +++ b/internal/interceptor/service_test.go @@ -0,0 +1,548 @@ +package interceptor + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "slices" + "strings" + "testing" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/interceptorv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +func TestDescribe(t *testing.T) { + t.Parallel() + svc := NewService(&fakeVerifier{}, Config{Audience: testAudience, ImplementationVersion: "v0.1.0-test"}, discardLogger()) + tests := []struct { + name string + gateway *extensionv1.PeerMetadata + wantCode codes.Code + }{ + {name: "protocol 1.0", gateway: gatewayPeer(1, 0)}, + {name: "protocol 1.3", gateway: gatewayPeer(1, 3)}, + {name: "requires the contract capability", gateway: gatewayPeer(1, 0, contractCapability)}, + {name: "missing metadata", gateway: nil, wantCode: codes.FailedPrecondition}, + {name: "missing protocol version", gateway: &extensionv1.PeerMetadata{ImplementationName: "openshell/test"}, wantCode: codes.FailedPrecondition}, + {name: "major 2", gateway: gatewayPeer(2, 0), wantCode: codes.FailedPrecondition}, + {name: "requires unsupported capability", gateway: gatewayPeer(1, 0, contractCapability, "openshell.gateway-interceptor.provider-profiles"), wantCode: codes.FailedPrecondition}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + m, err := svc.Describe(t.Context(), &interceptorv1.DescribeRequest{Gateway: tt.gateway}) + if tt.wantCode != codes.OK { + if status.Code(err) != tt.wantCode || m != nil { + t.Fatalf("Describe() = %v, %v; want code %v", m, err, tt.wantCode) + } + return + } + if err != nil { + t.Fatalf("Describe() error = %v", err) + } + checkManifest(t, m) + }) + } +} + +func checkManifest(t *testing.T, m *interceptorv1.InterceptorManifest) { + t.Helper() + if m.GetName() != "ans-openshell-interceptor" || m.GetFailurePolicy() != "fail_closed" || m.GetProviderProfiles() { + t.Fatalf("manifest header = %q/%q/%v", m.GetName(), m.GetFailurePolicy(), m.GetProviderProfiles()) + } + if m.GetExpectedAudience() != testAudience { + t.Fatalf("expected_audience = %q, want %q", m.GetExpectedAudience(), testAudience) + } + ext := m.GetExtension() + if ext.GetProtocolVersion().GetMajor() != 1 || ext.GetProtocolVersion().GetMinor() != 0 { + t.Fatalf("protocol_version = %v, want 1.0", ext.GetProtocolVersion()) + } + if ext.GetImplementationName() != "agentnameservice/ans-openshell" || ext.GetImplementationVersion() != "v0.1.0-test" { + t.Fatalf("implementation = %q %q", ext.GetImplementationName(), ext.GetImplementationVersion()) + } + if !slices.Equal(ext.GetSupportedCapabilities(), []string{"openshell.gateway-interceptor.contract"}) || len(ext.GetRequiredCapabilities()) != 0 { + t.Fatalf("capabilities = %v / %v", ext.GetSupportedCapabilities(), ext.GetRequiredCapabilities()) + } + want := map[string]string{ + "create-sandbox": "openshell.v1.OpenShell/CreateSandbox", + "update-config": "openshell.v1.OpenShell/UpdateConfig", + "submit-policy-analysis": "openshell.v1.OpenShell/SubmitPolicyAnalysis", + "edit-draft-chunk": "openshell.v1.OpenShell/EditDraftChunk", + "import-provider-profiles": "openshell.v1.OpenShell/ImportProviderProfiles", + "update-provider-profiles": "openshell.v1.OpenShell/UpdateProviderProfiles", + "approve-draft-chunk": "openshell.v1.OpenShell/ApproveDraftChunk", + "approve-all-draft-chunks": "openshell.v1.OpenShell/ApproveAllDraftChunks", + } + if len(m.GetBindings()) != len(want) { + t.Fatalf("bindings = %v, want %d", m.GetBindings(), len(want)) + } + for _, b := range m.GetBindings() { + if rpc, ok := want[b.GetId()]; !ok || b.GetSelector().GetRpc() != rpc { + t.Errorf("binding %q selects %q, want %q", b.GetId(), b.GetSelector().GetRpc(), rpc) + } + if phases := b.GetPhases(); len(phases) != 1 || phases[0] != interceptorv1.GatewayInterceptorPhase_GATEWAY_INTERCEPTOR_PHASE_VALIDATE { + t.Errorf("binding %q phases = %v, want [VALIDATE]", b.GetId(), phases) + } + if b.GetFailurePolicy() != "" { + t.Errorf("binding %q sets failure_policy %q; the manifest default applies", b.GetId(), b.GetFailurePolicy()) + } + } +} + +func TestEvaluate(t *testing.T) { + t.Parallel() + policy := func(hosts ...string) *structpb.Struct { + return toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"tools": rule(hosts...)})) + } + malformed := mustStruct(t, map[string]any{"spec": map[string]any{"policy": map[string]any{"networkPolicies": "nope"}}}) + unknownFields := mustStruct(t, map[string]any{ + "futureField": 1, + "spec": map[string]any{"policy": map[string]any{"networkPolicies": map[string]any{ + "tools": map[string]any{"endpoints": []any{map[string]any{"host": "api.example.com", "port": 443, "futureOption": true}}}, + }}}, + }) + manyHosts := make([]string, 0, 12) + for i := range 12 { + manyHosts = append(manyHosts, fmt.Sprintf("h%02d.example.com", i+1)) + } + tests := []struct { + name string + mode vetting.Mode + verdicts map[string]vetting.Verdict + eval *interceptorv1.InterceptorEvaluation + wantAllowed bool + wantStatus string + wantReason string + wantAnn map[string]string + wantLookups []string + }{ + { + name: "create sandbox all registered", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"api.example.com": registered(), "tools.example.com": registered()}, + eval: validateEval(methodCreate, policy("api.example.com", "Tools.Example.COM.")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.mode": "require_registered", "ans.vetting.verdicts": "api.example.com=registered,tools.example.com=registered", "ans.vetting.denied": "false"}, + wantLookups: []string{"api.example.com", "tools.example.com"}, + }, + { + name: "create sandbox one unregistered", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"api.example.com": registered(), "evil.example.com": withStatus(vetting.StatusUnregistered)}, + eval: validateEval(methodCreate, policy("evil.example.com", "api.example.com")), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: evil.example.com ans_destination_unregistered", + wantAnn: map[string]string{"ans.vetting.verdicts": "api.example.com=registered,evil.example.com=unregistered", "ans.vetting.denied": "true"}, + }, + { + name: "submit policy analysis revoked", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"a.example.com": registered(), "b.example.com": withStatus(vetting.StatusRevoked)}, + eval: validateEval(methodSubmit, toStruct(t, submitPolicyAnalysis(rule("a.example.com"), rule("b.example.com")))), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: b.example.com ans_destination_revoked", + wantLookups: []string{"a.example.com", "b.example.com"}, + }, + { + name: "edit draft chunk unknown host", mode: vetting.ModeRequire, + eval: validateEval(methodEdit, toStruct(t, editDraftChunk(rule("new.example.com")))), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: new.example.com ans_destination_unverified", + wantAnn: map[string]string{"ans.vetting.verdicts": "new.example.com=unknown"}, + }, + { + name: "untrusted tl and below threshold", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"a.example.com": withStatus(vetting.StatusUntrustedTL), "b.example.com": withStatus(vetting.StatusBelowThreshold)}, + eval: validateEval(methodCreate, policy("a.example.com", "b.example.com")), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: a.example.com ans_destination_untrusted_tl, b.example.com ans_destination_below_threshold", + }, + { + name: "observe never denies", mode: vetting.ModeObserve, + verdicts: map[string]vetting.Verdict{"a.example.com": withStatus(vetting.StatusUnregistered), "b.example.com": withStatus(vetting.StatusRevoked)}, + eval: validateEval(methodCreate, policy("a.example.com", "b.example.com")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.mode": "observe", "ans.vetting.verdicts": "a.example.com=unregistered,b.example.com=revoked", "ans.vetting.denied": "false"}, + }, + { + name: "off skips lookups", mode: vetting.ModeOff, + verdicts: map[string]vetting.Verdict{"a.example.com": withStatus(vetting.StatusUnregistered)}, + eval: validateEval(methodCreate, policy("a.example.com")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "vetting_off"}, + wantLookups: []string{}, + }, + { + name: "exempt hosts are never denied", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"api.openai.com": withStatus(vetting.StatusExempt), "s3.us-east-1.amazonaws.com": withStatus(vetting.StatusExempt)}, + eval: validateEval(methodCreate, policy("api.openai.com", "s3.us-east-1.amazonaws.com", "amazonaws.com")), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: amazonaws.com ans_destination_unverified", + wantAnn: map[string]string{"ans.vetting.verdicts": "amazonaws.com=unknown,api.openai.com=exempt,s3.us-east-1.amazonaws.com=exempt"}, + wantLookups: []string{"amazonaws.com", "api.openai.com", "s3.us-east-1.amazonaws.com"}, + }, + { + name: "exempt in observe", mode: vetting.ModeObserve, + verdicts: map[string]vetting.Verdict{"api.openai.com": withStatus(vetting.StatusExempt)}, + eval: validateEval(methodCreate, policy("api.openai.com")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.verdicts": "api.openai.com=exempt"}, + wantLookups: []string{"api.openai.com"}, + }, + { + name: "ip literals settled by the verifier", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{ + "192.0.2.5": withStatus(vetting.StatusUnregistered), "2001:db8::1": withStatus(vetting.StatusUnregistered), "[::1]": withStatus(vetting.StatusUnregistered), + }, + eval: validateEval(methodCreate, policy("192.0.2.5", "2001:db8::1", "[::1]")), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: 192.0.2.5 ans_destination_unregistered, 2001:db8::1 ans_destination_unregistered, [::1] ans_destination_unregistered", + wantLookups: []string{"192.0.2.5", "2001:db8::1", "[::1]"}, + }, + { + name: "wildcard host errored by the verifier", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"*.example.com": withStatus(vetting.StatusError)}, + eval: validateEval(methodCreate, policy("*.example.com")), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: *.example.com ans_destination_unverified", + wantAnn: map[string]string{"ans.vetting.verdicts": "*.example.com=error"}, + wantLookups: []string{"*.example.com"}, + }, + { + name: "wildcard host observed", mode: vetting.ModeObserve, + verdicts: map[string]vetting.Verdict{"**.example.com": withStatus(vetting.StatusError)}, + eval: validateEval(methodCreate, policy("**.example.com")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.verdicts": "**.example.com=error", "ans.vetting.denied": "false"}, + }, + { + name: "dedupe and normalization", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"api.example.com": registered()}, + eval: validateEval(methodCreate, policy(" API.Example.com. ", "api.example.com", "")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.verdicts": "api.example.com=registered"}, + wantLookups: []string{"api.example.com"}, + }, + { + name: "empty policy", mode: vetting.ModeRequire, + eval: validateEval(methodCreate, toStruct(t, createSandbox(nil))), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting.mode": "require_registered", "ans.vetting.verdicts": "", "ans.vetting.denied": "false"}, + wantLookups: []string{}, + }, + { + name: "unknown fields tolerated", mode: vetting.ModeRequire, + verdicts: map[string]vetting.Verdict{"api.example.com": registered()}, + eval: validateEval(methodCreate, unknownFields), + wantAllowed: true, + wantLookups: []string{"api.example.com"}, + }, + { + name: "unbound method", mode: vetting.ModeRequire, + eval: validateEval("DeleteSandbox", policy("evil.example.com")), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "unbound_method"}, + wantLookups: []string{}, + }, + { + name: "service mismatch is unbound", mode: vetting.ModeRequire, + eval: withService(validateEval(methodCreate, policy("evil.example.com")), "openshell.v1.Other"), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "unbound_method"}, + wantLookups: []string{}, + }, + { + name: "missing service is unbound", mode: vetting.ModeRequire, + eval: withService(validateEval(methodCreate, policy("evil.example.com")), ""), + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "unbound_method"}, + wantLookups: []string{}, + }, + { + name: "modify_operation phase", mode: vetting.ModeRequire, + eval: &interceptorv1.InterceptorEvaluation{Method: methodCreate, Phase: &interceptorv1.InterceptorEvaluation_ModifyOperation{ + ModifyOperation: &interceptorv1.ModifyOperationEvaluation{ProposedOperation: policy("evil.example.com")}}}, + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "skipped_phase"}, + wantLookups: []string{}, + }, + { + name: "post_commit phase", mode: vetting.ModeRequire, + eval: &interceptorv1.InterceptorEvaluation{Method: methodCreate, Phase: &interceptorv1.InterceptorEvaluation_PostCommit{ + PostCommit: &interceptorv1.PostCommitEvaluation{CommittedResponse: policy("evil.example.com")}}}, + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "skipped_phase"}, + }, + { + name: "no phase", mode: vetting.ModeRequire, + eval: &interceptorv1.InterceptorEvaluation{Method: methodCreate}, + wantAllowed: true, + wantAnn: map[string]string{"ans.vetting": "skipped_phase"}, + }, + { + name: "malformed operation", mode: vetting.ModeRequire, + eval: validateEval(methodCreate, malformed), + wantStatus: "FAILED_PRECONDITION", + wantReason: "ans destination vetting: proposed CreateSandbox operation could not be decoded", + wantAnn: map[string]string{"ans.vetting": "decode_error"}, + wantLookups: []string{}, + }, + { + name: "nil operation", mode: vetting.ModeObserve, + eval: validateEval(methodSubmit, nil), + wantStatus: "FAILED_PRECONDITION", + wantReason: "ans destination vetting: proposed SubmitPolicyAnalysis operation could not be decoded", + wantAnn: map[string]string{"ans.vetting": "decode_error"}, + }, + { + name: "reason capped at ten hosts", mode: vetting.ModeRequire, + eval: validateEval(methodCreate, policy(manyHosts...)), + wantStatus: "PERMISSION_DENIED", + wantReason: "ans destination vetting: " + strings.Join(suffixed(manyHosts[:10], " ans_destination_unverified"), ", ") + " (+2 more)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: tt.verdicts} + svc := NewService(fake, Config{Audience: testAudience, Mode: tt.mode}, discardLogger()) + res, err := svc.Evaluate(t.Context(), tt.eval) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if res.GetAllowed() != tt.wantAllowed { + t.Fatalf("allowed = %v, want %v (reason %q)", res.GetAllowed(), tt.wantAllowed, res.GetReason()) + } + if res.GetStatusCode() != tt.wantStatus { + t.Fatalf("status_code = %q, want %q", res.GetStatusCode(), tt.wantStatus) + } + if res.GetReason() != tt.wantReason { + t.Fatalf("reason = %q, want %q", res.GetReason(), tt.wantReason) + } + if len(res.GetPatches()) != 0 { + t.Fatalf("patches = %v, want none in validate", res.GetPatches()) + } + for k, want := range tt.wantAnn { + if got, ok := res.GetLogAnnotations()[k]; !ok || got != want { + t.Errorf("annotation %q = %q (present %v), want %q", k, got, ok, want) + } + } + _, skip := res.GetLogAnnotations()["ans.vetting"] + if skip && len(res.GetLogAnnotations()) != 1 { + t.Errorf("skip result carries extra annotations: %v", res.GetLogAnnotations()) + } + if _, ok := res.GetLogAnnotations()["ans.vetting.mode"]; !skip && !ok { + t.Errorf("annotations = %v, want ans.vetting.mode/verdicts/denied", res.GetLogAnnotations()) + } + if tt.wantLookups != nil { + if got := fake.hosts(); !slices.Equal(got, tt.wantLookups) { + t.Errorf("looked up %v, want %v", got, tt.wantLookups) + } + } + }) + } +} + +func suffixed(hosts []string, suffix string) []string { + out := make([]string, 0, len(hosts)) + for _, h := range hosts { + out = append(out, h+suffix) + } + return out +} + +func TestEvaluateLookupWait(t *testing.T) { + t.Parallel() + eval := func() *interceptorv1.InterceptorEvaluation { + return validateEval(methodCreate, toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"r": rule("cold.example.com")}))) + } + tests := []struct { + name string + wait time.Duration + ctx func(t *testing.T) context.Context + wantSecond bool + minWait time.Duration + maxWait time.Duration + }{ + {name: "default without deadline", ctx: func(t *testing.T) context.Context { return t.Context() }, + wantSecond: true, minWait: defaultLookupWait - 50*time.Millisecond, maxWait: defaultLookupWait}, + {name: "configured without deadline", wait: 200 * time.Millisecond, ctx: func(t *testing.T) context.Context { return t.Context() }, + wantSecond: true, minWait: 150 * time.Millisecond, maxWait: 200 * time.Millisecond}, + {name: "bounded by deadline", ctx: func(t *testing.T) context.Context { + ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond) + t.Cleanup(cancel) + return ctx + }, wantSecond: true, minWait: time.Millisecond, maxWait: 150 * time.Millisecond}, + {name: "expired deadline skips the wait", ctx: func(t *testing.T) context.Context { + ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(-time.Second)) + t.Cleanup(cancel) + return ctx + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{} + svc := NewService(fake, Config{Mode: vetting.ModeRequire, LookupWait: tt.wait}, discardLogger()) + if _, err := svc.Evaluate(tt.ctx(t), eval()); err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + calls := fake.recorded() + if len(calls) == 0 || calls[0].wait != 0 { + t.Fatalf("calls = %v, want a zero-wait pass first", calls) + } + if !tt.wantSecond { + if len(calls) != 1 { + t.Fatalf("calls = %v, want no bounded wait when the deadline has passed", calls) + } + return + } + if len(calls) != 2 { + t.Fatalf("calls = %v, want a zero-wait pass then a bounded wait", calls) + } + if w := calls[1].wait; w < tt.minWait || w > tt.maxWait { + t.Fatalf("bounded wait = %v, want within [%v, %v]", w, tt.minWait, tt.maxWait) + } + }) + } +} + +func TestEvaluateTwoPassLookups(t *testing.T) { + t.Parallel() + op := toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"r": rule("a.example.com", "b.example.com", "c.example.com")})) + verdicts := map[string]vetting.Verdict{"a.example.com": registered(), "b.example.com": registered()} + + t.Run("warm cache answers in the first pass", func(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: verdicts} + svc := NewService(fake, Config{Mode: vetting.ModeRequire, LookupWait: time.Second}, discardLogger()) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, op)) + if err != nil || res.GetAllowed() { + t.Fatalf("Evaluate() = %v, %v; want denied for c.example.com", res, err) + } + want := []lookupCall{{"a.example.com", 0}, {"b.example.com", 0}, {"c.example.com", 0}, {"c.example.com", time.Second}} + if got := fake.recorded(); !callsWithin(got, want, 50*time.Millisecond) { + t.Fatalf("calls = %v, want %v", got, want) + } + }) + t.Run("cold cache waits once per host after starting all", func(t *testing.T) { + t.Parallel() + fake := &fakeVerifier{verdicts: verdicts, coldFirst: true} + svc := NewService(fake, Config{Mode: vetting.ModeRequire, LookupWait: time.Second}, discardLogger()) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, op)) + if err != nil || res.GetAllowed() { + t.Fatalf("Evaluate() = %v, %v; want denied for c.example.com", res, err) + } + if res.GetReason() != "ans destination vetting: c.example.com ans_destination_unverified" { + t.Fatalf("reason = %q", res.GetReason()) + } + got := fake.recorded() + if len(got) != 6 { + t.Fatalf("calls = %v, want 3 zero-wait then 3 bounded", got) + } + for i, c := range got { + if (i < 3) != (c.wait == 0) { + t.Fatalf("call %d = %v, want zero-wait pass first", i, c) + } + } + }) +} + +func TestVerdictsAnnotationCap(t *testing.T) { + t.Parallel() + const n = 60 + hosts := make([]string, 0, n) + verdicts := make(map[string]vetting.Verdict, n) + for i := range n { + h := fmt.Sprintf("service-%02d.very-long-subdomain.example.com", i) + hosts = append(hosts, h) + verdicts[h] = registered() + } + svc := NewService(&fakeVerifier{verdicts: verdicts}, Config{Mode: vetting.ModeObserve}, discardLogger()) + op := toStruct(t, createSandbox(map[string]*sandboxv1.NetworkPolicyRule{"r": rule(hosts...)})) + res, err := svc.Evaluate(t.Context(), validateEval(methodCreate, op)) + if err != nil || !res.GetAllowed() { + t.Fatalf("Evaluate() = %v, %v", res, err) + } + ann := res.GetLogAnnotations()["ans.vetting.verdicts"] + if len(ann) > maxVerdictsBytes+16 { + t.Fatalf("verdicts annotation is %d bytes, want about %d", len(ann), maxVerdictsBytes) + } + entries := strings.Split(ann, ",") + var more int + if _, err := fmt.Sscanf(entries[len(entries)-1], "+%d more", &more); err != nil { + t.Fatalf("annotation does not end with the overflow marker: %q", ann) + } + if shown := len(entries) - 1; shown+more != n || shown == 0 { + t.Fatalf("shown %d + more %d != %d", shown, more, n) + } +} + +func TestVerdictsAnnotationSingleOversizeEntry(t *testing.T) { + t.Parallel() + host := strings.Repeat("a", maxVerdictsBytes) + got := verdictsAnnotation([]string{host}, []vetting.Verdict{{Status: vetting.StatusRegistered}}) + want := strings.Repeat("a", maxHostLogBytes) + "...=registered" + if got != want { + t.Fatalf("verdictsAnnotation() = %q, want %q", got, want) + } +} + +func TestNewServiceDefaults(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in time.Duration + want time.Duration + }{ + {name: "zero", in: 0, want: defaultLookupWait}, + {name: "negative", in: -time.Second, want: defaultLookupWait}, + {name: "set", in: 250 * time.Millisecond, want: 250 * time.Millisecond}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + svc := NewService(&fakeVerifier{}, Config{LookupWait: tt.in}, nil) + if svc.cfg.LookupWait != tt.want { + t.Fatalf("LookupWait = %v, want %v", svc.cfg.LookupWait, tt.want) + } + if svc.logger == nil { + t.Fatal("nil logger was not replaced") + } + }) + } +} + +func TestEvaluateLogs(t *testing.T) { + t.Parallel() + var buf bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buf, nil)) + svc := NewService(&fakeVerifier{}, Config{Mode: vetting.ModeRequire}, logger) + bad := mustStruct(t, map[string]any{"proposedRule": "nope"}) + if _, err := svc.Evaluate(t.Context(), validateEval(methodEdit, bad)); err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if !strings.Contains(buf.String(), "denied undecodable operation") || !strings.Contains(buf.String(), "method=EditDraftChunk") { + t.Fatalf("missing decode warning in log:\n%s", buf.String()) + } + buf.Reset() + op := toStruct(t, editDraftChunk(rule("x.example.com"))) + if _, err := svc.Evaluate(t.Context(), validateEval(methodEdit, op)); err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if !strings.Contains(buf.String(), "destination vetting evaluated") || !strings.Contains(buf.String(), "denied=1") { + t.Fatalf("missing evaluation log entry:\n%s", buf.String()) + } + if _, err := svc.Describe(t.Context(), &interceptorv1.DescribeRequest{Gateway: gatewayPeer(1, 2)}); err != nil { + t.Fatalf("Describe() error = %v", err) + } + if !strings.Contains(buf.String(), "gateway negotiated") || !strings.Contains(buf.String(), "protocol=1.2") { + t.Fatalf("missing negotiation log entry:\n%s", buf.String()) + } +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 0000000..bbfa0f1 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,35 @@ +// Package logging builds the process logger from command-line settings. +package logging + +import ( + "fmt" + "io" + "log/slog" + "strings" +) + +// New returns a slog.Logger writing to w. level is one of debug, info, warn or +// error; format is json or text. +func New(level, format string, w io.Writer) (*slog.Logger, error) { + var lvl slog.Level + switch strings.ToLower(strings.TrimSpace(level)) { + case "debug": + lvl = slog.LevelDebug + case "", "info": + lvl = slog.LevelInfo + case "warn", "warning": + lvl = slog.LevelWarn + case "error": + lvl = slog.LevelError + default: + return nil, fmt.Errorf("unknown log level %q (want debug, info, warn or error)", level) + } + opts := &slog.HandlerOptions{Level: lvl} + switch strings.ToLower(strings.TrimSpace(format)) { + case "", "json": + return slog.New(slog.NewJSONHandler(w, opts)), nil + case "text": + return slog.New(slog.NewTextHandler(w, opts)), nil + } + return nil, fmt.Errorf("unknown log format %q (want json or text)", format) +} diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go new file mode 100644 index 0000000..31e600a --- /dev/null +++ b/internal/logging/logging_test.go @@ -0,0 +1,53 @@ +package logging + +import ( + "bytes" + "context" + "strings" + "testing" +) + +func TestNew(t *testing.T) { + t.Parallel() + tests := []struct { + name string + level string + format string + wantErr bool + debugShown bool + jsonOut bool + }{ + {name: "defaults", jsonOut: true}, + {name: "debug text", level: "debug", format: "text", debugShown: true}, + {name: "warn json", level: "WARN", format: "JSON", jsonOut: true}, + {name: "error", level: "error", format: "text"}, + {name: "warning alias", level: "warning", format: "text"}, + {name: "bad level", level: "loud", wantErr: true}, + {name: "bad format", format: "xml", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var buf bytes.Buffer + logger, err := New(tt.level, tt.format, &buf) + if (err != nil) != tt.wantErr { + t.Fatalf("New() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + logger.DebugContext(context.Background(), "dbg") + logger.ErrorContext(context.Background(), "boom", "k", "v") + out := buf.String() + if strings.Contains(out, "dbg") != tt.debugShown { + t.Fatalf("debug shown = %v, want %v; output %q", !tt.debugShown, tt.debugShown, out) + } + if !strings.Contains(out, "boom") { + t.Fatalf("error line missing from %q", out) + } + if tt.jsonOut != strings.HasPrefix(out, "{") { + t.Fatalf("json = %v, want %v; output %q", strings.HasPrefix(out, "{"), tt.jsonOut, out) + } + }) + } +} diff --git a/internal/middleware/authorization.go b/internal/middleware/authorization.go new file mode 100644 index 0000000..704dbf5 --- /dev/null +++ b/internal/middleware/authorization.go @@ -0,0 +1,40 @@ +package middleware + +import ( + "errors" + "fmt" + "maps" + "strings" +) + +// IdentityAuthorizer grants a sandbox access to a signing identity. +type IdentityAuthorizer interface { + Allows(sandboxID, identityName string) bool +} + +// SandboxIdentities holds operator-approved, exact sandbox identity bindings. +type SandboxIdentities struct { + bindings map[string]string +} + +// NewSandboxIdentities validates and copies bindings. An empty map denies all signing. +func NewSandboxIdentities(bindings map[string]string, identities IdentityLookup) (*SandboxIdentities, error) { + if identities == nil { + return nil, errors.New("sandbox_identities: identity lookup is required") + } + for sandboxID, name := range bindings { + if sandboxID == "" || strings.TrimSpace(sandboxID) != sandboxID || sandboxID == "*" { + return nil, fmt.Errorf("sandbox_identities: invalid sandbox id %q", sandboxID) + } + if _, ok := identities.Identity(name); !ok { + return nil, fmt.Errorf("sandbox_identities: sandbox %q names unknown identity %q", sandboxID, name) + } + } + return &SandboxIdentities{bindings: maps.Clone(bindings)}, nil +} + +// Allows implements IdentityAuthorizer without wildcard or default grants. +func (s *SandboxIdentities) Allows(sandboxID, identityName string) bool { + name, ok := s.bindings[sandboxID] + return ok && name == identityName +} diff --git a/internal/middleware/authorization_test.go b/internal/middleware/authorization_test.go new file mode 100644 index 0000000..ac26a6c --- /dev/null +++ b/internal/middleware/authorization_test.go @@ -0,0 +1,129 @@ +package middleware + +import ( + "strings" + "testing" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/extauth" +) + +func TestNewSandboxIdentities(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + tests := []struct { + name string + bindings map[string]string + ids IdentityLookup + wantErr string + }{ + {name: "approved binding", bindings: map[string]string{testSandbox: testIdentity}, ids: ids}, + {name: "empty denies all", ids: ids}, + {name: "missing lookup", wantErr: "lookup is required"}, + {name: "empty sandbox", bindings: map[string]string{"": testIdentity}, ids: ids, wantErr: "invalid sandbox"}, + {name: "whitespace sandbox", bindings: map[string]string{" sb-1": testIdentity}, ids: ids, wantErr: "invalid sandbox"}, + {name: "wildcard sandbox", bindings: map[string]string{"*": testIdentity}, ids: ids, wantErr: "invalid sandbox"}, + {name: "unknown identity", bindings: map[string]string{testSandbox: "other"}, ids: ids, wantErr: "unknown identity"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + authz, err := NewSandboxIdentities(tt.bindings, tt.ids) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewSandboxIdentities() error = %v, want %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + if got := authz.Allows(testSandbox, testIdentity); got != (len(tt.bindings) != 0) { + t.Fatalf("Allows() = %v", got) + } + if authz.Allows("unapproved", testIdentity) || authz.Allows(testSandbox, "other") || authz.Allows("", "") { + t.Fatal("unapproved binding was allowed") + } + }) + } +} + +func TestSandboxIdentitiesCopiesApproval(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + bindings := map[string]string{testSandbox: testIdentity} + authz, err := NewSandboxIdentities(bindings, ids) + if err != nil { + t.Fatal(err) + } + bindings[testSandbox] = "other" + bindings["unapproved"] = testIdentity + if !authz.Allows(testSandbox, testIdentity) || authz.Allows("unapproved", testIdentity) { + t.Fatal("mutating the input map changed approved bindings") + } +} + +func TestNewServiceRequiresAuthorizer(t *testing.T) { + t.Parallel() + defer func() { + if recover() == nil { + t.Fatal("NewService(nil authorizer) did not panic") + } + }() + NewService(lookup{}, nil, nil, Config{}, nil) +} + +func TestEvaluateIdentityAuthorization(t *testing.T) { + t.Parallel() + const otherIdentity = "other-agent" + tests := []struct { + name string + sandbox string + identity string + bindings map[string]string + wantCode string + }{ + {name: "approved", sandbox: testSandbox, identity: testIdentity, + bindings: map[string]string{testSandbox: testIdentity}}, + {name: "known but unapproved identity", sandbox: testSandbox, identity: otherIdentity, + bindings: map[string]string{testSandbox: testIdentity}, wantCode: ReasonIdentityUnauthorized}, + {name: "unapproved sandbox", sandbox: "unapproved", identity: testIdentity, + bindings: map[string]string{testSandbox: testIdentity}, wantCode: ReasonIdentityUnauthorized}, + {name: "no approvals", sandbox: testSandbox, identity: testIdentity, wantCode: ReasonIdentityUnauthorized}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + signer := &fakeSigner{} + ids := lookup{ + testIdentity: newIdentity(t, testIdentity, signer, fullArtifacts()), + otherIdentity: newIdentity(t, otherIdentity, signer, fullArtifacts()), + } + authz, err := NewSandboxIdentities(tt.bindings, ids) + if err != nil { + t.Fatal(err) + } + svc := NewService(ids, authz, nil, Config{RequireAuthenticatedCaller: true}, nil) + req := evaluation(t, map[string]any{"identity": tt.identity}, nil) + req.Context.SandboxId = tt.sandbox + req.Context.Sandbox = testSandbox + req.Context.Workspace = testSandbox + validated, err := svc.ValidateConfig(t.Context(), &middlewarev1.ValidateConfigRequest{Config: req.GetConfig()}) + if err != nil || !validated.GetValid() { + t.Fatalf("ValidateConfig() = %v, %v", validated, err) + } + ctx := extauth.ContextWithCaller(t.Context(), extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: tt.sandbox}) + res, err := svc.EvaluateHttpRequest(ctx, req) + if err != nil || res.GetReasonCode() != tt.wantCode { + t.Fatalf("EvaluateHttpRequest() = %v, %v", res, err) + } + if tt.wantCode != "" { + if res.GetDecision() != deny || len(signer.calls) != 0 || len(res.GetHeaderMutations()) != 0 { + t.Fatalf("unapproved identity signed: %v", res) + } + } else if res.GetDecision() != allow || len(signer.calls) != 1 { + t.Fatalf("approved identity did not sign: %v", res) + } + }) + } +} diff --git a/internal/middleware/credentials.go b/internal/middleware/credentials.go new file mode 100644 index 0000000..dde0471 --- /dev/null +++ b/internal/middleware/credentials.go @@ -0,0 +1,53 @@ +package middleware + +const ( + bitsPerNibble = 4 + hexLetterOffset = 10 +) + +func containsCredentialMarker(value []byte) bool { + for i, b := range value { + if b != 'o' && b != 'O' && b != '%' { + continue + } + if encodedPrefix(value[i:], "openshell:resolve:env:") || encodedPrefix(value[i:], "OPENSHELL-RESOLVE-ENV-") { + return true + } + } + return false +} + +func encodedPrefix(value []byte, prefix string) bool { + for i := range len(prefix) { + if len(value) == 0 { + return false + } + b := value[0] + value = value[1:] + if b == '%' && len(value) >= 2 { + hi, hiOK := hexDigit(value[0]) + lo, loOK := hexDigit(value[1]) + if hiOK && loOK { + b = hi<= '0' && b <= '9': + return b - '0', true + case b >= 'a' && b <= 'f': + return b - 'a' + hexLetterOffset, true + case b >= 'A' && b <= 'F': + return b - 'A' + hexLetterOffset, true + default: + return 0, false + } +} diff --git a/internal/middleware/credentials_test.go b/internal/middleware/credentials_test.go new file mode 100644 index 0000000..57589a6 --- /dev/null +++ b/internal/middleware/credentials_test.go @@ -0,0 +1,98 @@ +package middleware + +import ( + "fmt" + "testing" +) + +func TestContainsCredentialMarker(t *testing.T) { + t.Parallel() + tests := []struct { + name string + value string + want bool + }{ + {name: "empty"}, + {name: "ordinary body", value: `{"task":"pay invoice"}`}, + {name: "raw canonical", value: `{"key":"openshell:resolve:env:KEY"}`, want: true}, + {name: "raw provider", value: "token=OPENSHELL-RESOLVE-ENV-KEY", want: true}, + {name: "encoded canonical", value: "token=openshell%3Aresolve%3aenv%3AKEY", want: true}, + {name: "encoded provider", value: "%4fPENSHELL%2dRESOLVE%2DENV-KEY", want: true}, + {name: "malformed prefix before marker", value: "%zz&token=%6fpenshell%3aresolve%3aenv%3aKEY", want: true}, + {name: "partial marker", value: "openshell:resolve:env"}, + {name: "different case", value: "OpenShell:resolve:env:KEY"}, + {name: "invalid percent", value: "%xz%3Zopenshel"}, + {name: "trailing percent", value: "o%"}, + {name: "nested encoding stays encoded", value: "%256fpenshell%253aresolve%253aenv%253aKEY"}, + {name: "binary payload", value: "\xff\x00openshell:resolve:env:KEY", want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := containsCredentialMarker([]byte(tt.value)); got != tt.want { + t.Fatalf("containsCredentialMarker() = %v, want %v", got, tt.want) + } + }) + } + for _, marker := range []string{"openshell:resolve:env:", "OPENSHELL-RESOLVE-ENV-"} { + for i := range len(marker) { + value := marker[:i] + fmt.Sprintf("%%%02x", marker[i]) + marker[i+1:] + if !containsCredentialMarker([]byte(value)) { + t.Errorf("encoded marker byte %d was missed", i) + } + } + } +} + +func TestEvaluateCredentialPlaceholders(t *testing.T) { + t.Parallel() + tests := []struct { + name string + body string + path string + query string + strip bool + want string + }{ + {name: "ordinary request", body: `{"amount":42}`}, + {name: "raw body placeholder", body: `{"token":"openshell:resolve:env:KEY"}`, want: ReasonRequestRewriteUnsupported}, + {name: "form encoded placeholder", body: "token=openshell%3aresolve%3aenv%3aKEY", want: ReasonRequestRewriteUnsupported}, + {name: "provider placeholder", body: "OPENSHELL-RESOLVE-ENV-KEY", want: ReasonRequestRewriteUnsupported}, + {name: "path placeholder", path: "/bot/openshell:resolve:env:KEY", want: ReasonRequestRewriteUnsupported}, + {name: "encoded path placeholder", path: "/bot/%6fpenshell%3aresolve%3aenv%3aKEY", want: ReasonRequestRewriteUnsupported}, + {name: "query is not proof bound", query: "token=openshell:resolve:env:KEY"}, + {name: "strip does not sign", strip: true, body: "openshell:resolve:env:KEY"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + signer := &fakeSigner{} + ids := lookup{testIdentity: newIdentity(t, testIdentity, signer, fullArtifacts())} + svc := NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{}, nil) + policy := signPolicy() + if tt.strip { + policy = map[string]any{"mode": "strip"} + } + req := evaluation(t, policy, []byte(tt.body)) + if tt.path != "" { + req.Target.Path = tt.path + } + req.Target.Query = tt.query + res, err := svc.EvaluateHttpRequest(t.Context(), req) + if err != nil || res.GetReasonCode() != tt.want { + t.Fatalf("EvaluateHttpRequest() = %v, %v", res, err) + } + wantCalls := 1 + if tt.strip { + wantCalls = 0 + } + if tt.want != "" { + if res.GetDecision() != deny || len(signer.calls) != 0 || len(res.GetHeaderMutations()) != 0 { + t.Fatalf("unsupported request was signed: %v", res) + } + } else if res.GetDecision() != allow || len(signer.calls) != wantCalls { + t.Fatalf("supported request failed: %v", res) + } + }) + } +} diff --git a/internal/middleware/e2e_test.go b/internal/middleware/e2e_test.go new file mode 100644 index 0000000..56c6b9a --- /dev/null +++ b/internal/middleware/e2e_test.go @@ -0,0 +1,293 @@ +package middleware + +import ( + "bytes" + "io" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/identity" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// callee is an HTTP service authenticating callers with the SDK's +// pop.Middleware, the way an ANS-6 Method B callee does. +type callee struct { + srv *httptest.Server + host string + port uint32 +} + +func newCallee(t *testing.T, tl *testfixtures.TL) *callee { + t.Helper() + replay := pop.NewMemoryReplayCache(t.Context(), 0) + t.Cleanup(replay.Close) + echo := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id, ok := pop.CallerFromContext(r.Context()) + if !ok { + http.Error(w, "no caller", http.StatusInternalServerError) + return + } + _, _ = io.WriteString(w, id.AnsName) + }) + srv := httptest.NewUnstartedServer(nil) + authority := srv.Listener.Addr().String() + srv.Config.Handler = pop.Middleware(tl.KeyStore(), replay, + pop.WithTrustedHosts(authority), pop.WithMiddlewareLogger(slog.New(slog.DiscardHandler)))(echo) + srv.Start() + t.Cleanup(srv.Close) + addr, ok := srv.Listener.Addr().(*net.TCPAddr) + if !ok { + t.Fatalf("listener address %T is not TCP", srv.Listener.Addr()) + } + return &callee{srv: srv, host: addr.IP.String(), port: uint32(addr.Port)} +} + +// evaluationFor describes a request to the callee the way the supervisor would. +func (c *callee) evaluationFor(t *testing.T, policy map[string]any, method, path string, body []byte) *middlewarev1.HttpRequestEvaluation { + t.Helper() + return &middlewarev1.HttpRequestEvaluation{ + Phase: preCredentials, + Context: &middlewarev1.RequestContext{RequestId: "req-e2e", SandboxId: testSandbox}, + Config: structOf(t, policy), + Target: &middlewarev1.HttpRequestTarget{Scheme: "http", Host: c.host, Port: c.port, Method: method, Path: path}, + Body: body, + } +} + +// send applies the middleware's header mutations to a real request with the +// same method, path and body and returns the callee's response. +func (c *callee) send(t *testing.T, res *middlewarev1.HttpRequestResult, method, path string, body []byte) (int, string) { + t.Helper() + req, err := http.NewRequestWithContext(t.Context(), method, c.srv.URL+path, bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Dpop", "smuggled-by-workload") + for _, m := range res.GetHeaderMutations() { + switch op := m.GetOperation().(type) { + case *middlewarev1.HeaderMutation_Write: + if op.Write.GetOnExisting() != middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_OVERWRITE { + t.Fatalf("mutation %v is not OVERWRITE", m) + } + req.Header.Set(op.Write.GetName(), op.Write.GetValue()) + case *middlewarev1.HeaderMutation_Remove: + req.Header.Del(op.Remove.GetName()) + default: + t.Fatalf("unexpected mutation %v", m) + } + } + resp, err := c.srv.Client().Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return resp.StatusCode, string(b) +} + +func newRegistry(t *testing.T, tl *testfixtures.TL, fixture *testfixtures.Identity) *identity.Registry { + t.Helper() + signer, _, err := identity.NewPEMSigner(fixture.KeyPEM, fixture.CertPEM) + if err != nil { + t.Fatal(err) + } + receipt, token := testfixtures.StaticArtifacts(tl, fixture) + id, err := identity.NewIdentity(testIdentity, fixture.AnsName, fixture.AgentID, signer, + identity.NewStaticSource(identity.Artifacts{ReceiptBase64: receipt, StatusTokenBase64: token})) + if err != nil { + t.Fatal(err) + } + reg, err := identity.NewRegistryFromIdentities(id) + if err != nil { + t.Fatal(err) + } + t.Cleanup(reg.Close) + return reg +} + +// e2e wires a fixture transparency log and identity into a Registry, the +// Service and a verifying callee. +type e2e struct { + callee *callee + registry *identity.Registry + service *Service +} + +func newE2E(t *testing.T) *e2e { + t.Helper() + tl := testfixtures.MustNewTL() + fixture := testfixtures.MustNewIdentity(testAgentID, testAnsName) + reg := newRegistry(t, tl, fixture) + return &e2e{callee: newCallee(t, tl), registry: reg, service: NewService(reg, sandboxAuthorizer(testSandbox), nil, Config{}, nil)} +} + +const e2ePath = "/v1/do" + +func TestEndToEndSigning(t *testing.T) { + t.Parallel() + env := newE2E(t) + c, s := env.callee, env.service + const path = e2ePath + body := []byte(`{"amount":42}`) + + t.Run("signed request authenticates", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, signPolicy(), http.MethodPost, path, body)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("evaluate = %v, %v", res, err) + } + code, got := c.send(t, res, http.MethodPost, path, body) + if code != http.StatusOK || got != testAnsName { + t.Fatalf("callee = %d %q, want 200 %q", code, got, testAnsName) + } + if code, _ := c.send(t, res, http.MethodPost, path, body); code != http.StatusUnauthorized { + t.Fatalf("replayed proof = %d, want 401", code) + } + }) + + t.Run("bodyless GET authenticates", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, signPolicy(), http.MethodGet, "/v1/items", nil)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("evaluate = %v, %v", res, err) + } + if code, got := c.send(t, res, http.MethodGet, "/v1/items", nil); code != http.StatusOK || got != testAnsName { + t.Fatalf("callee = %d %q", code, got) + } + }) + + t.Run("tampered body is rejected", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, signPolicy(), http.MethodPost, path, body)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("evaluate = %v, %v", res, err) + } + if code, _ := c.send(t, res, http.MethodPost, path, []byte(`{"amount":4200}`)); code != http.StatusUnauthorized { + t.Fatalf("tampered body = %d, want 401", code) + } + }) + + t.Run("redirected path is rejected", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, signPolicy(), http.MethodPost, path, body)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("evaluate = %v, %v", res, err) + } + if code, _ := c.send(t, res, http.MethodPost, "/v1/other", body); code != http.StatusUnauthorized { + t.Fatalf("other path = %d, want 401", code) + } + }) +} + +func TestEndToEndPolicy(t *testing.T) { + t.Parallel() + env := newE2E(t) + c, s, reg := env.callee, env.service, env.registry + const path = e2ePath + body := []byte(`{"amount":42}`) + + t.Run("strip mode leaves the request unauthenticated", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, map[string]any{"mode": "strip"}, http.MethodPost, path, body)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("evaluate = %v, %v", res, err) + } + if code, _ := c.send(t, res, http.MethodPost, path, body); code != http.StatusUnauthorized { + t.Fatalf("stripped request = %d, want 401", code) + } + }) + + t.Run("unknown identity fails policy validation", func(t *testing.T) { + t.Parallel() + v, err := s.ValidateConfig(t.Context(), &middlewarev1.ValidateConfigRequest{Config: structOf(t, map[string]any{"identity": "ghost"})}) + if err != nil || v.GetValid() { + t.Fatalf("ValidateConfig = %v, %v", v, err) + } + }) + + t.Run("supervisor for another sandbox is denied", func(t *testing.T) { + t.Parallel() + ctx := extauth.ContextWithCaller(t.Context(), extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: "sb-other"}) + res, err := s.EvaluateHttpRequest(ctx, c.evaluationFor(t, signPolicy(), http.MethodPost, path, body)) + if err != nil || res.GetDecision() != deny || res.GetReasonCode() != ReasonCallerSandboxMismatch { + t.Fatalf("evaluate = %v, %v", res, err) + } + }) + + t.Run("destination vetting", func(t *testing.T) { + t.Parallel() + vet := &fakeVerifier{verdict: vetting.Verdict{Status: vetting.StatusUnregistered}} + vetted := NewService(reg, sandboxAuthorizer(testSandbox), vet, Config{}, nil) + require := map[string]any{"identity": testIdentity, "destination_policy": "require_registered"} + res, err := vetted.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, require, http.MethodPost, path, body)) + if err != nil || res.GetDecision() != deny || res.GetReasonCode() != vetting.ReasonUnregistered { + t.Fatalf("require_registered = %v, %v", res, err) + } + observe := map[string]any{"identity": testIdentity, "destination_policy": "observe"} + res, err = vetted.EvaluateHttpRequest(t.Context(), c.evaluationFor(t, observe, http.MethodPost, path, body)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("observe = %v, %v", res, err) + } + types := make([]string, 0, len(res.GetFindings())) + for _, f := range res.GetFindings() { + types = append(types, f.GetType()) + } + if len(types) != 2 || types[0] != FindingDestinationPrefix+"unregistered" || types[1] != FindingIdentityAttached { + t.Fatalf("findings = %v", types) + } + if code, got := c.send(t, res, http.MethodPost, path, body); code != http.StatusOK || got != testAnsName { + t.Fatalf("callee under observe = %d %q", code, got) + } + }) +} + +// TestEndToEndExpiredTokenDenies runs the identity through the SDK's +// HeaderSupplier: once the status token has expired the supplier withholds it +// and the middleware fails closed. +func TestEndToEndExpiredTokenDenies(t *testing.T) { + t.Parallel() + tl := testfixtures.MustNewTL() + fixture := testfixtures.MustNewIdentity(testAgentID, testAnsName) + now := time.Now().Unix() + expired := tl.StatusToken(fixture.AgentID, fixture.AnsName, scitt.StatusActive, now-3600, now-10, fixture.Fingerprint) + client := scitt.NewMockClient(). + WithReceipt(fixture.AgentID, tl.Receipt(fixture.AgentID, fixture.AnsName)). + WithStatusToken(fixture.AgentID, expired) + supplier := scitt.NewHeaderSupplier(fixture.AgentID, client, scitt.NewRefreshableKeyStore(tl.KeyStore(), nil), + scitt.WithSupplierClockSkew(time.Minute)) + src, err := identity.NewSupplierSource(t.Context(), supplier) + if err != nil { + t.Fatal(err) + } + defer src.Close() + signer, _, err := identity.NewPEMSigner(fixture.KeyPEM, fixture.CertPEM) + if err != nil { + t.Fatal(err) + } + id, err := identity.NewIdentity(testIdentity, fixture.AnsName, fixture.AgentID, signer, src) + if err != nil { + t.Fatal(err) + } + reg, err := identity.NewRegistryFromIdentities(id) + if err != nil { + t.Fatal(err) + } + defer reg.Close() + c := newCallee(t, tl) + res, err := NewService(reg, sandboxAuthorizer(testSandbox), nil, Config{}, nil).EvaluateHttpRequest(t.Context(), + c.evaluationFor(t, signPolicy(), http.MethodPost, "/v1/do", nil)) + if err != nil || res.GetDecision() != deny || res.GetReasonCode() != ReasonStatusTokenUnavailable { + t.Fatalf("evaluate = %v, %v, want DENY %s", res, err, ReasonStatusTokenUnavailable) + } +} diff --git a/internal/middleware/grpc_test.go b/internal/middleware/grpc_test.go new file mode 100644 index 0000000..ff50ee9 --- /dev/null +++ b/internal/middleware/grpc_test.go @@ -0,0 +1,88 @@ +package middleware + +import ( + "context" + "errors" + "io" + "net" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" +) + +const bufSize = 1 << 20 + +// dialService serves s over an in-memory connection and returns a client. +func dialService(t *testing.T, s *Service) middlewarev1.SupervisorMiddlewareClient { + t.Helper() + lis := bufconn.Listen(bufSize) + server := grpc.NewServer() + middlewarev1.RegisterSupervisorMiddlewareServer(server, s) + go func() { _ = server.Serve(lis) }() + t.Cleanup(server.Stop) + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) })) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + return middlewarev1.NewSupervisorMiddlewareClient(conn) +} + +func TestGRPCContract(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + client := dialService(t, NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{Audience: "urn:openshell:extension:middleware:ans"}, nil)) + ctx := t.Context() + + m, err := client.Describe(ctx, &middlewarev1.MiddlewareDescribeRequest{ + Gateway: &extensionv1.PeerMetadata{ProtocolVersion: &extensionv1.ProtocolVersion{Major: 1}}}) + if err != nil || m.GetName() != ManifestName || len(m.GetBindings()) != 1 { + t.Fatalf("Describe() = %v, %v", m, err) + } + _, err = client.Describe(ctx, &middlewarev1.MiddlewareDescribeRequest{ + Gateway: &extensionv1.PeerMetadata{ProtocolVersion: &extensionv1.ProtocolVersion{Major: 2}}}) + if status.Code(err) != codes.FailedPrecondition { + t.Fatalf("Describe(v2) error = %v, want FailedPrecondition", err) + } + + v, err := client.ValidateConfig(ctx, &middlewarev1.ValidateConfigRequest{Config: structOf(t, signPolicy())}) + if err != nil || !v.GetValid() { + t.Fatalf("ValidateConfig() = %v, %v", v, err) + } + v, err = client.ValidateConfig(ctx, &middlewarev1.ValidateConfigRequest{Config: structOf(t, map[string]any{"identity": "ghost"})}) + if err != nil || v.GetValid() || v.GetReason() == "" { + t.Fatalf("ValidateConfig(ghost) = %v, %v", v, err) + } + if _, nilErr := client.ValidateConfig(ctx, &middlewarev1.ValidateConfigRequest{}); status.Code(nilErr) != codes.InvalidArgument { + t.Fatalf("ValidateConfig(nil) error = %v, want InvalidArgument", nilErr) + } + + res, err := client.EvaluateHttpRequest(ctx, evaluation(t, signPolicy(), []byte("body"))) + if err != nil || res.GetDecision() != allow || len(res.GetHeaderMutations()) != 3 { + t.Fatalf("EvaluateHttpRequest() = %v, %v", res, err) + } + res, err = client.EvaluateHttpRequest(ctx, evaluation(t, map[string]any{"identity": "ghost"}, nil)) + if err != nil || res.GetDecision() != deny || res.GetReasonCode() != ReasonIdentityUnknown { + t.Fatalf("EvaluateHttpRequest(ghost) = %v, %v", res, err) + } + + stream, err := client.EvaluateWebSocketSession(ctx) + if err != nil { + t.Fatalf("EvaluateWebSocketSession open: %v", err) + } + if err := stream.Send(&middlewarev1.WebSocketSessionEvent{}); err != nil && !errors.Is(err, io.EOF) { + t.Fatalf("Send: %v", err) + } + if _, err := stream.Recv(); status.Code(err) != codes.Unimplemented { + t.Fatalf("EvaluateWebSocketSession Recv error = %v, want Unimplemented", err) + } +} diff --git a/internal/middleware/policy.go b/internal/middleware/policy.go new file mode 100644 index 0000000..5218eef --- /dev/null +++ b/internal/middleware/policy.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "errors" + "fmt" + "maps" + "slices" + "strings" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// Signing modes a policy selects with its "mode" key. +const ( + // ModeSign attaches the identity's proof and artifacts; the default. + ModeSign = "sign" + // ModeStrip removes any DPoP and SCITT headers and attaches nothing. + ModeStrip = "strip" +) + +// Keys of the policy config block. +const ( + keyMode = "mode" + keyIdentity = "identity" + keyDestinationPolicy = "destination_policy" +) + +// PolicyConfig is a sandbox policy's network_middlewares[].config block for +// this middleware, parsed. +type PolicyConfig struct { + Mode string + Identity string + DestinationPolicy vetting.Mode +} + +// ParsePolicy decodes a policy config, rejecting unknown keys, non-string +// values and unknown mode or destination_policy values. Whether the named +// identity exists and whether destination vetting is configured is the +// Service's check. +func ParsePolicy(cfg *structpb.Struct) (PolicyConfig, error) { + if cfg == nil { + return PolicyConfig{}, errors.New("config is required") + } + p := PolicyConfig{Mode: ModeSign} + fields := cfg.GetFields() + for _, key := range slices.Sorted(maps.Keys(fields)) { + str, isString := fields[key].GetKind().(*structpb.Value_StringValue) + var err error + switch { + case key != keyMode && key != keyIdentity && key != keyDestinationPolicy: + err = fmt.Errorf("unknown field %q", key) + case !isString: + err = fmt.Errorf("%s must be a string", key) + case key == keyMode: + p.Mode, err = parseMode(str.StringValue) + case key == keyIdentity: + p.Identity = strings.TrimSpace(str.StringValue) + default: + p.DestinationPolicy, err = vetting.ParseMode(str.StringValue) + } + if err != nil { + return PolicyConfig{}, err + } + } + return p, nil +} + +func parseMode(s string) (string, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "", ModeSign: + return ModeSign, nil + case ModeStrip: + return ModeStrip, nil + } + return "", fmt.Errorf("unknown mode %q (want sign or strip)", s) +} diff --git a/internal/middleware/policy_test.go b/internal/middleware/policy_test.go new file mode 100644 index 0000000..82bd083 --- /dev/null +++ b/internal/middleware/policy_test.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "strings" + "testing" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +func structOf(t *testing.T, m map[string]any) *structpb.Struct { + t.Helper() + s, err := structpb.NewStruct(m) + if err != nil { + t.Fatalf("NewStruct(%v): %v", m, err) + } + return s +} + +func TestParsePolicy(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in map[string]any + nilIn bool + want PolicyConfig + wantErr string + }{ + {name: "nil struct", nilIn: true, wantErr: "config is required"}, + {name: "empty defaults to sign", in: map[string]any{}, want: PolicyConfig{Mode: ModeSign}}, + {name: "sign with identity", in: map[string]any{"mode": "sign", "identity": " payments-agent "}, + want: PolicyConfig{Mode: ModeSign, Identity: "payments-agent"}}, + {name: "identity only", in: map[string]any{"identity": "payments-agent"}, + want: PolicyConfig{Mode: ModeSign, Identity: "payments-agent"}}, + {name: "strip case-insensitive", in: map[string]any{"mode": " STRIP "}, want: PolicyConfig{Mode: ModeStrip}}, + {name: "destination observe", in: map[string]any{"identity": "a", "destination_policy": "observe"}, + want: PolicyConfig{Mode: ModeSign, Identity: "a", DestinationPolicy: vetting.ModeObserve}}, + {name: "destination require", in: map[string]any{"identity": "a", "destination_policy": "require_registered"}, + want: PolicyConfig{Mode: ModeSign, Identity: "a", DestinationPolicy: vetting.ModeRequire}}, + {name: "destination off", in: map[string]any{"identity": "a", "destination_policy": "off"}, + want: PolicyConfig{Mode: ModeSign, Identity: "a"}}, + {name: "unknown key", in: map[string]any{"identity": "a", "identtity": "b"}, wantErr: `unknown field "identtity"`}, + {name: "non-string mode", in: map[string]any{"mode": 1}, wantErr: "mode must be a string"}, + {name: "null identity", in: map[string]any{"identity": nil}, wantErr: "identity must be a string"}, + {name: "unknown mode", in: map[string]any{"mode": "observe"}, wantErr: `unknown mode "observe"`}, + {name: "unknown destination policy", in: map[string]any{"destination_policy": "deny"}, wantErr: "unknown destination_policy"}, + {name: "errors reported in key order", in: map[string]any{"zzz": true, "mode": 5}, wantErr: "mode must be a string"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var in *structpb.Struct + if !tt.nilIn { + in = structOf(t, tt.in) + } + got, err := ParsePolicy(in) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("ParsePolicy() error = %v, want containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("ParsePolicy() error = %v", err) + } + if got != tt.want { + t.Fatalf("ParsePolicy() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/internal/middleware/reasons.go b/internal/middleware/reasons.go new file mode 100644 index 0000000..6abc6d4 --- /dev/null +++ b/internal/middleware/reasons.go @@ -0,0 +1,66 @@ +// Package middleware implements the OpenShell supervisor middleware that gives +// a sandboxed agent its ANS identity: at PRE_CREDENTIALS it signs each allowed +// outbound HTTP request with the policy-selected identity's DPoP proof and +// attaches the identity's SCITT receipt and status token (ANS-6 Method B), or +// strips those headers, and optionally vets the destination host. +package middleware + +// Reason codes returned with a DENY. They follow the HttpRequestResult +// reason_code grammar (lowercase ASCII, digits and underscores, at most 64 +// bytes) and OpenShell may relay them to the sandbox. Destination codes are +// shared with the interceptor and live in package vetting. +const ( + // ReasonUnsupportedPhase is returned when the evaluation phase is not + // PRE_CREDENTIALS. + ReasonUnsupportedPhase = "ans_unsupported_phase" + // ReasonCallerUnauthenticated is returned when the service requires an + // authenticated caller and the call carried none. + ReasonCallerUnauthenticated = "ans_caller_unauthenticated" + // ReasonCallerNotSupervisor is returned when the authenticated caller is + // not a sandbox supervisor; only supervisors evaluate requests. + ReasonCallerNotSupervisor = "ans_caller_not_supervisor" + // ReasonCallerSandboxMismatch is returned when the supervisor credential + // names a different sandbox than the request context. + ReasonCallerSandboxMismatch = "ans_caller_sandbox_mismatch" + // ReasonPolicyInvalid is returned when the policy config would not pass + // ValidateConfig; OpenShell validates at policy creation so this guards + // against a config that bypassed it. + ReasonPolicyInvalid = "ans_policy_invalid" + // ReasonTargetInvalid is returned when the request target has no usable + // method, scheme or host. + ReasonTargetInvalid = "ans_target_invalid" + // ReasonIdentityUnknown is returned when the policy names an identity the + // service does not hold. + ReasonIdentityUnknown = "ans_identity_unknown" + // ReasonIdentityUnauthorized denies an identity absent from the sandbox binding. + ReasonIdentityUnauthorized = "ans_identity_unauthorized" + // ReasonRequestRewriteUnsupported denies content OpenShell could rewrite after signing. + ReasonRequestRewriteUnsupported = "ans_credential_rewrite_unsupported" + // ReasonStatusTokenUnavailable is returned when the identity has no + // unexpired status token (ANS-6 section 9.4, fail closed). + ReasonStatusTokenUnavailable = "ans_status_token_unavailable" + // ReasonReceiptUnavailable is returned when the identity has no receipt. + ReasonReceiptUnavailable = "ans_receipt_unavailable" + // ReasonSignFailed is returned when the identity's signer failed. + ReasonSignFailed = "ans_sign_failed" +) + +// Finding types and metadata keys attached to results. +const ( + // FindingIdentityAttached reports that the proof and artifacts were + // written. + FindingIdentityAttached = "ans_identity_attached" + // FindingIdentityStripped reports that the identity headers were removed. + FindingIdentityStripped = "ans_identity_stripped" + // FindingDestinationPrefix precedes the vetting status of the destination + // host: ans_destination_registered, ans_destination_unregistered and so on. + FindingDestinationPrefix = "ans_destination_" + // MetadataIdentity carries the policy's identity name. + MetadataIdentity = "ans.identity" + // MetadataAnsName carries the identity's ans:// name. + MetadataAnsName = "ans.name" + // MetadataAgentID carries the identity's agent id. + MetadataAgentID = "ans.agent_id" + // MetadataDestinationStatus carries the destination's vetting status. + MetadataDestinationStatus = "ans.destination_status" +) diff --git a/internal/middleware/service.go b/internal/middleware/service.go new file mode 100644 index 0000000..53c0a6e --- /dev/null +++ b/internal/middleware/service.go @@ -0,0 +1,455 @@ +package middleware + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/identity" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +// Manifest identity and the protocol contract this service negotiates. +const ( + // ManifestName is the diagnostic service name in the manifest. + ManifestName = "ans-openshell-middleware" + // ImplementationName identifies this implementation to the gateway. + ImplementationName = "agentnameservice/ans-openshell" + // CapabilityContract is the supervisor-middleware contract capability. + CapabilityContract = "openshell.supervisor-middleware.contract" + // ProtocolMajor and ProtocolMinor form the supported protocol version. + ProtocolMajor uint32 = 1 + ProtocolMinor uint32 = 0 +) + +// Defaults applied by NewService to zero Config fields. +const ( + // DefaultMaxPayloadBytes is OpenShell's 4 MiB platform cap on request + // bodies. + DefaultMaxPayloadBytes uint64 = 4 << 20 + // DefaultVettingBudget bounds the wait for an in-flight destination lookup + // under require_registered. + DefaultVettingBudget = 150 * time.Millisecond +) + +// Finding attributes. +const ( + findingCount uint32 = 1 + confidenceHigh = "high" + confidenceLow = "low" + severityInfo = "info" + severityLow = "low" + severityMedium = "medium" +) + +// Config tunes the service. +type Config struct { + // Audience is the exact JWT audience this service verifies on inbound + // calls; it is returned as expected_audience so the gateway refuses a + // mis-registered service. + Audience string + // MaxPayloadBytes is the largest request body the binding accepts. Zero + // selects DefaultMaxPayloadBytes. + MaxPayloadBytes uint64 + // RequestTimeout, when positive, is advertised as the binding's + // request_timeout (OpenShell accepts 10ms to 30s). + RequestTimeout time.Duration + // ImplementationVersion is the build version reported for diagnostics. + ImplementationVersion string + // VettingBudget bounds the wait for an in-flight destination lookup under + // require_registered; observe never waits. Zero selects + // DefaultVettingBudget. + VettingBudget time.Duration + // RequireAuthenticatedCaller denies evaluations that carry no + // authenticated caller. Set it whenever extension authentication is on; + // leave it off only with allow_insecure_no_auth. + RequireAuthenticatedCaller bool +} + +func (c Config) withDefaults() Config { + if c.MaxPayloadBytes == 0 { + c.MaxPayloadBytes = DefaultMaxPayloadBytes + } + if c.VettingBudget <= 0 { + c.VettingBudget = DefaultVettingBudget + } + return c +} + +// IdentityLookup resolves the identity a policy names. +type IdentityLookup interface { + Identity(name string) (*identity.Identity, bool) +} + +// Service implements openshell.middleware.v1.SupervisorMiddleware. The +// WebSocket stream RPC is not implemented and the manifest does not advertise +// it. +type Service struct { + middlewarev1.UnimplementedSupervisorMiddlewareServer + ids IdentityLookup + authz IdentityAuthorizer + vet vetting.Verifier + cfg Config + log *slog.Logger +} + +// NewService builds the service. vet may be nil when destination vetting is not +// configured; policies then may not set destination_policy. A nil logger +// discards. +func NewService(ids IdentityLookup, authz IdentityAuthorizer, vet vetting.Verifier, cfg Config, logger *slog.Logger) *Service { + if ids == nil { + panic("middleware.NewService: nil IdentityLookup") + } + if authz == nil { + panic("middleware.NewService: nil IdentityAuthorizer") + } + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + return &Service{ids: ids, authz: authz, vet: vet, cfg: cfg.withDefaults(), log: logger} +} + +// Describe negotiates the protocol with the gateway and returns the manifest: +// one HTTP_REQUEST binding at PRE_CREDENTIALS. A gateway on another major +// protocol version or requiring a capability this service lacks gets +// FailedPrecondition. +func (s *Service) Describe(ctx context.Context, + req *middlewarev1.MiddlewareDescribeRequest) (*middlewarev1.MiddlewareManifest, error) { + gw := req.GetGateway() + if err := negotiate(gw); err != nil { + s.log.WarnContext(ctx, "describe rejected", "gateway", gw.GetImplementationName(), + "gateway_version", gw.GetImplementationVersion(), "reason", err.Error()) + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + s.log.InfoContext(ctx, "describe", "gateway", gw.GetImplementationName(), + "gateway_version", gw.GetImplementationVersion()) + binding := &middlewarev1.MiddlewareBinding{ + Operation: middlewarev1.SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST, + Phase: middlewarev1.SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS, + MaxPayloadBytes: s.cfg.MaxPayloadBytes, + } + if s.cfg.RequestTimeout > 0 { + binding.RequestTimeout = durationpb.New(s.cfg.RequestTimeout) + } + return &middlewarev1.MiddlewareManifest{ + Name: ManifestName, + ServiceVersion: s.cfg.ImplementationVersion, + Bindings: []*middlewarev1.MiddlewareBinding{binding}, + ExpectedAudience: s.cfg.Audience, + Extension: &extensionv1.PeerMetadata{ + ProtocolVersion: &extensionv1.ProtocolVersion{Major: ProtocolMajor, Minor: ProtocolMinor}, + ImplementationName: ImplementationName, + ImplementationVersion: s.cfg.ImplementationVersion, + SupportedCapabilities: []string{CapabilityContract}, + }, + }, nil +} + +func negotiate(gw *extensionv1.PeerMetadata) error { + if major := gw.GetProtocolVersion().GetMajor(); major != ProtocolMajor { + return fmt.Errorf("gateway protocol version %d.%d is not supported (want %d.x)", + major, gw.GetProtocolVersion().GetMinor(), ProtocolMajor) + } + for _, c := range gw.GetRequiredCapabilities() { + if c != CapabilityContract { + return fmt.Errorf("gateway requires capability %q, which this middleware does not support", c) + } + } + return nil +} + +// ValidateConfig checks a policy's config block when the policy is created: +// unknown keys, unknown mode or destination_policy values, a sign policy +// without a known identity, a strip policy naming one, or a destination +// policy without vetting configured are invalid. An absent config struct is +// InvalidArgument. +func (s *Service) ValidateConfig(ctx context.Context, + req *middlewarev1.ValidateConfigRequest) (*middlewarev1.ValidateConfigResponse, error) { + if req.GetConfig() == nil { + return nil, status.Error(codes.InvalidArgument, "config is required") + } + p, err := s.parsePolicy(req.GetConfig()) + if err == nil && p.Mode == ModeSign { + if _, ok := s.ids.Identity(p.Identity); !ok { + err = fmt.Errorf("unknown identity %q", p.Identity) + } + } + if err != nil { + s.log.InfoContext(ctx, "policy config rejected", "middleware", req.GetMiddlewareName(), "reason", err.Error()) + return &middlewarev1.ValidateConfigResponse{Valid: false, Reason: err.Error()}, nil + } + return &middlewarev1.ValidateConfigResponse{Valid: true}, nil +} + +// parsePolicy parses cfg and checks its shape against this service: sign needs +// an identity name, strip must not name one, and a destination policy needs +// a vetting verifier. +func (s *Service) parsePolicy(cfg *structpb.Struct) (PolicyConfig, error) { + p, err := ParsePolicy(cfg) + if err != nil { + return PolicyConfig{}, err + } + if p.Mode == ModeSign && p.Identity == "" { + return PolicyConfig{}, errors.New("identity is required in sign mode") + } + if p.Mode == ModeStrip && p.Identity != "" { + return PolicyConfig{}, errors.New("identity must not be set in strip mode") + } + if p.DestinationPolicy != vetting.ModeOff && s.vet == nil { + return PolicyConfig{}, fmt.Errorf("destination_policy %s requires destination vetting, which is not configured", + p.DestinationPolicy) + } + return p, nil +} + +// EvaluateHttpRequest decides one buffered request. It never returns a gRPC +// error: every failure is a DENY with a stable reason code so the decision does +// not depend on the policy's on_error setting. Header values and bodies are +// never logged. +func (s *Service) EvaluateHttpRequest(ctx context.Context, //nolint:revive,staticcheck // the generated gRPC interface fixes the name + req *middlewarev1.HttpRequestEvaluation) (*middlewarev1.HttpRequestResult, error) { + res := s.evaluate(ctx, req) + attrs := []slog.Attr{ + slog.String("request_id", req.GetContext().GetRequestId()), + slog.String("sandbox_id", req.GetContext().GetSandboxId()), + slog.String("method", req.GetTarget().GetMethod()), + slog.String("host", req.GetTarget().GetHost()), + } + if res.GetDecision() == middlewarev1.Decision_DECISION_DENY { + attrs = append(attrs, slog.String("reason_code", res.GetReasonCode()), slog.String("reason", res.GetReason())) + s.log.LogAttrs(ctx, denialLevel(res.GetReasonCode()), "request denied", attrs...) + } else { + attrs = append(attrs, + slog.String("identity", res.GetMetadata()[MetadataIdentity]), + slog.String("ans_name", res.GetMetadata()[MetadataAnsName]), + slog.Int("findings", len(res.GetFindings()))) + s.log.LogAttrs(ctx, slog.LevelInfo, "request signed", attrs...) + } + return res, nil +} + +func (s *Service) evaluate(ctx context.Context, req *middlewarev1.HttpRequestEvaluation) *middlewarev1.HttpRequestResult { + out := &outcome{} + if phase := req.GetPhase(); phase != middlewarev1.SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS { + return out.deny(&denial{ReasonUnsupportedPhase, "phase " + phase.String() + " is not bound; only PRE_CREDENTIALS is"}) + } + if d := checkCaller(ctx, req.GetContext().GetSandboxId(), s.cfg.RequireAuthenticatedCaller); d != nil { + return out.deny(d) + } + policy, err := s.parsePolicy(req.GetConfig()) + if err != nil { + return out.deny(&denial{ReasonPolicyInvalid, "policy config: " + err.Error()}) + } + method, target, err := requestTarget(req.GetTarget()) + if err != nil { + return out.deny(&denial{ReasonTargetInvalid, err.Error()}) + } + if policy.DestinationPolicy != vetting.ModeOff { + if d := s.vetDestination(ctx, policy.DestinationPolicy, req.GetTarget().GetHost(), out); d != nil { + return out.deny(d) + } + } + if policy.Mode == ModeStrip { + out.strip() + return out.allow() + } + id, ok := s.ids.Identity(policy.Identity) + if !ok { + return out.deny(&denial{ReasonIdentityUnknown, fmt.Sprintf("identity %q is not configured", policy.Identity)}) + } + if !s.authz.Allows(req.GetContext().GetSandboxId(), policy.Identity) { + return out.deny(&denial{ReasonIdentityUnauthorized, "sandbox is not authorized to use the selected identity"}) + } + if containsCredentialMarker(req.GetBody()) || containsCredentialMarker([]byte(req.GetTarget().GetPath())) { + return out.deny(&denial{ReasonRequestRewriteUnsupported, "credential placeholders in a signed body or path require signing after credential rewriting"}) + } + if d := s.attach(ctx, id, method, target, req.GetBody(), out); d != nil { + return out.deny(d) + } + return out.allow() +} + +// denialLevel separates expected policy outcomes (INFO) from denials that mean +// the service or its dependencies are broken (WARN). +func denialLevel(code string) slog.Level { + switch code { + case ReasonSignFailed, ReasonStatusTokenUnavailable, ReasonReceiptUnavailable, ReasonIdentityUnknown, ReasonPolicyInvalid: + return slog.LevelWarn + } + return slog.LevelInfo +} + +// checkCaller binds the evaluation to its caller: only a sandbox supervisor +// evaluates requests, and only for the sandbox its credential was minted for. +// An unauthenticated call is denied when the service requires authentication +// and otherwise carries no binding to check (insecure transport mode). +func checkCaller(ctx context.Context, sandboxID string, require bool) *denial { + c, ok := extauth.CallerFromContext(ctx) + if !ok { + if require { + return &denial{ReasonCallerUnauthenticated, "no authenticated caller on an evaluation"} + } + return nil + } + if c.Kind != extauth.CallerSupervisor { + return &denial{ReasonCallerNotSupervisor, fmt.Sprintf("caller kind %q may not evaluate requests", c.Kind)} + } + if c.SandboxID != sandboxID { + return &denial{ReasonCallerSandboxMismatch, + fmt.Sprintf("supervisor credential is for sandbox %q but the request context names %q", c.SandboxID, sandboxID)} + } + return nil +} + +// requestTarget returns the method and bound URL of the target. +func requestTarget(t *middlewarev1.HttpRequestTarget) (string, string, error) { + if t.GetMethod() == "" { + return "", "", errors.New("target method is required") + } + target, err := TargetURL(t) + if err != nil { + return "", "", err + } + return t.GetMethod(), target, nil +} + +// vetDestination records the host's verdict as a finding and denies when the +// policy mode says so. Only require_registered waits for an in-flight lookup. +func (s *Service) vetDestination(ctx context.Context, mode vetting.Mode, host string, out *outcome) *denial { + var wait time.Duration + if mode == vetting.ModeRequire { + wait = s.cfg.VettingBudget + } + v := s.vet.Lookup(ctx, host, wait) + label := v.Status.String() + out.finding(FindingDestinationPrefix+label, "ANS destination "+label, destinationConfidence(v.Status), destinationSeverity(v.Status)) + out.meta(MetadataDestinationStatus, label) + if d := vetting.Decide(mode, v); d.Deny { + return &denial{d.ReasonCode, fmt.Sprintf("destination %s is %s under %s: %s", host, label, mode, v.Detail)} + } + return nil +} + +// attach signs the request as id and writes the three identity headers with +// OVERWRITE, so nothing the workload sent under those names survives. +func (s *Service) attach(ctx context.Context, id *identity.Identity, method, target string, body []byte, + out *outcome) *denial { + arts := id.Artifacts().Current() + if arts.StatusTokenBase64 == "" { + return &denial{ReasonStatusTokenUnavailable, fmt.Sprintf("identity %q has no unexpired status token", id.Name())} + } + if arts.ReceiptBase64 == "" { + return &denial{ReasonReceiptUnavailable, fmt.Sprintf("identity %q has no receipt", id.Name())} + } + proof, err := id.Signer().Sign(ctx, method, target, sha256.Sum256(body)) + if err != nil { + return &denial{ReasonSignFailed, fmt.Sprintf("identity %q: sign: %v", id.Name(), err)} + } + out.write(pop.DPoPHeader, proof) + out.write(scitt.HeaderReceipt, arts.ReceiptBase64) + out.write(scitt.HeaderStatusToken, arts.StatusTokenBase64) + out.finding(FindingIdentityAttached, "ANS identity attached", confidenceHigh, severityInfo) + out.meta(MetadataIdentity, id.Name()) + out.meta(MetadataAnsName, id.AnsName()) + out.meta(MetadataAgentID, id.AgentID()) + return nil +} + +func destinationSeverity(st vetting.Status) string { + switch st { + case vetting.StatusRegistered, vetting.StatusExempt: + return severityInfo + case vetting.StatusUnknown, vetting.StatusError: + return severityLow + case vetting.StatusUnregistered, vetting.StatusRevoked, vetting.StatusUntrustedTL, vetting.StatusBelowThreshold: + return severityMedium + } + return severityMedium +} + +func destinationConfidence(st vetting.Status) string { + if st == vetting.StatusUnknown || st == vetting.StatusError { + return confidenceLow + } + return confidenceHigh +} + +// denial is a fail-closed decision: the stable reason code OpenShell may relay +// and the free-text reason it never does. +type denial struct { + code string + reason string +} + +// outcome accumulates the findings, metadata and header mutations of one +// evaluation and renders the result. +type outcome struct { + findings []*middlewarev1.Finding + metadata map[string]string + mutations []*middlewarev1.HeaderMutation +} + +func (o *outcome) finding(typ, label, confidence, severity string) { + o.findings = append(o.findings, &middlewarev1.Finding{ + Type: typ, Label: label, Count: findingCount, Confidence: confidence, Severity: severity, + }) +} + +func (o *outcome) meta(key, value string) { + if o.metadata == nil { + o.metadata = make(map[string]string) + } + o.metadata[key] = value +} + +func (o *outcome) write(name, value string) { + o.mutations = append(o.mutations, &middlewarev1.HeaderMutation{ + Operation: &middlewarev1.HeaderMutation_Write{Write: &middlewarev1.WriteHeader{ + Name: name, + Value: value, + OnExisting: middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_OVERWRITE, + }}, + }) +} + +// strip removes the identity headers and records the finding. +func (o *outcome) strip() { + for _, name := range []string{pop.DPoPHeader, scitt.HeaderReceipt, scitt.HeaderStatusToken} { + o.mutations = append(o.mutations, &middlewarev1.HeaderMutation{ + Operation: &middlewarev1.HeaderMutation_Remove{Remove: &middlewarev1.RemoveHeader{Name: name}}, + }) + } + o.finding(FindingIdentityStripped, "ANS identity headers stripped", confidenceHigh, severityInfo) +} + +func (o *outcome) allow() *middlewarev1.HttpRequestResult { + return &middlewarev1.HttpRequestResult{ + Decision: middlewarev1.Decision_DECISION_ALLOW, + HeaderMutations: o.mutations, + Findings: o.findings, + Metadata: o.metadata, + } +} + +func (o *outcome) deny(d *denial) *middlewarev1.HttpRequestResult { + return &middlewarev1.HttpRequestResult{ + Decision: middlewarev1.Decision_DECISION_DENY, + Reason: d.reason, + ReasonCode: d.code, + Findings: o.findings, + Metadata: o.metadata, + } +} diff --git a/internal/middleware/service_test.go b/internal/middleware/service_test.go new file mode 100644 index 0000000..3382f4b --- /dev/null +++ b/internal/middleware/service_test.go @@ -0,0 +1,561 @@ +package middleware + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + "log/slog" + "regexp" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/agentnameservice/ans-openshell/gen/openshell/extensionv1" + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/extauth" + "github.com/agentnameservice/ans-openshell/internal/identity" + "github.com/agentnameservice/ans-openshell/internal/vetting" +) + +const ( + testIdentity = "payments-agent" + testAgentID = "7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07" + testAnsName = "ans://v1.0.0.payments.example.com" + testSandbox = "sb-1" + receiptB64 = "cmVjZWlwdA==" + tokenB64 = "dG9rZW4=" +) + +const ( + preCredentials = middlewarev1.SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS + preReturn = middlewarev1.SupervisorMiddlewarePhase_SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN + allow = middlewarev1.Decision_DECISION_ALLOW + deny = middlewarev1.Decision_DECISION_DENY +) + +type signCall struct { + method, url string + digest [sha256.Size]byte +} + +// fakeSigner records calls and returns a numbered proof or a fixed error. +type fakeSigner struct { + mu sync.Mutex + err error + calls []signCall +} + +func (f *fakeSigner) Sign(_ context.Context, method, rawURL string, digest [sha256.Size]byte) (string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = append(f.calls, signCall{method: method, url: rawURL, digest: digest}) + if f.err != nil { + return "", f.err + } + return "proof-" + strconv.Itoa(len(f.calls)), nil +} + +// fakeVerifier answers every lookup with one verdict and records the arguments. +type fakeVerifier struct { + mu sync.Mutex + verdict vetting.Verdict + hosts []string + waits []time.Duration +} + +func (f *fakeVerifier) Lookup(_ context.Context, host string, wait time.Duration) vetting.Verdict { + f.mu.Lock() + defer f.mu.Unlock() + f.hosts = append(f.hosts, host) + f.waits = append(f.waits, wait) + v := f.verdict + v.Host = host + return v +} + +type sandboxAuthorizer string + +func (a sandboxAuthorizer) Allows(sandboxID, _ string) bool { + return sandboxID == string(a) +} + +type lookup map[string]*identity.Identity + +func (l lookup) Identity(name string) (*identity.Identity, bool) { + id, ok := l[name] + return id, ok +} + +func newIdentity(t *testing.T, name string, signer identity.Signer, arts identity.Artifacts) *identity.Identity { + t.Helper() + id, err := identity.NewIdentity(name, testAnsName, testAgentID, signer, identity.NewStaticSource(arts)) + if err != nil { + t.Fatal(err) + } + return id +} + +func fullArtifacts() identity.Artifacts { + return identity.Artifacts{ReceiptBase64: receiptB64, StatusTokenBase64: tokenB64} +} + +// evaluation builds a PRE_CREDENTIALS request for a POST with the given policy. +func evaluation(t *testing.T, policy map[string]any, body []byte) *middlewarev1.HttpRequestEvaluation { + t.Helper() + return &middlewarev1.HttpRequestEvaluation{ + Phase: preCredentials, + Context: &middlewarev1.RequestContext{RequestId: "req-1", SandboxId: testSandbox}, + Config: structOf(t, policy), + Target: &middlewarev1.HttpRequestTarget{ + Scheme: "https", Host: "tools.example.com", Port: 443, Method: "POST", Path: "/v1/do", Query: "x=1", + }, + Headers: []*middlewarev1.HttpHeader{{Name: "dpop", Value: "smuggled"}}, + Body: body, + } +} + +func signPolicy() map[string]any { return map[string]any{"identity": testIdentity} } + +func TestNewServiceRequiresLookup(t *testing.T) { + t.Parallel() + defer func() { + if recover() == nil { + t.Fatal("NewService(nil lookup) did not panic") + } + }() + NewService(nil, sandboxAuthorizer(testSandbox), nil, Config{}, nil) +} + +func TestConfigDefaults(t *testing.T) { + t.Parallel() + s := NewService(lookup{}, sandboxAuthorizer(testSandbox), nil, Config{}, nil) + if s.cfg.MaxPayloadBytes != DefaultMaxPayloadBytes || s.cfg.VettingBudget != DefaultVettingBudget { + t.Fatalf("defaults not applied: %+v", s.cfg) + } + custom := Config{MaxPayloadBytes: 1024, VettingBudget: time.Second, RequestTimeout: time.Second} + if got := NewService(lookup{}, sandboxAuthorizer(testSandbox), nil, custom, nil).cfg; got != custom { + t.Fatalf("explicit config changed: %+v", got) + } +} + +func TestDescribe(t *testing.T) { + t.Parallel() + v1 := &extensionv1.ProtocolVersion{Major: 1} + tests := []struct { + name string + gateway *extensionv1.PeerMetadata + wantCode codes.Code + }{ + {name: "protocol 1.0", gateway: &extensionv1.PeerMetadata{ProtocolVersion: v1}}, + {name: "protocol 1.3 with supported requirement", gateway: &extensionv1.PeerMetadata{ + ProtocolVersion: &extensionv1.ProtocolVersion{Major: 1, Minor: 3}, RequiredCapabilities: []string{CapabilityContract}}}, + {name: "no gateway metadata", gateway: nil, wantCode: codes.FailedPrecondition}, + {name: "protocol 2.0", gateway: &extensionv1.PeerMetadata{ProtocolVersion: &extensionv1.ProtocolVersion{Major: 2}}, + wantCode: codes.FailedPrecondition}, + {name: "unsupported requirement", gateway: &extensionv1.PeerMetadata{ + ProtocolVersion: v1, RequiredCapabilities: []string{CapabilityContract, "openshell.websocket"}}, + wantCode: codes.FailedPrecondition}, + } + cfg := Config{Audience: "urn:openshell:extension:middleware:ans", ImplementationVersion: "v0.1.0", RequestTimeout: 250 * time.Millisecond} + s := NewService(lookup{}, sandboxAuthorizer(testSandbox), nil, cfg, nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + m, err := s.Describe(t.Context(), &middlewarev1.MiddlewareDescribeRequest{Gateway: tt.gateway}) + if tt.wantCode != codes.OK { + if status.Code(err) != tt.wantCode { + t.Fatalf("Describe() error = %v, want code %v", err, tt.wantCode) + } + return + } + if err != nil { + t.Fatalf("Describe() error = %v", err) + } + if m.GetName() != ManifestName || m.GetServiceVersion() != "v0.1.0" || m.GetExpectedAudience() != cfg.Audience { + t.Fatalf("manifest = %v", m) + } + ext := m.GetExtension() + if ext.GetProtocolVersion().GetMajor() != 1 || ext.GetProtocolVersion().GetMinor() != 0 || + ext.GetImplementationName() != ImplementationName || ext.GetImplementationVersion() != "v0.1.0" || + len(ext.GetSupportedCapabilities()) != 1 || ext.GetSupportedCapabilities()[0] != CapabilityContract { + t.Fatalf("extension metadata = %v", ext) + } + if len(m.GetBindings()) != 1 { + t.Fatalf("bindings = %v", m.GetBindings()) + } + b := m.GetBindings()[0] + if b.GetOperation() != middlewarev1.SupervisorMiddlewareOperation_SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST || + b.GetPhase() != preCredentials || b.GetMaxPayloadBytes() != DefaultMaxPayloadBytes || + b.GetRequestTimeout().AsDuration() != cfg.RequestTimeout { + t.Fatalf("binding = %v", b) + } + }) + } + t.Run("no request timeout when unset", func(t *testing.T) { + t.Parallel() + m, err := NewService(lookup{}, sandboxAuthorizer(testSandbox), nil, Config{}, nil).Describe(t.Context(), + &middlewarev1.MiddlewareDescribeRequest{Gateway: &extensionv1.PeerMetadata{ProtocolVersion: v1}}) + if err != nil { + t.Fatal(err) + } + if m.GetBindings()[0].GetRequestTimeout() != nil { + t.Fatalf("request_timeout = %v, want unset", m.GetBindings()[0].GetRequestTimeout()) + } + }) +} + +func TestValidateConfig(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + withVet := NewService(ids, sandboxAuthorizer(testSandbox), &fakeVerifier{}, Config{}, nil) + noVet := NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{}, nil) + tests := []struct { + name string + svc *Service + policy map[string]any + wantErr string + }{ + {name: "sign known identity", svc: noVet, policy: signPolicy()}, + {name: "strip", svc: noVet, policy: map[string]any{"mode": "strip"}}, + {name: "observe with vetting", svc: withVet, policy: map[string]any{"identity": testIdentity, "destination_policy": "observe"}}, + {name: "require with vetting", svc: withVet, policy: map[string]any{"mode": "strip", "destination_policy": "require_registered"}}, + {name: "sign unknown identity", svc: noVet, policy: map[string]any{"identity": "ghost"}, wantErr: `unknown identity "ghost"`}, + {name: "sign without identity", svc: noVet, policy: map[string]any{}, wantErr: "identity is required"}, + {name: "strip with identity", svc: noVet, policy: map[string]any{"mode": "strip", "identity": testIdentity}, + wantErr: "must not be set in strip mode"}, + {name: "unknown key", svc: noVet, policy: map[string]any{"identity": testIdentity, "order": "100"}, wantErr: `unknown field "order"`}, + {name: "bad mode", svc: noVet, policy: map[string]any{"mode": "verify"}, wantErr: "unknown mode"}, + {name: "bad destination policy", svc: withVet, policy: map[string]any{"identity": testIdentity, "destination_policy": "block"}, + wantErr: "unknown destination_policy"}, + {name: "destination policy without vetting", svc: noVet, + policy: map[string]any{"identity": testIdentity, "destination_policy": "observe"}, wantErr: "not configured"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resp, err := tt.svc.ValidateConfig(t.Context(), &middlewarev1.ValidateConfigRequest{ + Config: structOf(t, tt.policy), MiddlewareName: "ans"}) + if err != nil { + t.Fatalf("ValidateConfig() error = %v", err) + } + if tt.wantErr == "" { + if !resp.GetValid() || resp.GetReason() != "" { + t.Fatalf("ValidateConfig() = %v, want valid", resp) + } + return + } + if resp.GetValid() || !strings.Contains(resp.GetReason(), tt.wantErr) { + t.Fatalf("ValidateConfig() = %v, want invalid with %q", resp, tt.wantErr) + } + }) + } + t.Run("absent config struct is a gRPC error", func(t *testing.T) { + t.Parallel() + _, err := noVet.ValidateConfig(t.Context(), &middlewarev1.ValidateConfigRequest{}) + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("ValidateConfig(nil config) error = %v, want InvalidArgument", err) + } + }) +} + +func TestEvaluateSignAttachesIdentity(t *testing.T) { + t.Parallel() + signer := &fakeSigner{} + ids := lookup{testIdentity: newIdentity(t, testIdentity, signer, fullArtifacts())} + var logs bytes.Buffer + s := NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{}, slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) + body := []byte(`{"amount":1}`) + + res, err := s.EvaluateHttpRequest(t.Context(), evaluation(t, signPolicy(), body)) + if err != nil { + t.Fatalf("EvaluateHttpRequest() error = %v", err) + } + if res.GetDecision() != allow || res.GetReasonCode() != "" || res.GetHasBody() { + t.Fatalf("result = %v", res) + } + wantWrites := map[string]string{pop.DPoPHeader: "proof-1", scitt.HeaderReceipt: receiptB64, scitt.HeaderStatusToken: tokenB64} + muts := res.GetHeaderMutations() + if len(muts) != len(wantWrites) { + t.Fatalf("mutations = %v", muts) + } + for i, name := range []string{pop.DPoPHeader, scitt.HeaderReceipt, scitt.HeaderStatusToken} { + w := muts[i].GetWrite() + if w.GetName() != name || w.GetValue() != wantWrites[name] || + w.GetOnExisting() != middlewarev1.ExistingHeaderAction_EXISTING_HEADER_ACTION_OVERWRITE { + t.Fatalf("mutation %d = %v, want OVERWRITE %s", i, muts[i], name) + } + } + if len(res.GetFindings()) != 1 || res.GetFindings()[0].GetType() != FindingIdentityAttached || + res.GetFindings()[0].GetCount() != 1 || res.GetFindings()[0].GetSeverity() != severityInfo { + t.Fatalf("findings = %v", res.GetFindings()) + } + wantMeta := map[string]string{MetadataIdentity: testIdentity, MetadataAnsName: testAnsName, MetadataAgentID: testAgentID} + if got := res.GetMetadata(); len(got) != len(wantMeta) { + t.Fatalf("metadata = %v", got) + } + for k, v := range wantMeta { + if res.GetMetadata()[k] != v { + t.Errorf("metadata[%s] = %q, want %q", k, res.GetMetadata()[k], v) + } + } + if len(signer.calls) != 1 || signer.calls[0].method != "POST" || + signer.calls[0].url != "https://tools.example.com/v1/do" || signer.calls[0].digest != sha256.Sum256(body) { + t.Fatalf("signer calls = %+v", signer.calls) + } + if out := logs.String(); !strings.Contains(out, "request signed") || strings.Contains(out, "proof-1") || + strings.Contains(out, "amount") || strings.Contains(out, receiptB64) { + t.Fatalf("log output leaks header or body content or lacks the decision: %s", out) + } + + t.Run("bodyless request binds the empty digest", func(t *testing.T) { + t.Parallel() + res, err := s.EvaluateHttpRequest(t.Context(), evaluation(t, signPolicy(), nil)) + if err != nil || res.GetDecision() != allow { + t.Fatalf("result = %v, %v", res, err) + } + signer.mu.Lock() + defer signer.mu.Unlock() + if last := signer.calls[len(signer.calls)-1]; last.digest != sha256.Sum256(nil) { + t.Fatalf("digest = %x, want empty-content digest", last.digest) + } + }) +} + +func TestEvaluateStrip(t *testing.T) { + t.Parallel() + signer := &fakeSigner{} + ids := lookup{testIdentity: newIdentity(t, testIdentity, signer, fullArtifacts())} + s := NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{}, nil) + res, err := s.EvaluateHttpRequest(t.Context(), evaluation(t, map[string]any{"mode": "strip"}, []byte("x"))) + if err != nil || res.GetDecision() != allow { + t.Fatalf("result = %v, %v", res, err) + } + want := []string{pop.DPoPHeader, scitt.HeaderReceipt, scitt.HeaderStatusToken} + if len(res.GetHeaderMutations()) != len(want) { + t.Fatalf("mutations = %v", res.GetHeaderMutations()) + } + for i, m := range res.GetHeaderMutations() { + if m.GetRemove().GetName() != want[i] || m.GetWrite() != nil { + t.Fatalf("mutation %d = %v, want remove %s", i, m, want[i]) + } + } + if len(res.GetFindings()) != 1 || res.GetFindings()[0].GetType() != FindingIdentityStripped || len(res.GetMetadata()) != 0 { + t.Fatalf("findings = %v metadata = %v", res.GetFindings(), res.GetMetadata()) + } + if len(signer.calls) != 0 { + t.Fatal("strip mode signed") + } +} + +func TestEvaluateDenies(t *testing.T) { + t.Parallel() + okID := newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts()) + noToken := newIdentity(t, "no-token", &fakeSigner{}, identity.Artifacts{ReceiptBase64: receiptB64}) + noReceipt := newIdentity(t, "no-receipt", &fakeSigner{}, identity.Artifacts{StatusTokenBase64: tokenB64}) + brokenSigner := newIdentity(t, "broken", &fakeSigner{err: errors.New("hsm offline")}, fullArtifacts()) + ids := lookup{testIdentity: okID, "no-token": noToken, "no-receipt": noReceipt, "broken": brokenSigner} + supervisorOther := extauth.ContextWithCaller(t.Context(), extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: "sb-other"}) + + tests := []struct { + name string + ctx context.Context + vet vetting.Verifier + mutate func(e *middlewarev1.HttpRequestEvaluation) + policy map[string]any + wantCode string + }{ + {name: "pre-return phase", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Phase = preReturn }, wantCode: ReasonUnsupportedPhase}, + {name: "unspecified phase", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Phase = 0 }, wantCode: ReasonUnsupportedPhase}, + {name: "supervisor for another sandbox", ctx: supervisorOther, wantCode: ReasonCallerSandboxMismatch}, + {name: "absent config", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Config = nil }, wantCode: ReasonPolicyInvalid}, + {name: "unknown policy key", policy: map[string]any{"identity": testIdentity, "extra": "x"}, wantCode: ReasonPolicyInvalid}, + {name: "destination policy without vetting", policy: map[string]any{"identity": testIdentity, "destination_policy": "observe"}, + wantCode: ReasonPolicyInvalid}, + {name: "absent target", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Target = nil }, wantCode: ReasonTargetInvalid}, + {name: "empty method", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Target.Method = "" }, wantCode: ReasonTargetInvalid}, + {name: "unsupported scheme", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Target.Scheme = "ftp" }, wantCode: ReasonTargetInvalid}, + {name: "empty host", mutate: func(e *middlewarev1.HttpRequestEvaluation) { e.Target.Host = "" }, wantCode: ReasonTargetInvalid}, + {name: "unknown identity", policy: map[string]any{"identity": "ghost"}, wantCode: ReasonIdentityUnknown}, + {name: "status token unavailable", policy: map[string]any{"identity": "no-token"}, wantCode: ReasonStatusTokenUnavailable}, + {name: "receipt unavailable", policy: map[string]any{"identity": "no-receipt"}, wantCode: ReasonReceiptUnavailable}, + {name: "signer failure", policy: map[string]any{"identity": "broken"}, wantCode: ReasonSignFailed}, + {name: "unregistered destination under require", vet: &fakeVerifier{verdict: vetting.Verdict{Status: vetting.StatusUnregistered}}, + policy: map[string]any{"identity": testIdentity, "destination_policy": "require_registered"}, wantCode: vetting.ReasonUnregistered}, + {name: "revoked destination under require", vet: &fakeVerifier{verdict: vetting.Verdict{Status: vetting.StatusRevoked}}, + policy: map[string]any{"mode": "strip", "destination_policy": "require_registered"}, wantCode: vetting.ReasonRevoked}, + {name: "unknown destination under require", vet: &fakeVerifier{verdict: vetting.Verdict{Status: vetting.StatusUnknown}}, + policy: map[string]any{"identity": testIdentity, "destination_policy": "require_registered"}, wantCode: vetting.ReasonUnverified}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := tt.ctx + if ctx == nil { + ctx = t.Context() + } + policy := tt.policy + if policy == nil { + policy = signPolicy() + } + e := evaluation(t, policy, []byte("body")) + if tt.mutate != nil { + tt.mutate(e) + } + s := NewService(ids, sandboxAuthorizer(testSandbox), tt.vet, Config{}, nil) + res, err := s.EvaluateHttpRequest(ctx, e) + if err != nil { + t.Fatalf("EvaluateHttpRequest() error = %v", err) + } + if res.GetDecision() != deny || res.GetReasonCode() != tt.wantCode || res.GetReason() == "" { + t.Fatalf("result = %v, want DENY %s with a reason", res, tt.wantCode) + } + if len(res.GetHeaderMutations()) != 0 { + t.Fatalf("DENY carries mutations: %v", res.GetHeaderMutations()) + } + }) + } +} + +func TestEvaluateCallerBinding(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + tests := []struct { + name string + caller *extauth.Caller + require bool + wantCode string + }{ + {name: "unauthenticated in insecure mode"}, + {name: "unauthenticated when authentication is required", require: true, wantCode: ReasonCallerUnauthenticated}, + {name: "supervisor for this sandbox", caller: &extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: testSandbox}, require: true}, + {name: "supervisor for another sandbox", caller: &extauth.Caller{Kind: extauth.CallerSupervisor, SandboxID: "sb-other"}, + wantCode: ReasonCallerSandboxMismatch}, + {name: "gateway", caller: &extauth.Caller{Kind: extauth.CallerGateway}, wantCode: ReasonCallerNotSupervisor}, + {name: "unknown kind", caller: &extauth.Caller{Kind: "operator", SandboxID: testSandbox}, wantCode: ReasonCallerNotSupervisor}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := t.Context() + if tt.caller != nil { + ctx = extauth.ContextWithCaller(ctx, *tt.caller) + } + s := NewService(ids, sandboxAuthorizer(testSandbox), nil, Config{RequireAuthenticatedCaller: tt.require}, nil) + res, err := s.EvaluateHttpRequest(ctx, evaluation(t, signPolicy(), nil)) + if err != nil { + t.Fatalf("EvaluateHttpRequest() error = %v", err) + } + if tt.wantCode == "" { + if res.GetDecision() != allow { + t.Fatalf("result = %v, want ALLOW", res) + } + return + } + if res.GetDecision() != deny || res.GetReasonCode() != tt.wantCode { + t.Fatalf("result = %v, want DENY %s", res, tt.wantCode) + } + }) + } +} + +func TestEvaluateDestinationVetting(t *testing.T) { + t.Parallel() + ids := lookup{testIdentity: newIdentity(t, testIdentity, &fakeSigner{}, fullArtifacts())} + budget := 75 * time.Millisecond + tests := []struct { + name string + status vetting.Status + policy string + wantWait time.Duration + wantDeny bool + confidence string + severity string + }{ + {name: "registered under require", status: vetting.StatusRegistered, policy: "require_registered", wantWait: budget, + confidence: confidenceHigh, severity: severityInfo}, + {name: "exempt under require", status: vetting.StatusExempt, policy: "require_registered", wantWait: budget, + confidence: confidenceHigh, severity: severityInfo}, + {name: "unregistered under observe", status: vetting.StatusUnregistered, policy: "observe", wantWait: 0, + confidence: confidenceHigh, severity: severityMedium}, + {name: "untrusted tl under observe", status: vetting.StatusUntrustedTL, policy: "observe", wantWait: 0, + confidence: confidenceHigh, severity: severityMedium}, + {name: "below threshold under observe", status: vetting.StatusBelowThreshold, policy: "observe", wantWait: 0, + confidence: confidenceHigh, severity: severityMedium}, + {name: "error under observe", status: vetting.StatusError, policy: "observe", wantWait: 0, + confidence: confidenceLow, severity: severityLow}, + {name: "unknown under observe", status: vetting.StatusUnknown, policy: "observe", wantWait: 0, + confidence: confidenceLow, severity: severityLow}, + {name: "unregistered under require", status: vetting.StatusUnregistered, policy: "require_registered", wantWait: budget, + wantDeny: true, confidence: confidenceHigh, severity: severityMedium}, + {name: "unmapped status", status: vetting.Status(99), policy: "observe", wantWait: 0, + confidence: confidenceHigh, severity: severityMedium}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + vet := &fakeVerifier{verdict: vetting.Verdict{Status: tt.status, Detail: "test"}} + s := NewService(ids, sandboxAuthorizer(testSandbox), vet, Config{VettingBudget: budget}, nil) + policy := map[string]any{"identity": testIdentity, "destination_policy": tt.policy} + res, err := s.EvaluateHttpRequest(t.Context(), evaluation(t, policy, nil)) + if err != nil { + t.Fatal(err) + } + if len(vet.hosts) != 1 || vet.hosts[0] != "tools.example.com" || vet.waits[0] != tt.wantWait { + t.Fatalf("lookup args = %v %v, want host tools.example.com wait %v", vet.hosts, vet.waits, tt.wantWait) + } + wantType := FindingDestinationPrefix + tt.status.String() + var found *middlewarev1.Finding + for _, f := range res.GetFindings() { + if f.GetType() == wantType { + found = f + } + } + if found == nil || found.GetConfidence() != tt.confidence || found.GetSeverity() != tt.severity { + t.Fatalf("findings = %v, want %s %s/%s", res.GetFindings(), wantType, tt.confidence, tt.severity) + } + if res.GetMetadata()[MetadataDestinationStatus] != tt.status.String() { + t.Fatalf("metadata = %v", res.GetMetadata()) + } + if tt.wantDeny { + if res.GetDecision() != deny || res.GetReasonCode() != vetting.ReasonUnregistered { + t.Fatalf("result = %v, want DENY %s", res, vetting.ReasonUnregistered) + } + return + } + if res.GetDecision() != allow || len(res.GetFindings()) != 2 || res.GetMetadata()[MetadataIdentity] != testIdentity { + t.Fatalf("result = %v, want ALLOW with identity and destination findings", res) + } + }) + } +} + +func TestReasonCodeGrammar(t *testing.T) { + t.Parallel() + grammar := regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`) + check := func(code string) { + if !grammar.MatchString(code) { + t.Errorf("%q does not match the reason_code grammar", code) + } + } + for _, code := range []string{ + ReasonUnsupportedPhase, ReasonCallerUnauthenticated, ReasonCallerNotSupervisor, ReasonCallerSandboxMismatch, + ReasonPolicyInvalid, ReasonTargetInvalid, ReasonIdentityUnknown, + ReasonStatusTokenUnavailable, ReasonReceiptUnavailable, ReasonSignFailed, + vetting.ReasonUnregistered, vetting.ReasonRevoked, vetting.ReasonUntrustedTL, vetting.ReasonBelowThreshold, vetting.ReasonUnverified, + FindingIdentityAttached, FindingIdentityStripped, + } { + check(code) + } + for _, st := range []vetting.Status{vetting.StatusUnknown, vetting.StatusRegistered, vetting.StatusUnregistered, vetting.StatusRevoked, + vetting.StatusUntrustedTL, vetting.StatusBelowThreshold, vetting.StatusError, vetting.StatusExempt} { + check(FindingDestinationPrefix + st.String()) + } +} diff --git a/internal/middleware/target.go b/internal/middleware/target.go new file mode 100644 index 0000000..1f7e472 --- /dev/null +++ b/internal/middleware/target.go @@ -0,0 +1,81 @@ +package middleware + +import ( + "errors" + "fmt" + "net/netip" + "strconv" + "strings" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" +) + +const ( + schemeHTTP = "http" + schemeHTTPS = "https" + schemeWS = "ws" + schemeWSS = "wss" + defaultPortHTTP = 80 + defaultPortHTTPS = 443 +) + +// TargetURL renders the admitted target as the absolute URL the DPoP proof +// binds (ANS-6 section 7.3): scheme://host[:port]/path, with ws and wss mapped +// to http and https, the scheme's default port omitted, an empty path +// normalized to "/" and the query never included. +func TargetURL(t *middlewarev1.HttpRequestTarget) (string, error) { + scheme, defaultPort, err := httpScheme(t.GetScheme()) + if err != nil { + return "", err + } + host := strings.ToLower(strings.TrimSpace(t.GetHost())) + if host == "" { + return "", errors.New("target host is required") + } + if !validAuthorityHost(host) { + return "", errors.New("target host is not a valid authority") + } + if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { + host = "[" + host + "]" + } + if port := t.GetPort(); port != 0 && port != defaultPort { + host += ":" + strconv.FormatUint(uint64(port), 10) + } + path := t.GetPath() + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return scheme + "://" + host + path, nil +} + +// httpScheme maps a target scheme to the HTTP scheme the proof binds and that +// scheme's default port. +func httpScheme(scheme string) (string, uint32, error) { + switch strings.ToLower(strings.TrimSpace(scheme)) { + case schemeHTTP, schemeWS: + return schemeHTTP, defaultPortHTTP, nil + case schemeHTTPS, schemeWSS: + return schemeHTTPS, defaultPortHTTPS, nil + } + return "", 0, fmt.Errorf("unsupported target scheme %q", scheme) +} + +// validAuthorityHost rejects a host that could rebind the signed URL: anything +// that would terminate the authority or start a path, query, fragment or +// userinfo, and colons outside an IPv6 literal. +func validAuthorityHost(host string) bool { + if strings.ContainsAny(host, "/@?#\\ \t\r\n") { + return false + } + switch { + case strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]"): + _, err := netip.ParseAddr(host[1 : len(host)-1]) + return err == nil + case strings.ContainsAny(host, "[]"): + return false + case strings.Contains(host, ":"): + _, err := netip.ParseAddr(host) + return err == nil + } + return true +} diff --git a/internal/middleware/target_test.go b/internal/middleware/target_test.go new file mode 100644 index 0000000..2c2111e --- /dev/null +++ b/internal/middleware/target_test.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "crypto/sha256" + "net/http" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" + "github.com/agentnameservice/ans-openshell/internal/testfixtures" +) + +func target(scheme, host string, port uint32, path, query string) *middlewarev1.HttpRequestTarget { + return &middlewarev1.HttpRequestTarget{Scheme: scheme, Host: host, Port: port, Method: http.MethodGet, Path: path, Query: query} +} + +func TestTargetURL(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in *middlewarev1.HttpRequestTarget + want string + wantErr bool + }{ + {name: "https default port and query dropped", in: target("https", "tools.example.com", 443, "/v1/do", "a=1&b=2"), + want: "https://tools.example.com/v1/do"}, + {name: "https custom port", in: target("https", "tools.example.com", 8443, "/v1/do", ""), + want: "https://tools.example.com:8443/v1/do"}, + {name: "http default port", in: target("http", "tools.example.com", 80, "/x", ""), want: "http://tools.example.com/x"}, + {name: "http on 443 keeps the port", in: target("http", "tools.example.com", 443, "/x", ""), + want: "http://tools.example.com:443/x"}, + {name: "https on 80 keeps the port", in: target("https", "tools.example.com", 80, "/x", ""), + want: "https://tools.example.com:80/x"}, + {name: "ws maps to http", in: target("ws", "tools.example.com", 80, "/socket", ""), want: "http://tools.example.com/socket"}, + {name: "wss maps to https", in: target("wss", "tools.example.com", 8443, "/socket", ""), + want: "https://tools.example.com:8443/socket"}, + {name: "empty path becomes slash", in: target("https", "tools.example.com", 443, "", ""), want: "https://tools.example.com/"}, + {name: "path without leading slash", in: target("https", "tools.example.com", 0, "v1", ""), want: "https://tools.example.com/v1"}, + {name: "zero port omitted", in: target("https", "tools.example.com", 0, "/", ""), want: "https://tools.example.com/"}, + {name: "scheme and host lowercased, path preserved", in: target(" HTTPS ", " Tools.Example.COM ", 0, "/A/B%20c", ""), + want: "https://tools.example.com/A/B%20c"}, + {name: "ipv6 literal bracketed", in: target("https", "2001:db8::1", 8443, "/", ""), want: "https://[2001:db8::1]:8443/"}, + {name: "bracketed ipv6 kept", in: target("http", "[::1]", 8080, "/", ""), want: "http://[::1]:8080/"}, + {name: "unsupported scheme", in: target("ftp", "tools.example.com", 21, "/", ""), wantErr: true}, + {name: "empty scheme", in: target("", "tools.example.com", 443, "/", ""), wantErr: true}, + {name: "empty host", in: target("https", " ", 443, "/", ""), wantErr: true}, + {name: "nil target", in: nil, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := TargetURL(tt.in) + if (err != nil) != tt.wantErr { + t.Fatalf("TargetURL() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("TargetURL() = %q, want %q", got, tt.want) + } + }) + } +} + +// TestTargetURLMatchesVerifierNormalization proves a proof minted over +// TargetURL's output verifies against the URL a callee reconstructs from the +// wire request, including the query the proof must not bind. +func TestTargetURLMatchesVerifierNormalization(t *testing.T) { + t.Parallel() + id := testfixtures.MustNewIdentity("agent", "ans://v1.0.0.agent.example.com") + signer, err := pop.NewSigner(id.Key, id.CertDER) + if err != nil { + t.Fatal(err) + } + bound, err := TargetURL(target("wss", "Tools.Example.com", 443, "/v1/stream", "token=secret")) + if err != nil { + t.Fatal(err) + } + proof, err := signer.Sign(t.Context(), http.MethodGet, bound, pop.WithContentDigest(sha256.Sum256(nil))) + if err != nil { + t.Fatal(err) + } + replay := pop.NewMemoryReplayCache(t.Context(), 0) + defer replay.Close() + wire := "https://tools.example.com:443/v1/stream?token=secret" + if _, err := pop.VerifyProof(t.Context(), proof, http.MethodGet, wire, time.Now(), 0, replay); err != nil { + t.Fatalf("proof over %q does not verify against %q: %v", bound, wire, err) + } +} diff --git a/internal/middleware/target_validation_test.go b/internal/middleware/target_validation_test.go new file mode 100644 index 0000000..fbb53dc --- /dev/null +++ b/internal/middleware/target_validation_test.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "testing" + + "github.com/agentnameservice/ans-openshell/gen/openshell/middlewarev1" +) + +func TestTargetURL_RejectsAuthorityRebinding(t *testing.T) { + t.Parallel() + tests := []struct { + host string + ok bool + want string + }{ + {host: "tools.example.com", ok: true, want: "https://tools.example.com/v1"}, + {host: "2001:db8::1", ok: true, want: "https://[2001:db8::1]/v1"}, + {host: "[2001:db8::1]", ok: true, want: "https://[2001:db8::1]/v1"}, + {host: "evil.example/", ok: false}, + {host: "evil.example/../other", ok: false}, + {host: "user@evil.example", ok: false}, + {host: "evil.example?x=1", ok: false}, + {host: "evil.example#frag", ok: false}, + {host: "evil example", ok: false}, + {host: "evil.example:443:8443", ok: false}, + {host: "[not-an-ip]", ok: false}, + {host: "evil]example", ok: false}, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + t.Parallel() + got, err := TargetURL(&middlewarev1.HttpRequestTarget{Scheme: "https", Host: tt.host, Port: 443, Method: "GET", Path: "/v1"}) + if (err == nil) != tt.ok { + t.Fatalf("TargetURL(%q) error = %v, want ok=%v", tt.host, err, tt.ok) + } + if tt.ok && got != tt.want { + t.Fatalf("TargetURL(%q) = %q, want %q", tt.host, got, tt.want) + } + }) + } +} diff --git a/internal/pemkeys/pemkeys.go b/internal/pemkeys/pemkeys.go new file mode 100644 index 0000000..d35aff8 --- /dev/null +++ b/internal/pemkeys/pemkeys.go @@ -0,0 +1,65 @@ +// Package pemkeys parses the PEM-encoded key material the extension loads from +// files or mounted secrets: P-256 ECDSA private keys and X.509 certificates. +package pemkeys + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" +) + +// PEM block types accepted by this package. +const ( + TypePKCS8 = "PRIVATE KEY" + TypeSEC1 = "EC PRIVATE KEY" + TypeCertificate = "CERTIFICATE" +) + +// ParseP256PrivateKey decodes a PKCS#8 or SEC1 PEM block holding an ECDSA key +// on P-256, the only curve ANS identity certificates use. +func ParseP256PrivateKey(b []byte) (*ecdsa.PrivateKey, error) { + block, _ := pem.Decode(b) + if block == nil { + return nil, errors.New("no PEM block found") + } + var key any + var err error + switch block.Type { + case TypePKCS8: + key, err = x509.ParsePKCS8PrivateKey(block.Bytes) + case TypeSEC1: + key, err = x509.ParseECPrivateKey(block.Bytes) + default: + return nil, fmt.Errorf("unsupported PEM block %q (want %q or %q)", block.Type, TypePKCS8, TypeSEC1) + } + if err != nil { + return nil, fmt.Errorf("parse %s: %w", block.Type, err) + } + ec, ok := key.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("key is %T, want an ECDSA P-256 key", key) + } + if ec.Curve != elliptic.P256() { + return nil, fmt.Errorf("key curve is %s, want P-256", ec.Curve.Params().Name) + } + return ec, nil +} + +// ParseCertificate decodes a CERTIFICATE PEM block. +func ParseCertificate(b []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(b) + if block == nil { + return nil, errors.New("no PEM block found") + } + if block.Type != TypeCertificate { + return nil, fmt.Errorf("unsupported PEM block %q (want %q)", block.Type, TypeCertificate) + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse certificate: %w", err) + } + return cert, nil +} diff --git a/internal/pemkeys/pemkeys_test.go b/internal/pemkeys/pemkeys_test.go new file mode 100644 index 0000000..4cc0be4 --- /dev/null +++ b/internal/pemkeys/pemkeys_test.go @@ -0,0 +1,108 @@ +package pemkeys + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "strings" + "testing" + "time" +) + +func mustKey(t *testing.T, curve elliptic.Curve) *ecdsa.PrivateKey { + t.Helper() + k, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatal(err) + } + return k +} + +func pkcs8(t *testing.T, key any) []byte { + t.Helper() + der, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: TypePKCS8, Bytes: der}) +} + +func TestParseP256PrivateKey(t *testing.T) { + t.Parallel() + p256 := mustKey(t, elliptic.P256()) + sec1, err := x509.MarshalECPrivateKey(p256) + if err != nil { + t.Fatal(err) + } + _, edKey, _ := ed25519.GenerateKey(rand.Reader) + tests := []struct { + name string + in []byte + wantErr string + }{ + {name: "pkcs8", in: pkcs8(t, p256)}, + {name: "sec1", in: pem.EncodeToMemory(&pem.Block{Type: TypeSEC1, Bytes: sec1})}, + {name: "not pem", in: []byte("nope"), wantErr: "no PEM block"}, + {name: "unsupported block", in: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: []byte{1}}), wantErr: "unsupported PEM block"}, + {name: "pkcs8 garbage", in: pem.EncodeToMemory(&pem.Block{Type: TypePKCS8, Bytes: []byte{1, 2}}), wantErr: "parse PRIVATE KEY"}, + {name: "sec1 garbage", in: pem.EncodeToMemory(&pem.Block{Type: TypeSEC1, Bytes: []byte{1, 2}}), wantErr: "parse EC PRIVATE KEY"}, + {name: "not ecdsa", in: pkcs8(t, edKey), wantErr: "want an ECDSA P-256 key"}, + {name: "wrong curve", in: pkcs8(t, mustKey(t, elliptic.P384())), wantErr: "want P-256"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + key, err := ParseP256PrivateKey(tt.in) + if tt.wantErr == "" { + if err != nil || key == nil { + t.Fatalf("ParseP256PrivateKey() = %v, %v", key, err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("ParseP256PrivateKey() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +func TestParseCertificate(t *testing.T) { + t.Parallel() + key := mustKey(t, elliptic.P256()) + tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "t"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)} + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + in []byte + wantErr string + }{ + {name: "ok", in: pem.EncodeToMemory(&pem.Block{Type: TypeCertificate, Bytes: der})}, + {name: "not pem", in: []byte("x"), wantErr: "no PEM block"}, + {name: "wrong block", in: pem.EncodeToMemory(&pem.Block{Type: TypePKCS8, Bytes: der}), wantErr: "unsupported PEM block"}, + {name: "garbage", in: pem.EncodeToMemory(&pem.Block{Type: TypeCertificate, Bytes: []byte{1}}), wantErr: "parse certificate"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cert, err := ParseCertificate(tt.in) + if tt.wantErr == "" { + if err != nil || cert == nil { + t.Fatalf("ParseCertificate() = %v, %v", cert, err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("ParseCertificate() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} diff --git a/internal/testfixtures/identity.go b/internal/testfixtures/identity.go new file mode 100644 index 0000000..0c646cd --- /dev/null +++ b/internal/testfixtures/identity.go @@ -0,0 +1,97 @@ +package testfixtures + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "io" + "math/big" + "net/url" + "time" +) + +const ( + certValidBefore = time.Hour + certValidAfter = 24 * time.Hour +) + +// Identity is a registered agent's identity material: a P-256 key, the +// self-signed identity certificate carrying the agent's ans:// URI SAN, and the +// registration facts a status token vouches for. Trust in the certificate comes +// from the status token's fingerprint, not from a chain. +type Identity struct { + Key *ecdsa.PrivateKey + CertDER []byte + CertPEM []byte + KeyPEM []byte + Fingerprint [sha256.Size]byte + AgentID string + AnsName string +} + +// NewIdentity mints an identity for agentID registered under ansName with a +// certificate valid from one hour ago until 24 hours from now. +func NewIdentity(agentID, ansName string) (*Identity, error) { + now := time.Now() + return NewIdentityValid(agentID, ansName, now.Add(-certValidBefore), now.Add(certValidAfter)) +} + +// MustNewIdentity is NewIdentity that panics on error. +func MustNewIdentity(agentID, ansName string) *Identity { + id, err := NewIdentity(agentID, ansName) + if err != nil { + panic(err) + } + return id +} + +// NewIdentityValid is NewIdentity with an explicit certificate validity period. +// ansName becomes the certificate's URI SAN verbatim, so a test can mint a +// certificate whose SAN disagrees with the registration. +func NewIdentityValid(agentID, ansName string, notBefore, notAfter time.Time) (*Identity, error) { + return newIdentityValid(rand.Reader, agentID, ansName, notBefore, notAfter) +} + +func newIdentityValid(random io.Reader, agentID, ansName string, notBefore, notAfter time.Time) (*Identity, error) { + if agentID == "" { + return nil, errors.New("agent id must not be empty") + } + san, err := url.Parse(ansName) + if err != nil { + return nil, fmt.Errorf("parse ans name: %w", err) + } + key, err := newP256Key(random) + if err != nil { + return nil, err + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: agentID}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + URIs: []*url.URL{san}, + } + der, err := x509.CreateCertificate(random, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + return nil, fmt.Errorf("create identity certificate: %w", err) + } + keyPEM, err := PrivateKeyPEM(key) + if err != nil { + return nil, err + } + return &Identity{ + Key: key, + CertDER: der, + CertPEM: CertificatePEM(der), + KeyPEM: keyPEM, + Fingerprint: sha256.Sum256(der), + AgentID: agentID, + AnsName: ansName, + }, nil +} diff --git a/internal/testfixtures/keys.go b/internal/testfixtures/keys.go new file mode 100644 index 0000000..53370d8 --- /dev/null +++ b/internal/testfixtures/keys.go @@ -0,0 +1,70 @@ +// Package testfixtures mints the material an ANS deployment normally receives +// from a registration authority and a transparency log: P-256 identity keys and +// certificates, status tokens, receipts and root keys, plus a fake +// transparency-log HTTP server. It is a non-test package so the repository's +// tests and demo programs can share it. Constructors return errors and have +// Must variants that panic; minting functions panic only when the random +// source or an encoder fails on data the package produced itself, which no +// input can provoke. +package testfixtures + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" + "io" +) + +const ( + pemTypePKCS8 = "PRIVATE KEY" + pemTypeCertificate = "CERTIFICATE" + kidLen = 4 +) + +// NewP256Key generates a fresh ECDSA P-256 private key. +func NewP256Key() (*ecdsa.PrivateKey, error) { + return newP256Key(rand.Reader) +} + +func newP256Key(random io.Reader) (*ecdsa.PrivateKey, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), random) + if err != nil { + return nil, fmt.Errorf("generate P-256 key: %w", err) + } + return key, nil +} + +// PrivateKeyPEM encodes key as a PKCS#8 "PRIVATE KEY" PEM block. +func PrivateKeyPEM(key *ecdsa.PrivateKey) ([]byte, error) { + der, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, fmt.Errorf("marshal PKCS#8 key: %w", err) + } + return pem.EncodeToMemory(&pem.Block{Type: pemTypePKCS8, Bytes: der}), nil +} + +// CertificatePEM encodes a DER certificate as a "CERTIFICATE" PEM block. +func CertificatePEM(der []byte) []byte { + return pem.EncodeToMemory(&pem.Block{Type: pemTypeCertificate, Bytes: der}) +} + +// kidFor derives the SDK's 4-byte key identifier, SHA-256(SPKI)[:4], from a +// DER-encoded SubjectPublicKeyInfo. +func kidFor(spkiDER []byte) [kidLen]byte { + sum := sha256.Sum256(spkiDER) + var kid [kidLen]byte + copy(kid[:], sum[:kidLen]) + return kid +} + +// must panics on an error from encoding or signing data this package built +// itself; such a failure is a broken library, not bad input. +func must(err error) { + if err != nil { + panic(fmt.Sprintf("testfixtures: %v", err)) + } +} diff --git a/internal/testfixtures/server.go b/internal/testfixtures/server.go new file mode 100644 index 0000000..6d4f84b --- /dev/null +++ b/internal/testfixtures/server.go @@ -0,0 +1,60 @@ +package testfixtures + +import ( + "encoding/base64" + "io" + "net/http" + "net/http/httptest" +) + +const contentTypeCOSE = "application/cose" + +// FakeTLServer serves the transparency-log routes a scitt.HTTPClient reads for +// the given identities: GET /v1/agents/{agentId}/receipt, +// GET /v1/agents/{agentId}/status-token (a fresh ACTIVE token per call) and +// GET /root-keys (newline-delimited C2SP). Unknown agents receive 404. The +// SDK's HeaderSupplier and RefreshableKeyStore read only these three routes; +// /checkpoint is not served. The caller closes the server. +func FakeTLServer(tl *TL, idents ...*Identity) *httptest.Server { + byID := make(map[string]*Identity, len(idents)) + for _, id := range idents { + byID[id.AgentID] = id + } + lookup := func(w http.ResponseWriter, r *http.Request) (*Identity, bool) { + id, ok := byID[r.PathValue("agentId")] + if !ok { + http.NotFound(w, r) + } + return id, ok + } + mux := http.NewServeMux() + mux.HandleFunc("GET /v1/agents/{agentId}/receipt", func(w http.ResponseWriter, r *http.Request) { + if id, ok := lookup(w, r); ok { + writeCOSE(w, tl.Receipt(id.AgentID, id.AnsName)) + } + }) + mux.HandleFunc("GET /v1/agents/{agentId}/status-token", func(w http.ResponseWriter, r *http.Request) { + if id, ok := lookup(w, r); ok { + writeCOSE(w, tl.FreshStatusToken(id)) + } + }) + mux.HandleFunc("GET /root-keys", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + _, _ = io.WriteString(w, tl.RootKeyC2SP()+"\n") + }) + return httptest.NewServer(mux) +} + +func writeCOSE(w http.ResponseWriter, b []byte) { + w.Header().Set("Content-Type", contentTypeCOSE) + _, _ = w.Write(b) //nolint:gosec // G705: b is a COSE_Sign1 artifact the fixture minted, served as application/cose +} + +// StaticArtifacts mints a receipt and a fresh ACTIVE status token for id and +// returns them base64-encoded, receipt first, as they travel in the +// X-SCITT-Receipt and X-ANS-Status-Token headers. It serves tests and demos +// that pin artifacts instead of running a scitt.HeaderSupplier. +func StaticArtifacts(tl *TL, id *Identity) (string, string) { + return base64.StdEncoding.EncodeToString(tl.Receipt(id.AgentID, id.AnsName)), + base64.StdEncoding.EncodeToString(tl.FreshStatusToken(id)) +} diff --git a/internal/testfixtures/testfixtures_test.go b/internal/testfixtures/testfixtures_test.go new file mode 100644 index 0000000..26466c0 --- /dev/null +++ b/internal/testfixtures/testfixtures_test.go @@ -0,0 +1,385 @@ +package testfixtures + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "io" + "net/http" + "os" + "strings" + "testing" + "testing/iotest" + "time" + + "github.com/agentnameservice/ans-sdk-go/pop" + "github.com/agentnameservice/ans-sdk-go/verify/scitt" +) + +const ( + testAgentID = "7a4b2e91-83f6-4c12-9d58-bf1e6a3c9d07" + testAnsName = "ans://v1.0.0.payments.example.com" +) + +func TestKeysAndPEM(t *testing.T) { + t.Parallel() + key, err := NewP256Key() + if err != nil { + t.Fatalf("NewP256Key: %v", err) + } + if key.Curve != elliptic.P256() { + t.Fatalf("curve = %v, want P-256", key.Curve.Params().Name) + } + keyPEM, err := PrivateKeyPEM(key) + if err != nil { + t.Fatalf("PrivateKeyPEM: %v", err) + } + block, rest := pem.Decode(keyPEM) + if block == nil || block.Type != pemTypePKCS8 || len(rest) != 0 { + t.Fatalf("PrivateKeyPEM block = %+v, rest = %d bytes", block, len(rest)) + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + t.Fatalf("ParsePKCS8PrivateKey: %v", err) + } + if ec, ok := parsed.(*ecdsa.PrivateKey); !ok || !ec.Equal(key) { + t.Fatalf("parsed key %T does not equal the generated key", parsed) + } + certBlock, _ := pem.Decode(CertificatePEM([]byte{1, 2, 3})) + if certBlock == nil || certBlock.Type != pemTypeCertificate || string(certBlock.Bytes) != "\x01\x02\x03" { + t.Fatalf("CertificatePEM block = %+v", certBlock) + } + if _, err := PrivateKeyPEM(&ecdsa.PrivateKey{}); err == nil { + t.Fatal("PrivateKeyPEM accepted a key without a curve") + } +} + +func TestRandomSourceFailures(t *testing.T) { + // Go 1.26 requires this test-only switch to use injected entropy readers. + t.Setenv("GODEBUG", os.Getenv("GODEBUG")+",cryptocustomrand=1") + broken := iotest.ErrReader(errors.New("entropy exhausted")) + if _, err := newP256Key(broken); err == nil { + t.Fatal("newP256Key succeeded without entropy") + } + if _, err := newTL(broken); err == nil { + t.Fatal("newTL succeeded without entropy") + } + now := time.Now() + if _, err := newIdentityValid(broken, testAgentID, testAnsName, now, now.Add(time.Hour)); err == nil { + t.Fatal("newIdentityValid succeeded without entropy") + } +} + +func TestMustPanicsOnError(t *testing.T) { + t.Parallel() + must(nil) + defer func() { + if recover() == nil { + t.Fatal("must(err) did not panic") + } + }() + must(errors.New("boom")) +} + +func TestNewIdentity(t *testing.T) { + t.Parallel() + id := MustNewIdentity(testAgentID, testAnsName) + if id.AgentID != testAgentID || id.AnsName != testAnsName { + t.Fatalf("identity facts = %q %q", id.AgentID, id.AnsName) + } + if id.Fingerprint != sha256.Sum256(id.CertDER) { + t.Fatal("Fingerprint is not SHA-256 of CertDER") + } + cert, err := x509.ParseCertificate(id.CertDER) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + if len(cert.URIs) != 1 || cert.URIs[0].String() != testAnsName { + t.Fatalf("cert URIs = %v, want [%s]", cert.URIs, testAnsName) + } + now := time.Now() + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + t.Fatalf("certificate validity %v..%v does not contain now", cert.NotBefore, cert.NotAfter) + } + pub, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok || !pub.Equal(&id.Key.PublicKey) { + t.Fatal("certificate public key does not match Key") + } + certBlock, _ := pem.Decode(id.CertPEM) + if certBlock == nil || string(certBlock.Bytes) != string(id.CertDER) { + t.Fatal("CertPEM does not wrap CertDER") + } + keyBlock, _ := pem.Decode(id.KeyPEM) + if keyBlock == nil || keyBlock.Type != pemTypePKCS8 { + t.Fatalf("KeyPEM block = %+v", keyBlock) + } + if _, err := pop.NewSigner(id.Key, id.CertDER); err != nil { + t.Fatalf("pop.NewSigner rejects the identity: %v", err) + } +} + +func TestNewIdentityValidErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + agentID string + ansName string + }{ + {name: "empty agent id", agentID: "", ansName: testAnsName}, + {name: "unparseable ans name", agentID: testAgentID, ansName: "ans://bad\x7fname"}, + {name: "non-ASCII opaque SAN is not an IA5String", agentID: testAgentID, ansName: "urn:ans:ex\u00e4mple"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if _, err := NewIdentity(tt.agentID, tt.ansName); err == nil { + t.Fatal("NewIdentity() error = nil, want error") + } + }) + } +} + +func TestMustPanics(t *testing.T) { + t.Parallel() + defer func() { + if recover() == nil { + t.Fatal("MustNewIdentity did not panic on an invalid identity") + } + }() + MustNewIdentity("", testAnsName) +} + +func TestMustCBORPanics(t *testing.T) { + t.Parallel() + defer func() { + if recover() == nil { + t.Fatal("mustCBOR did not panic on an unencodable value") + } + }() + mustCBOR(make(chan int)) +} + +func TestTLRootKeyRoundTrip(t *testing.T) { + t.Parallel() + tl := MustNewTL() + tk, err := scitt.ParseC2SPKey(tl.RootKeyC2SP()) + if err != nil { + t.Fatalf("ParseC2SPKey(%q): %v", tl.RootKeyC2SP(), err) + } + if tk.Name != TLName { + t.Fatalf("key name = %q, want %q", tk.Name, TLName) + } + if !tk.Key.Equal(&tl.key.PublicKey) || tk.Kid != tl.kid { + t.Fatal("parsed root key does not match the log key") + } + got, err := tl.KeyStore().Get(tl.kid) + if err != nil || got.Name != TLName { + t.Fatalf("KeyStore().Get() = %+v, %v", got, err) + } + if tl.KeyStore().Len() != 1 { + t.Fatalf("KeyStore().Len() = %d, want 1", tl.KeyStore().Len()) + } +} + +func TestTLArtifactsVerify(t *testing.T) { + t.Parallel() + tl := MustNewTL() + id := MustNewIdentity(testAgentID, testAnsName) + now := time.Now().Unix() + + token := tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, now-60, now+3600, id.Fingerprint) + st, err := scitt.VerifyStatusTokenAt(token, tl.KeyStore(), 0, now) + if err != nil { + t.Fatalf("VerifyStatusTokenAt: %v", err) + } + if st.Payload.AgentID != testAgentID || st.Payload.AnsName != testAnsName || st.Payload.Status != scitt.StatusActive { + t.Fatalf("payload = %+v", st.Payload) + } + if st.Payload.Iat != now-60 || st.Payload.Exp != now+3600 { + t.Fatalf("iat/exp = %d/%d", st.Payload.Iat, st.Payload.Exp) + } + if !scitt.MatchesIdentityCert(&st.Payload, id.Fingerprint) { + t.Fatal("status token does not vouch the identity certificate") + } + if len(st.Payload.ValidIdentityCerts) != 1 || st.Payload.ValidIdentityCerts[0].CertType != scitt.CertTypeX509OVClient { + t.Fatalf("ValidIdentityCerts = %+v", st.Payload.ValidIdentityCerts) + } + + if _, expiredErr := scitt.VerifyStatusTokenAt(token, tl.KeyStore(), 0, now+7200); expiredErr == nil { + t.Fatal("expired token verified") + } + revoked := tl.StatusToken(id.AgentID, id.AnsName, scitt.StatusRevoked, now-60, now+3600, id.Fingerprint) + if _, revokedErr := scitt.VerifyStatusTokenAt(revoked, tl.KeyStore(), 0, now); revokedErr == nil { + t.Fatal("revoked token verified") + } + other := MustNewTL() + if _, foreignErr := scitt.VerifyStatusTokenAt(token, other.KeyStore(), 0, now); foreignErr == nil { + t.Fatal("token verified under a different log key") + } + + receipt := tl.Receipt(id.AgentID, id.AnsName) + rcpt, err := scitt.VerifyReceipt(receipt, tl.KeyStore()) + if err != nil { + t.Fatalf("VerifyReceipt: %v", err) + } + if rcpt.TreeSize != 1 || rcpt.LeafIndex != 0 { + t.Fatalf("receipt tree = %d/%d", rcpt.TreeSize, rcpt.LeafIndex) + } + var leaf struct { + Payload struct { + Producer struct { + Event map[string]string `json:"event"` + } `json:"producer"` + } `json:"payload"` + SchemaVersion string `json:"schemaVersion"` + } + if err := json.Unmarshal(rcpt.EventBytes, &leaf); err != nil { + t.Fatalf("leaf JSON: %v", err) + } + ev := leaf.Payload.Producer.Event + if leaf.SchemaVersion != "V1" || ev["ansId"] != testAgentID || ev["ansName"] != testAnsName { + t.Fatalf("leaf = %s", rcpt.EventBytes) + } + + fresh := tl.FreshStatusToken(id) + fst, err := scitt.VerifyStatusTokenAt(fresh, tl.KeyStore(), 0, now) + if err != nil { + t.Fatalf("FreshStatusToken does not verify: %v", err) + } + if ttl := time.Duration(fst.Payload.Exp-now) * time.Second; ttl < StatusTokenTTL-time.Minute || ttl > StatusTokenTTL+time.Minute { + t.Fatalf("fresh token ttl = %v, want about %v", ttl, StatusTokenTTL) + } +} + +func TestFixturesSatisfyVerifyCaller(t *testing.T) { + t.Parallel() + tl := MustNewTL() + id := MustNewIdentity(testAgentID, testAnsName) + signer, err := pop.NewSigner(id.Key, id.CertDER) + if err != nil { + t.Fatalf("NewSigner: %v", err) + } + const method, target = http.MethodPost, "https://tools.example.com/v1/do" + body := []byte(`{"q":1}`) + proof, err := signer.Sign(t.Context(), method, target, pop.WithContent(body)) + if err != nil { + t.Fatalf("Sign: %v", err) + } + replay := pop.NewMemoryReplayCache(t.Context(), 0) + defer replay.Close() + hdrs := &scitt.Headers{Receipt: tl.Receipt(id.AgentID, id.AnsName), StatusToken: tl.FreshStatusToken(id)} + caller, err := pop.VerifyCaller(t.Context(), proof, hdrs, method, target, tl.KeyStore(), replay, + pop.WithVerifyOptions(pop.WithReceivedContent(func() ([]byte, error) { return body, nil }))) + if err != nil { + t.Fatalf("VerifyCaller: %v", err) + } + if caller.AnsName != testAnsName || caller.AgentID != testAgentID || caller.Fingerprint != id.Fingerprint { + t.Fatalf("caller = %+v", caller) + } +} + +func TestFakeTLServer(t *testing.T) { + t.Parallel() + tl := MustNewTL() + id := MustNewIdentity(testAgentID, testAnsName) + srv := FakeTLServer(tl, id) + defer srv.Close() + + get := func(path string) (int, string, []byte) { + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL+path, nil) + if err != nil { + t.Fatalf("new request: %v", err) + } + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return resp.StatusCode, resp.Header.Get("Content-Type"), b + } + + if code, _, body := get("/root-keys"); code != http.StatusOK || string(body) != tl.RootKeyC2SP()+"\n" { + t.Fatalf("/root-keys = %d %q", code, body) + } + code, ctype, receipt := get("/v1/agents/" + testAgentID + "/receipt") + if code != http.StatusOK || ctype != contentTypeCOSE { + t.Fatalf("receipt = %d %q", code, ctype) + } + if _, err := scitt.VerifyReceipt(receipt, tl.KeyStore()); err != nil { + t.Fatalf("served receipt does not verify: %v", err) + } + code, _, token := get("/v1/agents/" + testAgentID + "/status-token") + if code != http.StatusOK { + t.Fatalf("status-token = %d", code) + } + if _, err := scitt.VerifyStatusToken(token, tl.KeyStore(), 0); err != nil { + t.Fatalf("served status token does not verify: %v", err) + } + for _, path := range []string{"/v1/agents/unknown/receipt", "/v1/agents/unknown/status-token", "/checkpoint"} { + if code, _, _ := get(path); code != http.StatusNotFound { + t.Errorf("GET %s = %d, want 404", path, code) + } + } +} + +func TestFakeTLServerFeedsHeaderSupplier(t *testing.T) { + t.Parallel() + tl := MustNewTL() + id := MustNewIdentity(testAgentID, testAnsName) + srv := FakeTLServer(tl, id) + defer srv.Close() + + client, err := scitt.NewHTTPClient(srv.URL, scitt.WithAllowInsecureTransport()) + if err != nil { + t.Fatalf("NewHTTPClient: %v", err) + } + keys := scitt.NewRefreshableKeyStore(tl.KeyStore(), client) + if err := keys.DoRefresh(t.Context()); err != nil { + t.Fatalf("DoRefresh against /root-keys: %v", err) + } + if keys.Len() != 1 { + t.Fatalf("keys after refresh = %d, want 1", keys.Len()) + } + supplier := scitt.NewHeaderSupplier(id.AgentID, client, keys, scitt.WithInitTimeout(5*time.Second)) + hdrs := supplier.CurrentHeaders() + if hdrs.ReceiptBase64 == "" || hdrs.StatusTokenBase64 == "" { + t.Fatalf("CurrentHeaders() = %v, want both headers; last error %v", hdrs, supplier.LastError()) + } + if !supplier.Healthy() { + t.Fatalf("supplier unhealthy: %v", supplier.LastError()) + } + if err := supplier.RefreshNow(context.WithoutCancel(t.Context())); err != nil { + t.Fatalf("RefreshNow: %v", err) + } +} + +func TestStaticArtifacts(t *testing.T) { + t.Parallel() + tl := MustNewTL() + id := MustNewIdentity(testAgentID, testAnsName) + receiptB64, tokenB64 := StaticArtifacts(tl, id) + receipt, err := base64.StdEncoding.DecodeString(receiptB64) + if err != nil { + t.Fatalf("receipt is not standard base64: %v", err) + } + token, err := base64.StdEncoding.DecodeString(tokenB64) + if err != nil { + t.Fatalf("token is not standard base64: %v", err) + } + if _, err := scitt.VerifyReceipt(receipt, tl.KeyStore()); err != nil { + t.Fatalf("receipt: %v", err) + } + if _, err := scitt.VerifyStatusToken(token, tl.KeyStore(), 0); err != nil { + t.Fatalf("token: %v", err) + } + if strings.ContainsAny(receiptB64+tokenB64, "-_") { + t.Fatal("artifacts must use the standard base64 alphabet") + } +} diff --git a/internal/testfixtures/tl.go b/internal/testfixtures/tl.go new file mode 100644 index 0000000..4f8049a --- /dev/null +++ b/internal/testfixtures/tl.go @@ -0,0 +1,187 @@ +package testfixtures + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "time" + + "github.com/agentnameservice/ans-sdk-go/verify/scitt" + "github.com/fxamacker/cbor/v2" +) + +// TLName is the signer name the fixture log's C2SP root key carries. +const TLName = "ans-openshell-test-log" + +// StatusTokenTTL is the lifetime of the status tokens FreshStatusToken mints. +const StatusTokenTTL = time.Hour + +// COSE labels and values of the artifacts a transparency log signs (RFC 9052 +// headers, RFC 9162 verifiable data structure, SCITT receipt proofs). +const ( + coseLabelAlg = 1 + coseLabelKid = 4 + coseLabelVDS = 395 + coseLabelVDP = 396 + coseAlgES256 = -7 + coseSign1Tag = 18 + vdsRFC9162 = 1 + vdpTreeSize = -1 + vdpLeafIndex = -2 + p1363HalfLen = 32 + p1363SigLen = 2 * p1363HalfLen + statusTokenBackoff = time.Minute +) + +// Status token payload claims (CBOR integer keys). +const ( + claimAgentID = 1 + claimStatus = 2 + claimIat = 3 + claimExp = 4 + claimAnsName = 5 + claimIdentityCerts = 6 +) + +// TL is a fixture transparency log: one ES256 signing key, published as a C2SP +// root key a verifier pins, and the minting side of the artifacts the log +// issues to its agents. +type TL struct { + key *ecdsa.PrivateKey + kid [kidLen]byte + rootKey string + keyStore *scitt.KeyStore +} + +// NewTL creates a transparency log with a fresh signing key. +func NewTL() (*TL, error) { + return newTL(rand.Reader) +} + +func newTL(random io.Reader) (*TL, error) { + key, err := newP256Key(random) + if err != nil { + return nil, err + } + spki, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + must(err) + kid := kidFor(spki) + rootKey := TLName + "+" + hex.EncodeToString(kid[:]) + "+" + base64.StdEncoding.EncodeToString(spki) + keyStore, err := scitt.NewKeyStore([]string{rootKey}) + must(err) + return &TL{key: key, kid: kid, rootKey: rootKey, keyStore: keyStore}, nil +} + +// MustNewTL is NewTL that panics on error. +func MustNewTL() *TL { + tl, err := NewTL() + if err != nil { + panic(err) + } + return tl +} + +// RootKeyC2SP returns the log's public key in the C2SP form +// "name+hexkid+base64(SPKI)" that scitt.ParseC2SPKey and the /root-keys +// endpoint use. +func (t *TL) RootKeyC2SP() string { return t.rootKey } + +// KeyStore returns a store trusting only this log's key. It implements +// scitt.KeyLookup, so it serves pop.VerifyCaller and pop.Middleware directly. +func (t *TL) KeyStore() *scitt.KeyStore { return t.keyStore } + +// StatusToken mints a COSE_Sign1 status token vouching identityFP as the +// x509-ov-client identity certificate of agentID, registered under ansName, +// with the given status and validity window (Unix seconds). +func (t *TL) StatusToken(agentID, ansName string, status scitt.AgentStatus, iat, exp int64, + identityFP [sha256.Size]byte) []byte { + protected := mustCBOR(map[int64]any{coseLabelAlg: int64(coseAlgES256), coseLabelKid: t.kid[:]}) + certs := []any{map[string]any{ + "fingerprint": identityFP[:], + "cert_type": string(scitt.CertTypeX509OVClient), + }} + payload := mustCBOR(map[int64]any{ + claimAgentID: agentID, + claimStatus: string(status), + claimIat: iat, + claimExp: exp, + claimAnsName: ansName, + claimIdentityCerts: certs, + }) + return t.sign1(protected, map[int64]any{}, payload) +} + +// FreshStatusToken mints an ACTIVE status token for id issued one minute ago +// and expiring StatusTokenTTL from now. +func (t *TL) FreshStatusToken(id *Identity) []byte { + now := time.Now() + return t.StatusToken(id.AgentID, id.AnsName, scitt.StatusActive, + now.Add(-statusTokenBackoff).Unix(), now.Add(StatusTokenTTL).Unix(), id.Fingerprint) +} + +// Receipt mints a COSE_Sign1 SCITT receipt (RFC 9162 tree of one leaf) over the +// registration event of agentID under ansName, in the leaf envelope a live log +// signs. +func (t *TL) Receipt(agentID, ansName string) []byte { + protected := mustCBOR(map[int64]any{ + coseLabelAlg: int64(coseAlgES256), + coseLabelKid: t.kid[:], + coseLabelVDS: int64(vdsRFC9162), + }) + unprotected := map[int64]any{coseLabelVDP: map[int64]any{vdpTreeSize: uint64(1), vdpLeafIndex: uint64(0)}} + return t.sign1(protected, unprotected, leafJSON(agentID, ansName)) +} + +// sign1 assembles a COSE_Sign1 (CBOR tag 18) over protected and payload with a +// P1363 ECDSA signature. +func (t *TL) sign1(protected []byte, unprotected any, payload []byte) []byte { + digest, err := scitt.ComputeSigStructureDigest(protected, payload) + must(err) + r, s, err := ecdsa.Sign(rand.Reader, t.key, digest[:]) + must(err) + sig := make([]byte, p1363SigLen) + r.FillBytes(sig[:p1363HalfLen]) + s.FillBytes(sig[p1363HalfLen:]) + return mustCBOR(cbor.Tag{Number: coseSign1Tag, Content: []any{protected, unprotected, payload, sig}}) +} + +func mustCBOR(v any) []byte { + b, err := cbor.Marshal(v) + must(err) + return b +} + +// leafEnvelope is the transparency-log leaf a receipt signs: the event wrapped +// in payload.producer.event, naming the agent by ansId (schema V1). +type leafEnvelope struct { + Payload leafPayload `json:"payload"` + SchemaVersion string `json:"schemaVersion"` +} + +type leafPayload struct { + Producer leafProducer `json:"producer"` +} + +type leafProducer struct { + Event leafEvent `json:"event"` +} + +type leafEvent struct { + AnsID string `json:"ansId"` + AnsName string `json:"ansName"` +} + +func leafJSON(agentID, ansName string) []byte { + env := leafEnvelope{ + Payload: leafPayload{Producer: leafProducer{Event: leafEvent{AnsID: agentID, AnsName: ansName}}}, + SchemaVersion: "V1", + } + b, err := json.Marshal(env) + must(err) + return b +} diff --git a/internal/vetting/cache.go b/internal/vetting/cache.go new file mode 100644 index 0000000..6662792 --- /dev/null +++ b/internal/vetting/cache.go @@ -0,0 +1,63 @@ +package vetting + +import ( + "container/list" + "time" +) + +// refreshAheadDivisor places the refresh point in the last fraction of an +// entry's TTL, so a host that stays in use is re-looked-up before its verdict +// expires and the request path never sees the gap. +const refreshAheadDivisor = 10 + +type cacheEntry struct { + verdict Verdict + expiresAt time.Time + refreshAt time.Time + elem *list.Element +} + +// verdictCache is a bounded TTL map that evicts the entry written least +// recently. It is not safe for concurrent use; Service serializes access. +type verdictCache struct { + max int + entries map[string]*cacheEntry + order *list.List +} + +func newVerdictCache(maxEntries int) *verdictCache { + return &verdictCache{ + max: maxEntries, + entries: make(map[string]*cacheEntry), + order: list.New(), + } +} + +// get returns the entry for host whether or not it has expired. +func (c *verdictCache) get(host string) (*cacheEntry, bool) { + e, ok := c.entries[host] + return e, ok +} + +func (c *verdictCache) put(host string, v Verdict, now time.Time, ttl time.Duration) { + e, ok := c.entries[host] + if !ok { + e = &cacheEntry{} + e.elem = c.order.PushBack(host) + c.entries[host] = e + } else { + c.order.MoveToBack(e.elem) + } + e.verdict = v + e.expiresAt = now.Add(ttl) + e.refreshAt = now.Add(ttl - ttl/refreshAheadDivisor) + for c.order.Len() > c.max { + oldest := c.order.Front() + delete(c.entries, oldest.Value.(string)) //nolint:errcheck // list values are host keys written by put + c.order.Remove(oldest) + } +} + +func (c *verdictCache) len() int { + return len(c.entries) +} diff --git a/internal/vetting/cache_test.go b/internal/vetting/cache_test.go new file mode 100644 index 0000000..85a8f04 --- /dev/null +++ b/internal/vetting/cache_test.go @@ -0,0 +1,42 @@ +package vetting + +import ( + "testing" + "time" +) + +func TestVerdictCacheEvictsOldestWrite(t *testing.T) { + t.Parallel() + c := newVerdictCache(2) + now := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC) + ttl := 10 * time.Minute + + c.put("a", Verdict{Host: "a", Status: StatusRegistered}, now, ttl) + c.put("b", Verdict{Host: "b", Status: StatusUnregistered}, now, ttl) + c.put("c", Verdict{Host: "c", Status: StatusRevoked}, now, ttl) + if _, ok := c.get("a"); ok { + t.Fatal("a should have been evicted") + } + if c.len() != 2 { + t.Fatalf("len() = %d, want 2", c.len()) + } + + c.put("b", Verdict{Host: "b", Status: StatusRegistered}, now.Add(time.Minute), ttl) + c.put("d", Verdict{Host: "d", Status: StatusError}, now, ttl) + if _, ok := c.get("c"); ok { + t.Fatal("c should have been evicted after b was rewritten") + } + b, ok := c.get("b") + if !ok || b.verdict.Status != StatusRegistered { + t.Fatalf("get(b) = %+v, %v; want the rewritten entry", b, ok) + } + if want := now.Add(time.Minute + ttl); !b.expiresAt.Equal(want) { + t.Fatalf("expiresAt = %v, want %v", b.expiresAt, want) + } + if want := now.Add(time.Minute + ttl - time.Minute); !b.refreshAt.Equal(want) { + t.Fatalf("refreshAt = %v, want %v", b.refreshAt, want) + } + if _, ok := c.get("missing"); ok { + t.Fatal("get(missing) should miss") + } +} diff --git a/internal/vetting/helpers_test.go b/internal/vetting/helpers_test.go new file mode 100644 index 0000000..2659044 --- /dev/null +++ b/internal/vetting/helpers_test.go @@ -0,0 +1,199 @@ +package vetting + +import ( + "bytes" + "context" + "log/slog" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/models" + "github.com/agentnameservice/ans-sdk-go/verify" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + trustedTL = "tl.example.com" + testHost = "agent.example.com" +) + +type fakeClock struct { + mu sync.Mutex + now time.Time +} + +func newFakeClock() *fakeClock { + return &fakeClock{now: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)} +} + +func (c *fakeClock) Now() time.Time { + c.mu.Lock() + defer c.mu.Unlock() + return c.now +} + +func (c *fakeClock) Advance(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.now = c.now.Add(d) +} + +// countingDNS counts LookupAnsBadge calls and, when gate is set, holds them +// until the gate is closed or the lookup context ends. +type countingDNS struct { + inner verify.DNSResolver + calls atomic.Int32 + gate chan struct{} +} + +func (d *countingDNS) LookupAnsBadge(ctx context.Context, fqdn models.Fqdn) (verify.DNSLookupResult, error) { + d.calls.Add(1) + if d.gate != nil { + select { + case <-d.gate: + case <-ctx.Done(): + return verify.DNSLookupResult{}, ctx.Err() + } + } + return d.inner.LookupAnsBadge(ctx, fqdn) +} + +func (d *countingDNS) FindBadgeForVersion(ctx context.Context, fqdn models.Fqdn, version models.Version) (*verify.AnsBadgeRecord, error) { + return d.inner.FindBadgeForVersion(ctx, fqdn, version) +} + +func (d *countingDNS) FindPreferredBadge(ctx context.Context, fqdn models.Fqdn) (*verify.AnsBadgeRecord, error) { + return d.inner.FindPreferredBadge(ctx, fqdn) +} + +// countingTL records every badge URL fetched. +type countingTL struct { + inner verify.TransparencyLogClient + mu sync.Mutex + urls []string +} + +func (c *countingTL) FetchBadge(ctx context.Context, rawURL string) (*models.Badge, error) { + c.mu.Lock() + c.urls = append(c.urls, rawURL) + c.mu.Unlock() + return c.inner.FetchBadge(ctx, rawURL) +} + +func (c *countingTL) fetched() []string { + c.mu.Lock() + defer c.mu.Unlock() + return append([]string(nil), c.urls...) +} + +type scoreFunc func(ctx context.Context, agentID string) (float64, error) + +func (f scoreFunc) LowestDimension(ctx context.Context, agentID string) (float64, error) { + return f(ctx, agentID) +} + +type syncBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *syncBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *syncBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + +func badgeURL(agentID string) string { + return "https://" + trustedTL + "/v1/agents/" + agentID +} + +func badgeFor(host string, status models.BadgeStatus) *models.Badge { + b := &models.Badge{Status: status} + b.Payload.Producer.Event.ANSID = "id-" + host + b.Payload.Producer.Event.ANSName = "ans://v1.0.0." + host + b.Payload.Producer.Event.Agent.Host = host + return b +} + +func record(version, rawURL string) verify.AnsBadgeRecord { + rec := verify.AnsBadgeRecord{FormatVersion: "ans-badge1", URL: rawURL} + if version != "" { + v, err := models.ParseVersion(version) + if err != nil { + panic(err) + } + rec.Version = &v + } + return rec +} + +type harness struct { + dns *verify.MockDNSResolver + cdns *countingDNS + tl *verify.MockTransparencyLogClient + ctl *countingTL + clock *fakeClock + logs *syncBuffer + cfg config.Vetting + deps Deps +} + +func newHarness() *harness { + h := &harness{ + dns: verify.NewMockDNSResolver(), + tl: verify.NewMockTransparencyLogClient(), + clock: newFakeClock(), + logs: &syncBuffer{}, + } + h.cdns = &countingDNS{inner: h.dns} + h.ctl = &countingTL{inner: h.tl} + h.cfg = config.Vetting{ + TrustedTLDomains: []string{trustedTL}, + ExemptHosts: []string{"api.openai.com", "*.amazonaws.com"}, + } + h.deps = Deps{DNS: h.cdns, TL: h.ctl, Clock: h.clock.Now} + return h +} + +// publish puts one record for host at the trusted log with a badge of the +// given status. +func (h *harness) publish(host string, status models.BadgeStatus) { + u := badgeURL(host) + h.dns.WithRecords(host, []verify.AnsBadgeRecord{record("v1.0.0", u)}) + h.tl.WithBadge(u, badgeFor(host, status)) +} + +func (h *harness) registered(host string) { + h.publish(host, models.BadgeStatusActive) +} + +func (h *harness) service(t *testing.T) *Service { + t.Helper() + svc, err := New(h.cfg, h.deps, slog.New(slog.NewTextHandler(h.logs, nil))) + if err != nil { + t.Fatalf("New() error = %v", err) + } + t.Cleanup(svc.Close) + return svc +} + +func eventually(t *testing.T, what string, cond func() bool) { + t.Helper() + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + if cond() { + return + } + time.Sleep(time.Millisecond) + } + t.Fatalf("timed out waiting for %s", what) +} diff --git a/internal/vetting/host.go b/internal/vetting/host.go new file mode 100644 index 0000000..c9c9cbb --- /dev/null +++ b/internal/vetting/host.go @@ -0,0 +1,166 @@ +package vetting + +import ( + "errors" + "fmt" + "net" + "net/netip" + "net/url" + "slices" + "strings" + + "github.com/agentnameservice/ans-sdk-go/verify" +) + +const httpsScheme = "https" + +// normalizeName lowercases a DNS name and drops surrounding whitespace and the +// trailing root dot. +func normalizeName(raw string) string { + return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(raw)), ".") +} + +// normalizeHost turns the host a caller dialed into the cache key: a +// normalized name with any port and IPv6 brackets removed. +func normalizeHost(raw string) string { + h := strings.ToLower(strings.TrimSpace(raw)) + if hostOnly, _, err := net.SplitHostPort(h); err == nil && hostOnly != "" { + h = hostOnly + } + if strings.HasPrefix(h, "[") && strings.HasSuffix(h, "]") { + h = h[1 : len(h)-1] + } + return normalizeName(h) +} + +func isIPLiteral(host string) bool { + _, err := netip.ParseAddr(host) + return err == nil +} + +// isIPRange recognises the CIDR form OpenShell accepts in allowed_ips. +func isIPRange(host string) bool { + _, err := netip.ParsePrefix(host) + return err == nil +} + +// exemptSet holds vetting.exempt_hosts: exact names and "*.suffix" patterns. +// A suffix pattern matches subdomains only; list the apex separately to +// exempt it. +type exemptSet struct { + exact map[string]struct{} + suffixes []string +} + +func parseExemptHosts(patterns []string) (exemptSet, error) { + set := exemptSet{exact: make(map[string]struct{}, len(patterns))} + for _, raw := range patterns { + p := normalizeName(raw) + if p == "" { + return exemptSet{}, errors.New("vetting: exempt_hosts contains an empty entry") + } + suffix, wildcard := strings.CutPrefix(p, "*.") + if strings.Contains(suffix, "*") || (wildcard && suffix == "") { + return exemptSet{}, fmt.Errorf("vetting: unsupported exempt_hosts pattern %q (use a hostname or *.suffix)", raw) + } + if wildcard { + set.suffixes = append(set.suffixes, "."+suffix) + continue + } + set.exact[p] = struct{}{} + } + return set, nil +} + +func (e exemptSet) contains(host string) bool { + if _, ok := e.exact[host]; ok { + return true + } + for _, suffix := range e.suffixes { + if len(host) > len(suffix) && strings.HasSuffix(host, suffix) { + return true + } + } + return false +} + +// tlAllowlist is vetting.trusted_tl_domains. A badge URL host is trusted when +// it equals a listed domain or is a subdomain of one. +type tlAllowlist []string + +func parseTrustedDomains(domains []string) (tlAllowlist, error) { + list := make(tlAllowlist, 0, len(domains)) + for _, raw := range domains { + d := normalizeName(raw) + if d == "" || strings.ContainsAny(d, "*/:@ \t") { + return nil, fmt.Errorf("vetting: trusted_tl_domains entry %q is not a hostname", raw) + } + list = append(list, d) + } + if len(list) == 0 { + return nil, errors.New("vetting: trusted_tl_domains must name at least one transparency log") + } + return list, nil +} + +func (a tlAllowlist) trusts(hostname string) bool { + for _, d := range a { + if hostname == d || strings.HasSuffix(hostname, "."+d) { + return true + } + } + return false +} + +// filter keeps the records whose badge URL may be fetched, highest version +// first, and reports why the first rejected record was refused. +func (a tlAllowlist) filter(records []verify.AnsBadgeRecord) ([]verify.AnsBadgeRecord, string) { + kept := make([]verify.AnsBadgeRecord, 0, len(records)) + reason := "" + for _, rec := range records { + if why := a.reject(rec.URL); why != "" { + if reason == "" { + reason = why + } + continue + } + kept = append(kept, rec) + } + slices.SortStableFunc(kept, byVersionDesc) + return kept, reason +} + +// reject explains why rawURL must not be fetched, or returns "" when it names +// a trusted transparency log over HTTPS on the default port. +func (a tlAllowlist) reject(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return "badge url is malformed" + } + hostname := normalizeName(u.Hostname()) + switch { + case u.Scheme != httpsScheme: + return "badge url is not https" + case u.User != nil: + return "badge url carries userinfo" + case hostname == "": + return "badge url has no host" + case u.Port() != "" && u.Port() != "443": + return "badge url uses a non-standard port" + case !a.trusts(hostname): + return "untrusted transparency log " + hostname + } + return "" +} + +func byVersionDesc(a, b verify.AnsBadgeRecord) int { + switch { + case a.Version == nil && b.Version == nil: + return 0 + case a.Version == nil: + return 1 + case b.Version == nil: + return -1 + } + return b.Version.Compare(*a.Version) +} diff --git a/internal/vetting/host_test.go b/internal/vetting/host_test.go new file mode 100644 index 0000000..ef73cec --- /dev/null +++ b/internal/vetting/host_test.go @@ -0,0 +1,158 @@ +package vetting + +import ( + "slices" + "strings" + "testing" + + "github.com/agentnameservice/ans-sdk-go/verify" +) + +func TestNormalizeHost(t *testing.T) { + t.Parallel() + tests := []struct { + in string + want string + }{ + {"Agent.Example.COM.", testHost}, + {" agent.example.com ", testHost}, + {"agent.example.com:8443", testHost}, + {"192.0.2.1:443", "192.0.2.1"}, + {"[::1]:443", "::1"}, + {"[::1]", "::1"}, + {"::1", "::1"}, + {"", ""}, + {":443", ":443"}, + } + for _, tt := range tests { + if got := normalizeHost(tt.in); got != tt.want { + t.Errorf("normalizeHost(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestExemptSet(t *testing.T) { + t.Parallel() + set, err := parseExemptHosts([]string{"API.openai.com.", "*.Amazonaws.com"}) + if err != nil { + t.Fatalf("parseExemptHosts() error = %v", err) + } + tests := []struct { + host string + want bool + }{ + {"api.openai.com", true}, + {"openai.com", false}, + {"s3.amazonaws.com", true}, + {"a.b.amazonaws.com", true}, + {"amazonaws.com", false}, + {"notamazonaws.com", false}, + {"*.amazonaws.com", true}, + } + for _, tt := range tests { + if got := set.contains(tt.host); got != tt.want { + t.Errorf("contains(%q) = %v, want %v", tt.host, got, tt.want) + } + } +} + +func TestParseExemptHostsErrors(t *testing.T) { + t.Parallel() + for _, patterns := range [][]string{{""}, {" . "}, {"*."}, {"api.*.example.com"}, {"*.*.example.com"}, {"*"}} { + if _, err := parseExemptHosts(patterns); err == nil { + t.Errorf("parseExemptHosts(%q) expected an error", patterns) + } + } +} + +func TestParseTrustedDomains(t *testing.T) { + t.Parallel() + list, err := parseTrustedDomains([]string{" TL.Example.com. ", "logs.example.net"}) + if err != nil { + t.Fatalf("parseTrustedDomains() error = %v", err) + } + if want := (tlAllowlist{trustedTL, "logs.example.net"}); !slices.Equal(list, want) { + t.Fatalf("parseTrustedDomains() = %v, want %v", list, want) + } + for _, domains := range [][]string{nil, {}, {""}, {"https://tl.example.com"}, {"tl.example.com:443"}, {"*.example.com"}, {"tl.example.com/v1"}, {"tl example.com"}} { + if _, err := parseTrustedDomains(domains); err == nil { + t.Errorf("parseTrustedDomains(%q) expected an error", domains) + } + } +} + +func TestTLAllowlistReject(t *testing.T) { + t.Parallel() + list := tlAllowlist{trustedTL} + tests := []struct { + rawURL string + want string + }{ + {"https://tl.example.com/v1/agents/x", ""}, + {"https://TL.Example.COM./v1/agents/x", ""}, + {"https://eu.tl.example.com/v1/agents/x", ""}, + {"https://tl.example.com:443/v1/agents/x", ""}, + {"https://nottl.example.com/v1/agents/x", "untrusted transparency log nottl.example.com"}, + {"https://evil.example.net/v1/agents/x", "untrusted transparency log evil.example.net"}, + {"http://tl.example.com/v1/agents/x", "badge url is not https"}, + {"https://user@tl.example.com/v1/agents/x", "badge url carries userinfo"}, + {"https:///v1/agents/x", "badge url has no host"}, + {"https://tl.example.com:8443/v1/agents/x", "badge url uses a non-standard port"}, + {"https://tl.example.com:bad/v1/agents/x", "badge url is malformed"}, + } + for _, tt := range tests { + if got := list.reject(tt.rawURL); got != tt.want { + t.Errorf("reject(%q) = %q, want %q", tt.rawURL, got, tt.want) + } + } +} + +func TestTLAllowlistFilterOrdersByVersion(t *testing.T) { + t.Parallel() + list := tlAllowlist{trustedTL} + records := []verify.AnsBadgeRecord{ + record("", badgeURL("none")), + record("v1.2.0", badgeURL("v1.2")), + record("v2.0.0", "https://evil.example.net/v1/agents/x"), + record("", badgeURL("none2")), + record("v1.10.0", badgeURL("v1.10")), + } + kept, reason := list.filter(records) + urls := make([]string, 0, len(kept)) + for _, rec := range kept { + urls = append(urls, strings.TrimPrefix(rec.URL, badgeURL(""))) + } + if want := []string{"v1.10", "v1.2", "none", "none2"}; !slices.Equal(urls, want) { + t.Fatalf("filter() order = %v, want %v", urls, want) + } + if reason != "untrusted transparency log evil.example.net" { + t.Fatalf("filter() reason = %q", reason) + } + if _, reason := list.filter(records[:2]); reason != "" { + t.Fatalf("filter() reason = %q, want none", reason) + } +} + +func TestByVersionDesc(t *testing.T) { + t.Parallel() + v1 := record("v1.0.0", "") + v2 := record("v2.0.0", "") + none := record("", "") + tests := []struct { + name string + a, b verify.AnsBadgeRecord + want int + }{ + {name: "both versionless", a: none, b: none, want: 0}, + {name: "versionless sorts after", a: none, b: v1, want: 1}, + {name: "versioned sorts before versionless", a: v1, b: none, want: -1}, + {name: "higher sorts first", a: v2, b: v1, want: -1}, + {name: "lower sorts last", a: v1, b: v2, want: 1}, + {name: "equal", a: v1, b: v1, want: 0}, + } + for _, tt := range tests { + if got := byVersionDesc(tt.a, tt.b); got != tt.want { + t.Errorf("%s: byVersionDesc() = %d, want %d", tt.name, got, tt.want) + } + } +} diff --git a/internal/vetting/lookup.go b/internal/vetting/lookup.go new file mode 100644 index 0000000..604fcdb --- /dev/null +++ b/internal/vetting/lookup.go @@ -0,0 +1,129 @@ +package vetting + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/agentnameservice/ans-sdk-go/models" + "github.com/agentnameservice/ans-sdk-go/verify" +) + +// resolve runs one uncached lookup: DNS record, trusted-TL allowlist, badge +// status and, when configured, the Trust Index threshold. +func (s *Service) resolve(ctx context.Context, fqdn models.Fqdn) Verdict { + host := fqdn.String() + res, err := s.dns.LookupAnsBadge(ctx, fqdn) + if err != nil { + s.warn(ctx, host, "dns lookup failed", err) + return Verdict{Host: host, Status: StatusError, Detail: "dns lookup failed"} + } + if !res.Found || len(res.Records) == 0 { + return Verdict{Host: host, Status: StatusUnregistered, Detail: "no _ans-badge record"} + } + records, reason := s.trusted.filter(res.Records) + if len(records) == 0 { + return Verdict{Host: host, Status: StatusUntrustedTL, Detail: reason} + } + v := s.fetchBadges(ctx, host, records) + if v.Status != StatusRegistered || s.trustIndex == nil { + return v + } + return s.applyThreshold(ctx, v) +} + +// badgeScan accumulates what the fetched badges said when none was ACTIVE. +type badgeScan struct { + deprecated *models.Badge + terminal *models.Badge + fetchErr bool + unknown bool +} + +// fetchBadges fetches badges highest version first and stops at the first +// ACTIVE or WARNING one. A badge sealed for a different host does not count +// for this one: a record may only vouch for the name it is published under. +func (s *Service) fetchBadges(ctx context.Context, host string, records []verify.AnsBadgeRecord) Verdict { + var scan badgeScan + for _, rec := range records { + if ctx.Err() != nil { + scan.fetchErr = true + break + } + badge, err := s.tl.FetchBadge(ctx, rec.URL) + if err != nil { + s.warn(ctx, host, "badge fetch failed", err) + scan.fetchErr = true + continue + } + if normalizeName(badge.AgentHost()) != host { + continue + } + switch { + case badge.Status.IsActive(): + return badgeVerdict(host, StatusRegistered, badge) + case badge.Status.IsValidForConnection(): + if scan.deprecated == nil { + scan.deprecated = badge + } + case badge.Status.ShouldReject(): + if scan.terminal == nil { + scan.terminal = badge + } + default: + scan.unknown = true + } + } + return scan.verdict(host) +} + +func (b badgeScan) verdict(host string) Verdict { + switch { + case b.deprecated != nil: + return badgeVerdict(host, StatusRegistered, b.deprecated) + case b.fetchErr: + return Verdict{Host: host, Status: StatusError, Detail: "badge fetch failed"} + case b.unknown: + return Verdict{Host: host, Status: StatusError, Detail: "badge status unknown"} + case b.terminal != nil: + return badgeVerdict(host, StatusRevoked, b.terminal) + } + return Verdict{Host: host, Status: StatusUnregistered, Detail: "badge names a different host"} +} + +func badgeVerdict(host string, status Status, badge *models.Badge) Verdict { + return Verdict{ + Host: host, + Status: status, + AnsName: badge.AgentName(), + AgentID: badge.AgentID(), + BadgeStatus: string(badge.Status), + Detail: "badge status " + string(badge.Status), + } +} + +// applyThreshold downgrades a registered verdict whose Trust Index score is +// missing or below the minimum. +func (s *Service) applyThreshold(ctx context.Context, v Verdict) Verdict { + score, err := s.trustIndex.LowestDimension(ctx, v.AgentID) + switch { + case errors.Is(err, ErrNoTrustRecord): + v.Status = StatusBelowThreshold + v.Detail = "no trust index record" + case err != nil: + s.warn(ctx, v.Host, "trust index lookup failed", err) + v.Status = StatusError + v.Detail = "trust index unavailable" + case score < s.minTrustScore: + v.Status = StatusBelowThreshold + v.Detail = fmt.Sprintf("trust index score %.0f below minimum %.0f", score, s.minTrustScore) + } + return v +} + +func (s *Service) warn(ctx context.Context, host, msg string, err error) { + s.logger.LogAttrs(ctx, slog.LevelWarn, msg, slog.String("host", host), slog.Any("error", err)) +} + +var _ Verifier = (*Service)(nil) diff --git a/internal/vetting/lookup_security_test.go b/internal/vetting/lookup_security_test.go new file mode 100644 index 0000000..dded987 --- /dev/null +++ b/internal/vetting/lookup_security_test.go @@ -0,0 +1,173 @@ +package vetting + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/models" + "github.com/agentnameservice/ans-sdk-go/verify" +) + +type badgeClientFunc func(context.Context, string) (*models.Badge, error) + +func (f badgeClientFunc) FetchBadge(ctx context.Context, target string) (*models.Badge, error) { + return f(ctx, target) +} + +func TestBadgeScanStopsOnCancellation(t *testing.T) { + t.Parallel() + for _, afterFirst := range []bool{false, true} { + t.Run(map[bool]string{false: "before first fetch", true: "after terminal badge"}[afterFirst], func(t *testing.T) { + t.Parallel() + h := newHarness() + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + calls := 0 + h.deps.TL = badgeClientFunc(func(context.Context, string) (*models.Badge, error) { + calls++ + cancel() + return badgeFor(testHost, models.BadgeStatusRevoked), nil + }) + if !afterFirst { + cancel() + } + svc := h.service(t) + got := svc.fetchBadges(ctx, testHost, []verify.AnsBadgeRecord{ + record("v2.0.0", badgeURL("v2")), record("v1.0.0", badgeURL("v1")), + }) + wantCalls := 0 + if afterFirst { + wantCalls = 1 + } + if got.Status != StatusError || calls != wantCalls { + t.Fatalf("fetchBadges() = %+v, calls %d; want error and %d calls", got, calls, wantCalls) + } + }) + } +} + +func TestBadgeScanRespectsLookupDeadline(t *testing.T) { + t.Parallel() + h := newHarness() + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("v2.0.0", badgeURL("v2")), record("v1.0.0", badgeURL("v1")), + }) + h.deps.LookupTimeout = 10 * time.Millisecond + var calls atomic.Int32 + h.deps.TL = badgeClientFunc(func(ctx context.Context, _ string) (*models.Badge, error) { + calls.Add(1) + <-ctx.Done() + return nil, ctx.Err() + }) + svc := h.service(t) + got := svc.Lookup(t.Context(), testHost, time.Second) + if got.Status != StatusError || calls.Load() != 1 { + t.Fatalf("Lookup() = %+v, calls %d; want error and one fetch", got, calls.Load()) + } +} + +type vettingTransportFunc func(*http.Request) (*http.Response, error) + +func (f vettingTransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestHTTPVettingClientsRejectRedirects(t *testing.T) { + t.Parallel() + for _, kind := range []string{"badge", "trust index"} { + t.Run(kind, func(t *testing.T) { + t.Parallel() + for _, target := range []string{"", "https://other.example.com/forged", "http://tl.example.com/forged", "/moved", "https://tl.example.com:8443/forged"} { + t.Run(target, func(t *testing.T) { + t.Parallel() + badge, err := json.Marshal(badgeFor(testHost, models.BadgeStatusActive)) + if err != nil { + t.Fatal(err) + } + body := string(badge) + if kind == "trust index" { + body = `{"trustEvaluation":{"trustVector":{"integrity":90,"identity":90,"solvency":90,"behavior":90,"safety":90}}}` + } + var calls atomic.Int32 + transport := vettingTransportFunc(func(req *http.Request) (*http.Response, error) { + status := http.StatusOK + header := make(http.Header) + if calls.Add(1) == 1 && target != "" { + status = http.StatusFound + header.Set("Location", target) + } + return &http.Response{StatusCode: status, Header: header, Body: io.NopCloser(strings.NewReader(body)), Request: req}, nil + }) + if kind == "badge" { + assertBadgeRedirectResult(t, transport, target != "") + } else { + assertTrustIndexRedirectResult(t, transport, target != "") + } + if calls.Load() != 1 { + t.Fatalf("HTTP requests = %d, want one", calls.Load()) + } + }) + } + }) + } +} + +func assertBadgeRedirectResult(t *testing.T, transport http.RoundTripper, redirected bool) { + t.Helper() + h := newHarness() + h.registered(testHost) + h.deps.TL = newHTTPBadgeClient(&http.Client{Transport: transport}) + svc := h.service(t) + v := svc.Lookup(t.Context(), testHost, time.Second) + want := StatusRegistered + if redirected { + want = StatusError + } + if v.Status != want { + t.Fatalf("Lookup() = %+v, want %s", v, want) + } +} + +func assertTrustIndexRedirectResult(t *testing.T, transport http.RoundTripper, redirected bool) { + t.Helper() + client, err := NewHTTPTrustIndexClient("https://tl.example.com", nil) + if err != nil { + t.Fatal(err) + } + client.client.Transport = transport + score, err := client.LowestDimension(t.Context(), "agent-id") + if !redirected && (err != nil || score != 90) { + t.Fatalf("LowestDimension() = %v, %v; want 90", score, err) + } + if redirected && (err == nil || !strings.Contains(err.Error(), "unexpected status 302")) { + t.Fatalf("LowestDimension() error = %v; want rejected redirect", err) + } +} + +func TestWithoutRedirectsPreservesSuppliedClient(t *testing.T) { + t.Parallel() + wantErr := errors.New("supplied redirect policy") + original := &http.Client{ + Timeout: time.Second, + CheckRedirect: func(*http.Request, []*http.Request) error { + return wantErr + }, + } + configured := withoutRedirects(original) + if configured == original || configured.Timeout != original.Timeout { + t.Fatal("client must be copied with its timeout preserved") + } + if err := original.CheckRedirect(nil, nil); !errors.Is(err, wantErr) { + t.Fatalf("original redirect policy changed: %v", err) + } + if err := configured.CheckRedirect(nil, nil); !errors.Is(err, http.ErrUseLastResponse) { + t.Fatalf("configured redirect policy = %v, want ErrUseLastResponse", err) + } +} diff --git a/internal/vetting/review_fixes_test.go b/internal/vetting/review_fixes_test.go new file mode 100644 index 0000000..2fcce58 --- /dev/null +++ b/internal/vetting/review_fixes_test.go @@ -0,0 +1,61 @@ +package vetting + +import ( + "errors" + "testing" + "time" +) + +func TestLookupClassifiesIPRanges(t *testing.T) { + t.Parallel() + h := newHarness() + svc := h.service(t) + for _, host := range []string{"198.51.100.0/24", "2001:db8::/32"} { + v := svc.Lookup(t.Context(), host, time.Second) + if v.Status != StatusUnregistered || v.Detail != "ip range" { + t.Fatalf("Lookup(%q) = %+v, want unregistered ip range", host, v) + } + } + if h.cdns.calls.Load() != 0 { + t.Fatal("an IP range reached DNS") + } +} + +func TestRefreshFailureKeepsValidVerdict(t *testing.T) { + t.Parallel() + h := newHarness() + h.cfg.CacheTTL = 10 * time.Minute + h.registered("agent.example.com") + svc := h.service(t) + + if v := svc.Lookup(t.Context(), "agent.example.com", 5*time.Second); v.Status != StatusRegistered { + t.Fatalf("initial Lookup = %+v, want registered", v) + } + h.dns.WithError("agent.example.com", errors.New("servfail")) + h.clock.Advance(9*time.Minute + 5*time.Second) + if v := svc.Lookup(t.Context(), "agent.example.com", 0); v.Status != StatusRegistered { + t.Fatalf("Lookup inside the refresh window = %+v, want the cached registered verdict", v) + } + eventually(t, "the failed refresh to be counted", func() bool { return svc.Stats().Errors == 1 }) + lookups := svc.Stats().Lookups + h.clock.Advance(10 * time.Second) + for range 3 { + if v := svc.Lookup(t.Context(), "agent.example.com", 5*time.Second); v.Status != StatusRegistered { + t.Fatalf("Lookup after a failed refresh = %+v, want the still-valid registered verdict", v) + } + } + if got := svc.Stats().Lookups; got != lookups { + t.Fatalf("a failed refresh was retried %d times before the error TTL elapsed", got-lookups) + } + h.clock.Advance(defaultErrorTTL) + if v := svc.Lookup(t.Context(), "agent.example.com", 0); v.Status != StatusRegistered { + t.Fatalf("Lookup at the retry point = %+v, want the cached registered verdict", v) + } + if got := svc.Stats().Lookups; got != lookups+1 { + t.Fatalf("lookups after the error TTL = %d, want one retry (%d)", got, lookups+1) + } + h.clock.Advance(time.Minute) + if v := svc.Lookup(t.Context(), "agent.example.com", 5*time.Second); v.Status != StatusError { + t.Fatalf("Lookup after expiry with DNS failing = %+v, want error", v) + } +} diff --git a/internal/vetting/service.go b/internal/vetting/service.go new file mode 100644 index 0000000..4511c80 --- /dev/null +++ b/internal/vetting/service.go @@ -0,0 +1,346 @@ +package vetting + +import ( + "context" + "errors" + "log/slog" + "net/http" + "strings" + "sync" + "time" + + "github.com/agentnameservice/ans-sdk-go/models" + "github.com/agentnameservice/ans-sdk-go/verify" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +const ( + defaultCacheTTL = 10 * time.Minute + defaultErrorTTL = 30 * time.Second + defaultLookupTimeout = 5 * time.Second + defaultHTTPTimeout = 5 * time.Second + maxCacheEntries = 10_000 +) + +// Deps are the collaborators a Service uses. Zero fields take defaults: the +// SDK's system DNS resolver and HTTP transparency-log client, an HTTP Trust +// Index client for cfg.TrustIndexURL (none when it is empty), time.Now and a +// 5 s lookup timeout. TL is only ever handed URLs that passed the trusted-TL +// allowlist. +type Deps struct { + DNS verify.DNSResolver + TL verify.TransparencyLogClient + TrustIndex TrustIndexClient + Clock func() time.Time + LookupTimeout time.Duration +} + +// Stats is a snapshot of the cache and lookup counters for a health endpoint. +type Stats struct { + // Entries is the number of cached verdicts, expired ones included. + Entries int + // Hits and Misses count Lookup calls that reached the cache; exempt, IP + // literal and wildcard hosts are settled before it. + Hits uint64 + Misses uint64 + // Lookups counts background lookups started; Errors counts those that + // ended in StatusError. + Lookups uint64 + Errors uint64 +} + +// Service implements Verifier with a bounded verdict cache and single-flight +// background lookups. Lookups are bounded by Deps.LookupTimeout and survive +// cancellation of the request that started them, so a result is cached for the +// callers that follow. +type Service struct { + dns verify.DNSResolver + tl verify.TransparencyLogClient + trustIndex TrustIndexClient + clock func() time.Time + lookupTimeout time.Duration + logger *slog.Logger + + trusted tlAllowlist + exempt exemptSet + cacheTTL time.Duration + errorTTL time.Duration + minTrustScore float64 + + mu sync.Mutex + cache *verdictCache + inflight map[string]*flight + closed bool + stats Stats + + closeCtx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +// New validates cfg and builds a Service. The trusted-TL allowlist must not be +// empty: it is the trust root of the badge tier. A Trust Index is consulted +// only together with a positive min_trust_score, so one without the other is +// rejected. A nil logger discards logs. +func New(cfg config.Vetting, deps Deps, logger *slog.Logger) (*Service, error) { + trusted, err := parseTrustedDomains(cfg.TrustedTLDomains) + if err != nil { + return nil, err + } + exempt, err := parseExemptHosts(cfg.ExemptHosts) + if err != nil { + return nil, err + } + if cfg.CacheTTL < 0 || cfg.RequestBudget < 0 || cfg.MinTrustScore < 0 { + return nil, errors.New("vetting: cache_ttl, request_budget and min_trust_score must not be negative") + } + trustIndex, err := trustIndexFor(cfg, deps) + if err != nil { + return nil, err + } + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + if strings.HasPrefix(strings.ToLower(cfg.TrustIndexURL), "http://") { + logger.Warn("trust index url uses plaintext http; scores can be tampered with in transit") + } + cacheTTL := cfg.CacheTTL + if cacheTTL == 0 { + cacheTTL = defaultCacheTTL + } + if deps.DNS == nil { + deps.DNS = verify.NewStandardDNSResolver() + } + if deps.TL == nil { + deps.TL = newHTTPBadgeClient(nil) + } + if deps.Clock == nil { + deps.Clock = time.Now + } + if deps.LookupTimeout <= 0 { + deps.LookupTimeout = defaultLookupTimeout + } + closeCtx, cancel := context.WithCancel(context.Background()) + return &Service{ + dns: deps.DNS, + tl: deps.TL, + trustIndex: trustIndex, + clock: deps.Clock, + lookupTimeout: deps.LookupTimeout, + logger: logger, + trusted: trusted, + exempt: exempt, + cacheTTL: cacheTTL, + errorTTL: min(defaultErrorTTL, cacheTTL), + minTrustScore: cfg.MinTrustScore, + cache: newVerdictCache(maxCacheEntries), + inflight: make(map[string]*flight), + closeCtx: closeCtx, + cancel: cancel, + }, nil +} + +func newHTTPBadgeClient(client *http.Client) verify.TransparencyLogClient { + return verify.NewHTTPTransparencyLogClient().WithHTTPClient(withoutRedirects(client)) +} + +func withoutRedirects(client *http.Client) *http.Client { + if client == nil { + client = &http.Client{Timeout: defaultHTTPTimeout} + } + configured := *client + configured.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + return &configured +} + +func trustIndexFor(cfg config.Vetting, deps Deps) (TrustIndexClient, error) { + trustIndex := deps.TrustIndex + if trustIndex == nil && cfg.TrustIndexURL != "" { + client, err := NewHTTPTrustIndexClient(cfg.TrustIndexURL, nil) + if err != nil { + return nil, err + } + trustIndex = client + } + switch { + case cfg.MinTrustScore > 0 && trustIndex == nil: + return nil, errors.New("vetting: min_trust_score requires trust_index_url") + case cfg.MinTrustScore == 0 && trustIndex != nil: + return nil, errors.New("vetting: trust_index_url requires a positive min_trust_score") + } + return trustIndex, nil +} + +// flight is one in-progress lookup shared by every caller waiting on a host. +type flight struct { + done chan struct{} + verdict Verdict +} + +// Lookup implements Verifier. Exempt hosts, IP literals, wildcard patterns and +// names that cannot be DNS names are settled without a lookup; everything else +// is served from the cache or looked up in the background, with Lookup +// blocking up to wait (bounded by ctx) for the result. +func (s *Service) Lookup(ctx context.Context, host string, wait time.Duration) Verdict { + fqdn, v, settled := s.classify(normalizeHost(host)) + if settled { + return v + } + v, f := s.lookupCached(ctx, fqdn) + if f == nil { + return v + } + return awaitFlight(ctx, fqdn.String(), f, wait) +} + +// Prewarm starts lookups for hosts that are not cached without waiting for +// them. +func (s *Service) Prewarm(ctx context.Context, hosts ...string) { + for _, h := range hosts { + s.Lookup(ctx, h, 0) + } +} + +// Close stops starting lookups, cancels those in flight and waits for them to +// finish. Cached verdicts stay readable. +func (s *Service) Close() { + s.mu.Lock() + s.closed = true + s.mu.Unlock() + s.cancel() + s.wg.Wait() +} + +// Stats returns a snapshot of the counters. +func (s *Service) Stats() Stats { + s.mu.Lock() + defer s.mu.Unlock() + st := s.stats + st.Entries = s.cache.len() + return st +} + +func (s *Service) classify(host string) (models.Fqdn, Verdict, bool) { + switch { + case host == "": + return models.Fqdn{}, Verdict{Status: StatusError, Detail: "empty host"}, true + case s.exempt.contains(host): + return models.Fqdn{}, Verdict{Host: host, Status: StatusExempt, Detail: "exempt host"}, true + case strings.Contains(host, "*"): + return models.Fqdn{}, Verdict{Host: host, Status: StatusError, Detail: "wildcard host cannot be vetted"}, true + case isIPLiteral(host): + return models.Fqdn{}, Verdict{Host: host, Status: StatusUnregistered, Detail: "ip literal"}, true + case isIPRange(host): + return models.Fqdn{}, Verdict{Host: host, Status: StatusUnregistered, Detail: "ip range"}, true + } + fqdn, err := models.NewFqdn(host) + if err != nil { + return models.Fqdn{}, Verdict{Host: host, Status: StatusUnregistered, Detail: "invalid hostname"}, true + } + return fqdn, Verdict{}, false +} + +// lookupCached returns an unexpired cached verdict, or the flight to wait on. +// A cached verdict in the last tenth of its TTL also starts a refresh. When the +// service is closed and nothing is cached the verdict is StatusError. +func (s *Service) lookupCached(ctx context.Context, fqdn models.Fqdn) (Verdict, *flight) { + host := fqdn.String() + s.mu.Lock() + defer s.mu.Unlock() + now := s.clock() + if e, ok := s.cache.get(host); ok && now.Before(e.expiresAt) { + s.stats.Hits++ + if !now.Before(e.refreshAt) { + s.startLocked(ctx, fqdn) + } + return e.verdict, nil + } + s.stats.Misses++ + f := s.startLocked(ctx, fqdn) + if f == nil { + return Verdict{Host: host, Status: StatusError, Detail: "vetting service closed"}, nil + } + return Verdict{}, f +} + +func (s *Service) startLocked(ctx context.Context, fqdn models.Fqdn) *flight { + if s.closed { + return nil + } + host := fqdn.String() + if f, ok := s.inflight[host]; ok { + return f + } + f := &flight{done: make(chan struct{})} + s.inflight[host] = f + s.stats.Lookups++ + s.wg.Add(1) + go s.run(ctx, fqdn, f) + return f +} + +func (s *Service) run(ctx context.Context, fqdn models.Fqdn, f *flight) { + defer s.wg.Done() + lctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), s.lookupTimeout) + defer cancel() + stop := context.AfterFunc(s.closeCtx, cancel) + defer stop() + + v := s.resolve(lctx, fqdn) + published, prev, had := s.store(fqdn.String(), v) + f.verdict = published + close(f.done) + if !had || prev != published.Status { + s.logger.LogAttrs(lctx, slog.LevelInfo, "destination verdict", + slog.String("host", published.Host), slog.String("status", published.Status.String()), + slog.String("detail", published.Detail)) + } +} + +// store caches v, releases the flight and returns the verdict to publish with +// the status it replaced. A lookup that failed while a healthy verdict is still +// unexpired does not replace it, and the next refresh waits errorTTL: a +// dependency blip during refresh-ahead must not turn a registered host into a +// denial, and a failing dependency must not be retried on every cache hit. +func (s *Service) store(host string, v Verdict) (Verdict, Status, bool) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.inflight, host) + var prev Status + e, had := s.cache.get(host) + if had { + prev = e.verdict.Status + } + now := s.clock() + if v.Status == StatusError { + s.stats.Errors++ + if had && e.verdict.Status != StatusError && now.Before(e.expiresAt) { + e.refreshAt = now.Add(s.errorTTL) + return e.verdict, prev, had + } + s.cache.put(host, v, now, s.errorTTL) + return v, prev, had + } + s.cache.put(host, v, now, s.cacheTTL) + return v, prev, had +} + +func awaitFlight(ctx context.Context, host string, f *flight, wait time.Duration) Verdict { + pending := Verdict{Host: host, Status: StatusUnknown, Detail: "lookup in progress"} + if wait <= 0 { + return pending + } + timer := time.NewTimer(wait) + defer timer.Stop() + select { + case <-f.done: + return f.verdict + case <-timer.C: + return pending + case <-ctx.Done(): + return pending + } +} diff --git a/internal/vetting/service_test.go b/internal/vetting/service_test.go new file mode 100644 index 0000000..3ffd1d3 --- /dev/null +++ b/internal/vetting/service_test.go @@ -0,0 +1,773 @@ +package vetting + +import ( + "context" + "errors" + "log/slog" + "slices" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-sdk-go/models" + "github.com/agentnameservice/ans-sdk-go/verify" + + "github.com/agentnameservice/ans-openshell/internal/config" +) + +func TestNewValidation(t *testing.T) { + t.Parallel() + noScore := scoreFunc(func(context.Context, string) (float64, error) { return 0, nil }) + tests := []struct { + name string + cfg config.Vetting + deps Deps + wantErr string + }{ + {name: "minimal", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}}}, + {name: "trust index with threshold", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, TrustIndexURL: "https://ti.example.com", MinTrustScore: 50}}, + {name: "injected trust index with threshold", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, MinTrustScore: 50}, deps: Deps{TrustIndex: noScore}}, + {name: "no trusted domains", cfg: config.Vetting{}, wantErr: "trusted_tl_domains"}, + {name: "trusted domain with scheme", cfg: config.Vetting{TrustedTLDomains: []string{"https://tl.example.com"}}, wantErr: "not a hostname"}, + {name: "negative cache ttl", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, CacheTTL: -time.Second}, wantErr: "must not be negative"}, + {name: "negative request budget", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, RequestBudget: -time.Second}, wantErr: "must not be negative"}, + {name: "negative min score", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, MinTrustScore: -1}, wantErr: "must not be negative"}, + {name: "threshold without trust index", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, MinTrustScore: 50}, wantErr: "requires trust_index_url"}, + {name: "trust index without threshold", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, TrustIndexURL: "https://ti.example.com"}, wantErr: "positive min_trust_score"}, + {name: "injected trust index without threshold", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}}, deps: Deps{TrustIndex: noScore}, wantErr: "positive min_trust_score"}, + {name: "bad trust index url", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, TrustIndexURL: "ftp://ti.example.com", MinTrustScore: 50}, wantErr: "must be http(s)"}, + {name: "empty exempt entry", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, ExemptHosts: []string{""}}, wantErr: "empty entry"}, + {name: "unsupported exempt pattern", cfg: config.Vetting{TrustedTLDomains: []string{trustedTL}, ExemptHosts: []string{"api.*.example.com"}}, wantErr: "unsupported exempt_hosts pattern"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + svc, err := New(tt.cfg, tt.deps, nil) + if svc != nil { + defer svc.Close() + } + if tt.wantErr == "" { + if err != nil { + t.Fatalf("New() error = %v", err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("New() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +func TestNewDefaults(t *testing.T) { + t.Parallel() + logs := &syncBuffer{} + cfg := config.Vetting{TrustedTLDomains: []string{trustedTL}, TrustIndexURL: "http://ti.example.com", MinTrustScore: 10} + svc, err := New(cfg, Deps{}, slog.New(slog.NewTextHandler(logs, nil))) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer svc.Close() + if svc.dns == nil || svc.tl == nil || svc.trustIndex == nil || svc.clock == nil || svc.logger == nil { + t.Fatal("New() left a dependency nil") + } + if _, ok := svc.trustIndex.(*HTTPTrustIndexClient); !ok { + t.Fatalf("trustIndex = %T, want *HTTPTrustIndexClient", svc.trustIndex) + } + if svc.cacheTTL != defaultCacheTTL || svc.errorTTL != defaultErrorTTL || svc.lookupTimeout != defaultLookupTimeout { + t.Fatalf("defaults: cacheTTL %v errorTTL %v lookupTimeout %v", svc.cacheTTL, svc.errorTTL, svc.lookupTimeout) + } + if svc.clock().IsZero() { + t.Fatal("default clock returned the zero time") + } + if !strings.Contains(logs.String(), "plaintext http") { + t.Fatalf("expected a plaintext warning, got %q", logs.String()) + } + + short, err := New(config.Vetting{TrustedTLDomains: []string{trustedTL}, CacheTTL: 10 * time.Second}, Deps{LookupTimeout: time.Second}, nil) + if err != nil { + t.Fatalf("New() error = %v", err) + } + defer short.Close() + if short.errorTTL != 10*time.Second || short.lookupTimeout != time.Second { + t.Fatalf("errorTTL %v lookupTimeout %v", short.errorTTL, short.lookupTimeout) + } +} + +func TestLookupSettledWithoutLookup(t *testing.T) { + t.Parallel() + tests := []struct { + name string + host string + status Status + want string + detail string + }{ + {name: "exempt exact", host: "API.OpenAI.com.", status: StatusExempt, want: "api.openai.com", detail: "exempt host"}, + {name: "exempt exact with port", host: "api.openai.com:443", status: StatusExempt, want: "api.openai.com", detail: "exempt host"}, + {name: "exempt suffix", host: "s3.amazonaws.com", status: StatusExempt, want: "s3.amazonaws.com", detail: "exempt host"}, + {name: "exempt pattern literal", host: "*.amazonaws.com", status: StatusExempt, want: "*.amazonaws.com", detail: "exempt host"}, + {name: "ipv4", host: "192.0.2.1", status: StatusUnregistered, want: "192.0.2.1", detail: "ip literal"}, + {name: "ipv4 with port", host: "192.0.2.1:8443", status: StatusUnregistered, want: "192.0.2.1", detail: "ip literal"}, + {name: "ipv6", host: "::1", status: StatusUnregistered, want: "::1", detail: "ip literal"}, + {name: "ipv6 bracketed with port", host: "[2001:db8::1]:443", status: StatusUnregistered, want: "2001:db8::1", detail: "ip literal"}, + {name: "wildcard", host: "*.example.com", status: StatusError, want: "*.example.com", detail: "wildcard host cannot be vetted"}, + {name: "empty", host: "", status: StatusError, want: "", detail: "empty host"}, + {name: "blank", host: " ", status: StatusError, want: "", detail: "empty host"}, + {name: "underscore", host: "bad_host.example.com", status: StatusUnregistered, want: "bad_host.example.com", detail: "invalid hostname"}, + {name: "leading hyphen", host: "-bad.example.com", status: StatusUnregistered, want: "-bad.example.com", detail: "invalid hostname"}, + } + h := newHarness() + svc := h.service(t) + for _, tt := range tests { + got := svc.Lookup(t.Context(), tt.host, time.Second) + if got.Status != tt.status || got.Host != tt.want || got.Detail != tt.detail { + t.Errorf("%s: Lookup(%q) = %+v, want status %v host %q detail %q", tt.name, tt.host, got, tt.status, tt.want, tt.detail) + } + } + if calls := h.cdns.calls.Load(); calls != 0 { + t.Fatalf("DNS calls = %d, want 0", calls) + } + if st := svc.Stats(); st != (Stats{}) { + t.Fatalf("Stats() = %+v, want all zero", st) + } +} + +func TestLookupStatuses(t *testing.T) { + t.Parallel() + evilURL := "https://evil.example.net/v1/agents/x" + tests := []struct { + name string + host string + setup func(h *harness) + status Status + badgeStatus string + detail string + fetched []string + }{ + { + name: "active", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatusActive) }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "warning", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatusWarning) }, + status: StatusRegistered, + badgeStatus: "WARNING", + detail: "badge status WARNING", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "deprecated only", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatusDeprecated) }, + status: StatusRegistered, + badgeStatus: "DEPRECATED", + detail: "badge status DEPRECATED", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "deprecated newest with active sibling", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("v1.0.0", badgeURL("v1")), + record("v2.0.0", badgeURL("v2")), + }) + h.tl.WithBadge(badgeURL("v2"), badgeFor(testHost, models.BadgeStatusDeprecated)) + h.tl.WithBadge(badgeURL("v1"), badgeFor(testHost, models.BadgeStatusActive)) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL("v2"), badgeURL("v1")}, + }, + { + name: "revoked", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatusRevoked) }, + status: StatusRevoked, + badgeStatus: "REVOKED", + detail: "badge status REVOKED", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "expired", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatusExpired) }, + status: StatusRevoked, + badgeStatus: "EXPIRED", + detail: "badge status EXPIRED", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "revoked newest with active sibling", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("v2.0.0", badgeURL("v2")), + record("v1.0.0", badgeURL("v1")), + }) + h.tl.WithBadge(badgeURL("v2"), badgeFor(testHost, models.BadgeStatusRevoked)) + h.tl.WithBadge(badgeURL("v1"), badgeFor(testHost, models.BadgeStatusActive)) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL("v2"), badgeURL("v1")}, + }, + { + name: "revoked with sibling fetch error", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("v2.0.0", badgeURL("v2")), + record("v1.0.0", badgeURL("v1")), + }) + h.tl.WithBadge(badgeURL("v2"), badgeFor(testHost, models.BadgeStatusRevoked)) + h.tl.WithError(badgeURL("v1"), &verify.TlogError{Type: verify.TlogErrorServiceUnavailable}) + }, + status: StatusError, + detail: "badge fetch failed", + fetched: []string{badgeURL("v2"), badgeURL("v1")}, + }, + { + name: "unknown badge status", + setup: func(h *harness) { h.publish(testHost, models.BadgeStatus("UNKNOWN")) }, + status: StatusError, + detail: "badge status unknown", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "no record", + setup: func(*harness) {}, + status: StatusUnregistered, + detail: "no _ans-badge record", + }, + { + name: "exempt apex is looked up", + host: "amazonaws.com", + setup: func(*harness) {}, + status: StatusUnregistered, + detail: "no _ans-badge record", + }, + { + name: "dns error", + setup: func(h *harness) { + h.dns.WithError(testHost, &verify.DNSError{Type: verify.DNSErrorTimeout, Fqdn: testHost}) + }, + status: StatusError, + detail: "dns lookup failed", + }, + { + name: "badge fetch error", + setup: func(h *harness) { + u := badgeURL(testHost) + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", u)}) + h.tl.WithError(u, &verify.TlogError{Type: verify.TlogErrorServiceUnavailable, URL: u}) + }, + status: StatusError, + detail: "badge fetch failed", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "badge missing at log", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", badgeURL(testHost))}) + }, + status: StatusError, + detail: "badge fetch failed", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "badge for a different host", + setup: func(h *harness) { + u := badgeURL(testHost) + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", u)}) + h.tl.WithBadge(u, badgeFor("other.example.com", models.BadgeStatusActive)) + }, + status: StatusUnregistered, + detail: "badge names a different host", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "badge host compared case-insensitively", + setup: func(h *harness) { + u := badgeURL(testHost) + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", u)}) + b := badgeFor(testHost, models.BadgeStatusActive) + b.Payload.Producer.Event.Agent.Host = "Agent.Example.COM." + h.tl.WithBadge(u, b) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "untrusted log is never fetched", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", evilURL)}) + h.tl.WithBadge(evilURL, badgeFor(testHost, models.BadgeStatusActive)) + }, + status: StatusUntrustedTL, + detail: "untrusted transparency log evil.example.net", + }, + { + name: "untrusted record skipped when a trusted one exists", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("v2.0.0", evilURL), + record("v1.0.0", badgeURL(testHost)), + }) + h.tl.WithBadge(evilURL, badgeFor(testHost, models.BadgeStatusActive)) + h.tl.WithBadge(badgeURL(testHost), badgeFor(testHost, models.BadgeStatusActive)) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL(testHost)}, + }, + { + name: "plain http badge url", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", "http://tl.example.com/v1/agents/x")}) + }, + status: StatusUntrustedTL, + detail: "badge url is not https", + }, + { + name: "non-standard port", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", "https://tl.example.com:8443/v1/agents/x")}) + }, + status: StatusUntrustedTL, + detail: "badge url uses a non-standard port", + }, + { + name: "subdomain of a trusted log", + setup: func(h *harness) { + u := "https://EU.tl.example.com/v1/agents/x" + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{record("v1.0.0", u)}) + h.tl.WithBadge(u, badgeFor(testHost, models.BadgeStatusActive)) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{"https://EU.tl.example.com/v1/agents/x"}, + }, + { + name: "versionless record is tried last", + setup: func(h *harness) { + h.dns.WithRecords(testHost, []verify.AnsBadgeRecord{ + record("", badgeURL("latest")), + record("v1.0.0", badgeURL("v1")), + }) + h.tl.WithBadge(badgeURL("latest"), badgeFor(testHost, models.BadgeStatusActive)) + h.tl.WithBadge(badgeURL("v1"), badgeFor(testHost, models.BadgeStatusRevoked)) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL("v1"), badgeURL("latest")}, + }, + { + name: "active fifth version is reached", + setup: func(h *harness) { + records := make([]verify.AnsBadgeRecord, 0, 5) + for i := 1; i <= 5; i++ { + id := "v" + string(rune('0'+i)) + records = append(records, record(id+".0.0", badgeURL(id))) + status := models.BadgeStatusRevoked + if i == 1 { + status = models.BadgeStatusActive + } + h.tl.WithBadge(badgeURL(id), badgeFor(testHost, status)) + } + h.dns.WithRecords(testHost, records) + }, + status: StatusRegistered, + badgeStatus: "ACTIVE", + detail: "badge status ACTIVE", + fetched: []string{badgeURL("v5"), badgeURL("v4"), badgeURL("v3"), badgeURL("v2"), badgeURL("v1")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := newHarness() + tt.setup(h) + host := tt.host + if host == "" { + host = testHost + } + svc := h.service(t) + got := svc.Lookup(t.Context(), host, 5*time.Second) + if got.Host != host || got.Status != tt.status || got.BadgeStatus != tt.badgeStatus || got.Detail != tt.detail { + t.Fatalf("Lookup() = %+v, want host %q status %v badge %q detail %q", got, host, tt.status, tt.badgeStatus, tt.detail) + } + if tt.badgeStatus != "" && (got.AnsName != "ans://v1.0.0."+host || got.AgentID != "id-"+host) { + t.Fatalf("Lookup() identity = %q/%q", got.AnsName, got.AgentID) + } + if fetched := h.ctl.fetched(); !slices.Equal(fetched, tt.fetched) { + t.Fatalf("fetched %v, want %v", fetched, tt.fetched) + } + if calls := h.cdns.calls.Load(); calls != 1 { + t.Fatalf("DNS calls = %d, want 1", calls) + } + }) + } +} + +func TestLookupTrustIndex(t *testing.T) { + t.Parallel() + tests := []struct { + name string + badge models.BadgeStatus + score float64 + err error + status Status + detail string + calls int32 + }{ + {name: "above", badge: models.BadgeStatusActive, score: 80, status: StatusRegistered, detail: "badge status ACTIVE", calls: 1}, + {name: "equal", badge: models.BadgeStatusActive, score: 60, status: StatusRegistered, detail: "badge status ACTIVE", calls: 1}, + {name: "below", badge: models.BadgeStatusActive, score: 40, status: StatusBelowThreshold, detail: "trust index score 40 below minimum 60", calls: 1}, + {name: "no record", badge: models.BadgeStatusActive, err: ErrNoTrustRecord, status: StatusBelowThreshold, detail: "no trust index record", calls: 1}, + {name: "unavailable", badge: models.BadgeStatusActive, err: errors.New("boom"), status: StatusError, detail: "trust index unavailable", calls: 1}, + {name: "skipped when revoked", badge: models.BadgeStatusRevoked, status: StatusRevoked, detail: "badge status REVOKED"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := newHarness() + h.publish(testHost, tt.badge) + var calls atomic.Int32 + h.cfg.MinTrustScore = 60 + h.deps.TrustIndex = scoreFunc(func(_ context.Context, agentID string) (float64, error) { + calls.Add(1) + if agentID != "id-"+testHost { + t.Errorf("LowestDimension agentID = %q", agentID) + } + return tt.score, tt.err + }) + svc := h.service(t) + got := svc.Lookup(t.Context(), testHost, 5*time.Second) + if got.Status != tt.status || got.Detail != tt.detail || got.AgentID != "id-"+testHost { + t.Fatalf("Lookup() = %+v, want status %v detail %q", got, tt.status, tt.detail) + } + if calls.Load() != tt.calls { + t.Fatalf("LowestDimension calls = %d, want %d", calls.Load(), tt.calls) + } + }) + } +} + +func TestLookupCacheHitAndExpiry(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + svc := h.service(t) + ctx := t.Context() + + if got := svc.Lookup(ctx, testHost, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("first Lookup() = %+v", got) + } + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusRegistered { + t.Fatalf("cached Lookup() = %+v", got) + } + if st := svc.Stats(); st != (Stats{Entries: 1, Hits: 1, Misses: 1, Lookups: 1}) { + t.Fatalf("Stats() = %+v", st) + } + + h.clock.Advance(defaultCacheTTL) + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusUnknown || got.Detail != "lookup in progress" || got.Host != testHost { + t.Fatalf("expired Lookup() = %+v, want unknown", got) + } + if got := svc.Lookup(ctx, testHost, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("refreshed Lookup() = %+v", got) + } + if st := svc.Stats(); st.Lookups != 2 || st.Entries != 1 || st.Hits+st.Misses != 4 { + t.Fatalf("Stats() = %+v, want 2 lookups, 1 entry, 4 cache reads", st) + } + if calls := h.cdns.calls.Load(); calls != 2 { + t.Fatalf("DNS calls = %d, want 2", calls) + } +} + +func TestLookupRefreshAhead(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + svc := h.service(t) + ctx := t.Context() + + svc.Lookup(ctx, testHost, 5*time.Second) + h.clock.Advance(defaultCacheTTL - defaultCacheTTL/refreshAheadDivisor) + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusRegistered { + t.Fatalf("Lookup() in the refresh window = %+v, want the cached verdict", got) + } + if st := svc.Stats(); st.Lookups != 2 || st.Hits != 1 { + t.Fatalf("Stats() = %+v, want a second lookup started on a hit", st) + } + eventually(t, "refresh lookup", func() bool { return h.cdns.calls.Load() == 2 }) + svc.Lookup(ctx, testHost, 5*time.Second) + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusRegistered { + t.Fatalf("Lookup() after refresh = %+v", got) + } + if st := svc.Stats(); st.Lookups != 2 { + t.Fatalf("Stats() = %+v, want no further lookups", st) + } +} + +func TestLookupErrorTTL(t *testing.T) { + t.Parallel() + h := newHarness() + h.dns.WithError(testHost, &verify.DNSError{Type: verify.DNSErrorLookupFailed, Fqdn: testHost, Reason: "servfail"}) + svc := h.service(t) + ctx := t.Context() + + if got := svc.Lookup(ctx, testHost, 5*time.Second); got.Status != StatusError || got.Detail != "dns lookup failed" { + t.Fatalf("Lookup() = %+v", got) + } + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusError { + t.Fatalf("cached error Lookup() = %+v", got) + } + h.clock.Advance(defaultErrorTTL) + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusUnknown { + t.Fatalf("Lookup() after error TTL = %+v, want unknown", got) + } + if st := svc.Stats(); st.Errors != 1 || st.Lookups != 2 { + t.Fatalf("Stats() = %+v", st) + } +} + +func TestLookupEviction(t *testing.T) { + t.Parallel() + h := newHarness() + hosts := []string{"a.example.com", "b.example.com", "c.example.com"} + for _, host := range hosts { + h.registered(host) + } + svc := h.service(t) + svc.cache = newVerdictCache(2) + ctx := t.Context() + for _, host := range hosts { + if got := svc.Lookup(ctx, host, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("Lookup(%q) = %+v", host, got) + } + } + if st := svc.Stats(); st.Entries != 2 { + t.Fatalf("Stats() = %+v, want 2 entries", st) + } + if got := svc.Lookup(ctx, "a.example.com", 0); got.Status != StatusUnknown { + t.Fatalf("Lookup(evicted) = %+v, want unknown", got) + } + if got := svc.Lookup(ctx, "c.example.com", 0); got.Status != StatusRegistered { + t.Fatalf("Lookup(retained) = %+v", got) + } +} + +func TestLookupSingleFlight(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + h.cdns.gate = make(chan struct{}) + svc := h.service(t) + ctx := t.Context() + + const n = 16 + results := make(chan Verdict, n) + var wg sync.WaitGroup + for range n { + wg.Add(1) + go func() { + defer wg.Done() + results <- svc.Lookup(ctx, testHost, 5*time.Second) + }() + } + eventually(t, "the lookup to reach DNS", func() bool { return h.cdns.calls.Load() == 1 }) + close(h.cdns.gate) + wg.Wait() + close(results) + for got := range results { + if got.Status != StatusRegistered { + t.Fatalf("Lookup() = %+v", got) + } + } + st := svc.Stats() + if st.Lookups != 1 || st.Hits+st.Misses != n || h.cdns.calls.Load() != 1 { + t.Fatalf("Stats() = %+v, DNS calls %d; want one lookup", st, h.cdns.calls.Load()) + } +} + +func TestLookupWaitZeroThenCached(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + svc := h.service(t) + ctx := t.Context() + + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusUnknown || got.Host != testHost { + t.Fatalf("Lookup(wait 0) = %+v, want unknown", got) + } + if got := svc.Lookup(ctx, testHost, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("Lookup(wait) = %+v", got) + } + if got := svc.Lookup(ctx, testHost, 0); got.Status != StatusRegistered { + t.Fatalf("Lookup(cached) = %+v", got) + } + if st := svc.Stats(); st.Lookups != 1 { + t.Fatalf("Stats() = %+v, want one lookup", st) + } +} + +func TestLookupWaitElapses(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + h.cdns.gate = make(chan struct{}) + svc := h.service(t) + ctx := t.Context() + + if got := svc.Lookup(ctx, testHost, 20*time.Millisecond); got.Status != StatusUnknown { + t.Fatalf("Lookup(short wait) = %+v, want unknown", got) + } + eventually(t, "the lookup to reach DNS", func() bool { return h.cdns.calls.Load() == 1 }) + close(h.cdns.gate) + if got := svc.Lookup(ctx, testHost, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("Lookup(after release) = %+v", got) + } +} + +func TestLookupContextCanceled(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + h.cdns.gate = make(chan struct{}) + svc := h.service(t) + + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan Verdict, 1) + go func() { done <- svc.Lookup(ctx, testHost, 10*time.Second) }() + eventually(t, "the lookup to reach DNS", func() bool { return h.cdns.calls.Load() == 1 }) + cancel() + select { + case got := <-done: + if got.Status != StatusUnknown { + t.Fatalf("Lookup(canceled) = %+v, want unknown", got) + } + case <-time.After(5 * time.Second): + t.Fatal("Lookup did not return after cancellation") + } + close(h.cdns.gate) + if got := svc.Lookup(t.Context(), testHost, 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("Lookup(after cancellation) = %+v, want the background result", got) + } + if st := svc.Stats(); st.Lookups != 1 { + t.Fatalf("Stats() = %+v, want one lookup", st) + } +} + +func TestPrewarm(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered("a.example.com") + svc := h.service(t) + ctx := t.Context() + + svc.Prewarm(ctx, "a.example.com", "b.example.com", "api.openai.com", "192.0.2.1", "A.example.com.") + if st := svc.Stats(); st.Lookups != 2 { + t.Fatalf("Stats() = %+v, want 2 lookups", st) + } + eventually(t, "prewarmed verdicts", func() bool { + return svc.Lookup(ctx, "a.example.com", 0).Status == StatusRegistered && + svc.Lookup(ctx, "b.example.com", 0).Status == StatusUnregistered + }) + if calls := h.cdns.calls.Load(); calls != 2 { + t.Fatalf("DNS calls = %d, want 2", calls) + } +} + +func TestClose(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered("a.example.com") + h.registered("b.example.com") + svc := h.service(t) + ctx := t.Context() + + if got := svc.Lookup(ctx, "a.example.com", 5*time.Second); got.Status != StatusRegistered { + t.Fatalf("Lookup(a) = %+v", got) + } + h.cdns.gate = make(chan struct{}) + if got := svc.Lookup(ctx, "b.example.com", 0); got.Status != StatusUnknown { + t.Fatalf("Lookup(b) = %+v, want unknown", got) + } + eventually(t, "the lookup to reach DNS", func() bool { return h.cdns.calls.Load() == 2 }) + + closed := make(chan struct{}) + go func() { + svc.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(5 * time.Second): + t.Fatal("Close did not return while a lookup was blocked") + } + + if got := svc.Lookup(ctx, "b.example.com", 0); got.Status != StatusError || got.Detail != "dns lookup failed" { + t.Fatalf("Lookup(b) after close = %+v, want the canceled lookup's error", got) + } + if got := svc.Lookup(ctx, "a.example.com", 0); got.Status != StatusRegistered { + t.Fatalf("Lookup(a) after close = %+v, want the cached verdict", got) + } + if got := svc.Lookup(ctx, "c.example.com", time.Second); got.Status != StatusError || got.Detail != "vetting service closed" { + t.Fatalf("Lookup(c) after close = %+v", got) + } + h.clock.Advance(defaultCacheTTL - defaultCacheTTL/refreshAheadDivisor) + if got := svc.Lookup(ctx, "a.example.com", 0); got.Status != StatusRegistered { + t.Fatalf("Lookup(a) in refresh window after close = %+v", got) + } + if st := svc.Stats(); st.Lookups != 2 || st.Errors != 1 { + t.Fatalf("Stats() = %+v, want no lookups after close", st) + } + svc.Close() +} + +func TestLookupLogsHostAndStatusOnly(t *testing.T) { + t.Parallel() + h := newHarness() + h.registered(testHost) + h.dns.WithError("down.example.com", &verify.DNSError{Type: verify.DNSErrorTimeout, Fqdn: "down.example.com"}) + svc := h.service(t) + ctx := t.Context() + + svc.Lookup(ctx, testHost, 5*time.Second) + svc.Lookup(ctx, "down.example.com", 5*time.Second) + eventually(t, "log lines", func() bool { + return strings.Count(h.logs.String(), "destination verdict") == 2 + }) + logs := h.logs.String() + for _, want := range []string{ + `level=INFO msg="destination verdict" host=agent.example.com status=registered`, + `level=WARN msg="dns lookup failed" host=down.example.com error=`, + `level=INFO msg="destination verdict" host=down.example.com status=error`, + } { + if !strings.Contains(logs, want) { + t.Errorf("logs missing %q:\n%s", want, logs) + } + } + if strings.Contains(logs, "ans://") || strings.Contains(logs, "id-") { + t.Fatalf("logs leak badge identity:\n%s", logs) + } + + h.clock.Advance(defaultCacheTTL) + svc.Lookup(ctx, testHost, 5*time.Second) + eventually(t, "the refresh to finish", func() bool { return h.cdns.calls.Load() == 3 }) + svc.Lookup(ctx, testHost, 5*time.Second) + if n := strings.Count(h.logs.String(), "host=agent.example.com status=registered"); n != 1 { + t.Fatalf("unchanged verdict logged %d times, want once", n) + } +} diff --git a/internal/vetting/trustindex.go b/internal/vetting/trustindex.go new file mode 100644 index 0000000..d66f28d --- /dev/null +++ b/internal/vetting/trustindex.go @@ -0,0 +1,102 @@ +package vetting + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// TrustIndexClient reads the Trust Index value the threshold check compares +// against vetting.min_trust_score. +type TrustIndexClient interface { + // LowestDimension returns the agent's lowest Trust Vector dimension, or + // ErrNoTrustRecord when the Trust Index has no record for agentID. + LowestDimension(ctx context.Context, agentID string) (float64, error) +} + +// ErrNoTrustRecord reports that the Trust Index has no record for the agent. +// The lookup treats it as a score of nothing: below any positive minimum. +var ErrNoTrustRecord = errors.New("trust index: no record for agent") + +const ( + maxTrustIndexResponseBytes = 1 << 20 + trustIndexDetailPath = "/v1/ans/registered-agents/" +) + +type trustVector struct { + Integrity int `json:"integrity"` + Identity int `json:"identity"` + Solvency int `json:"solvency"` + Behavior int `json:"behavior"` + Safety int `json:"safety"` +} + +type trustEvaluation struct { + TrustVector *trustVector `json:"trustVector"` +} + +type agentDetail struct { + TrustEvaluation *trustEvaluation `json:"trustEvaluation"` +} + +// HTTPTrustIndexClient reads GET /v1/ans/registered-agents/{agentId} from a +// Trust Index deployment. The detail response carries no composite score, +// only the Trust Vector's five dimensions (integrity, identity, solvency, +// behavior, safety; each an integer 0-100), so the threshold is a floor every +// dimension must clear. +type HTTPTrustIndexClient struct { + base string + client *http.Client +} + +// NewHTTPTrustIndexClient targets the Trust Index at baseURL. A nil client +// gets a 5 s timeout. +func NewHTTPTrustIndexClient(baseURL string, client *http.Client) (*HTTPTrustIndexClient, error) { + u, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil { + return nil, fmt.Errorf("trust index: invalid base url %q: %w", baseURL, err) + } + if (u.Scheme != httpsScheme && u.Scheme != "http") || u.Host == "" { + return nil, fmt.Errorf("trust index: base url %q must be http(s) with a host", baseURL) + } + return &HTTPTrustIndexClient{base: strings.TrimRight(u.String(), "/"), client: withoutRedirects(client)}, nil +} + +// LowestDimension fetches the agent's detail record and returns the smallest of +// its five Trust Vector dimensions. +func (c *HTTPTrustIndexClient) LowestDimension(ctx context.Context, agentID string) (float64, error) { + if agentID == "" { + return 0, errors.New("trust index: empty agent id") + } + target := c.base + trustIndexDetailPath + url.PathEscape(agentID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) + if err != nil { + return 0, fmt.Errorf("trust index: build request: %w", err) + } + req.Header.Set("Accept", "application/json") + resp, err := c.client.Do(req) //nolint:gosec // G704: base URL is operator configuration; the agent id is path-escaped + if err != nil { + return 0, fmt.Errorf("trust index: %w", err) + } + defer resp.Body.Close() + switch { + case resp.StatusCode == http.StatusNotFound: + return 0, ErrNoTrustRecord + case resp.StatusCode != http.StatusOK: + return 0, fmt.Errorf("trust index: unexpected status %d", resp.StatusCode) + } + var detail agentDetail + if err := json.NewDecoder(io.LimitReader(resp.Body, maxTrustIndexResponseBytes)).Decode(&detail); err != nil { + return 0, fmt.Errorf("trust index: decode response: %w", err) + } + if detail.TrustEvaluation == nil || detail.TrustEvaluation.TrustVector == nil { + return 0, errors.New("trust index: response has no trust vector") + } + tv := detail.TrustEvaluation.TrustVector + return float64(min(tv.Integrity, tv.Identity, tv.Solvency, tv.Behavior, tv.Safety)), nil +} diff --git a/internal/vetting/trustindex_test.go b/internal/vetting/trustindex_test.go new file mode 100644 index 0000000..bf4b6d0 --- /dev/null +++ b/internal/vetting/trustindex_test.go @@ -0,0 +1,116 @@ +package vetting + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestHTTPTrustIndexClientLowestDimension(t *testing.T) { + t.Parallel() + vector := `{"agentId":"a1","trustEvaluation":{"agentId":"a1","trustVector":{"integrity":90,"identity":70,"solvency":55,"behavior":80,"safety":100},"recommendedProfile":"TRANSACTIONAL"}}` + tests := []struct { + name string + status int + body string + want float64 + wantErr error + wantErrSub string + }{ + {name: "lowest dimension", status: http.StatusOK, body: vector, want: 55}, + {name: "not found", status: http.StatusNotFound, body: `{"type":"about:blank","code":"AGENT_NOT_FOUND"}`, wantErr: ErrNoTrustRecord}, + {name: "server error", status: http.StatusInternalServerError, wantErrSub: "unexpected status 500"}, + {name: "malformed json", status: http.StatusOK, body: "{", wantErrSub: "decode response"}, + {name: "missing evaluation", status: http.StatusOK, body: `{"agentId":"a1"}`, wantErrSub: "no trust vector"}, + {name: "missing vector", status: http.StatusOK, body: `{"trustEvaluation":{"agentId":"a1"}}`, wantErrSub: "no trust vector"}, + {name: "oversized body", status: http.StatusOK, body: `{"pad":"` + strings.Repeat("x", maxTrustIndexResponseBytes) + `",` + vector[1:], wantErrSub: "decode response"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var gotPath, gotAccept string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath, gotAccept = r.URL.EscapedPath(), r.Header.Get("Accept") + w.WriteHeader(tt.status) + _, _ = w.Write([]byte(tt.body)) + })) + t.Cleanup(server.Close) + client, err := NewHTTPTrustIndexClient(server.URL+"/", server.Client()) + if err != nil { + t.Fatalf("NewHTTPTrustIndexClient() error = %v", err) + } + got, err := client.LowestDimension(t.Context(), "agent/1") + if gotPath != "/v1/ans/registered-agents/agent%2F1" || gotAccept != "application/json" { + t.Fatalf("request path %q accept %q", gotPath, gotAccept) + } + switch { + case tt.wantErr != nil: + if !errors.Is(err, tt.wantErr) { + t.Fatalf("LowestDimension() error = %v, want %v", err, tt.wantErr) + } + case tt.wantErrSub != "": + if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) { + t.Fatalf("LowestDimension() error = %v, want containing %q", err, tt.wantErrSub) + } + default: + if err != nil || got != tt.want { + t.Fatalf("LowestDimension() = %v, %v; want %v", got, err, tt.want) + } + } + }) + } +} + +func TestHTTPTrustIndexClientRequestFailures(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + t.Error("no request expected") + })) + client, err := NewHTTPTrustIndexClient(server.URL, server.Client()) + if err != nil { + t.Fatalf("NewHTTPTrustIndexClient() error = %v", err) + } + if _, err := client.LowestDimension(t.Context(), ""); err == nil || !strings.Contains(err.Error(), "empty agent id") { + t.Fatalf("LowestDimension(\"\") error = %v", err) + } + server.Close() + if _, err := client.LowestDimension(t.Context(), "a1"); err == nil { + t.Fatal("LowestDimension() against a closed server should fail") + } +} + +func TestNewHTTPTrustIndexClient(t *testing.T) { + t.Parallel() + tests := []struct { + baseURL string + wantBase string + wantErr bool + }{ + {baseURL: "https://ti.example.com/", wantBase: "https://ti.example.com"}, + {baseURL: " http://localhost:8080 ", wantBase: "http://localhost:8080"}, + {baseURL: "", wantErr: true}, + {baseURL: "ftp://ti.example.com", wantErr: true}, + {baseURL: "https://", wantErr: true}, + {baseURL: "ti.example.com", wantErr: true}, + {baseURL: "http://[::1", wantErr: true}, + } + for _, tt := range tests { + client, err := NewHTTPTrustIndexClient(tt.baseURL, nil) + if (err != nil) != tt.wantErr { + t.Errorf("NewHTTPTrustIndexClient(%q) error = %v, wantErr %v", tt.baseURL, err, tt.wantErr) + continue + } + if err != nil { + continue + } + if client.base != tt.wantBase { + t.Errorf("NewHTTPTrustIndexClient(%q) base = %q, want %q", tt.baseURL, client.base, tt.wantBase) + } + if client.client.Timeout != 5*time.Second { + t.Errorf("default client timeout = %v, want 5s", client.client.Timeout) + } + } +} diff --git a/internal/vetting/verdict.go b/internal/vetting/verdict.go new file mode 100644 index 0000000..5b8ffe3 --- /dev/null +++ b/internal/vetting/verdict.go @@ -0,0 +1,168 @@ +// Package vetting decides whether a destination host is a registered ANS agent +// and what a policy mode makes of that answer. The Verifier does the lookups +// (DNS _ans-badge record, trusted-TL allowlist, badge status, optional Trust +// Index score) with a cache so the request path never waits on the network for +// a host it has seen before; Decide maps a Verdict and a Mode to an allow or +// deny with a stable reason code shared by the middleware and the interceptor. +package vetting + +import ( + "context" + "fmt" + "strings" + "time" +) + +// Status is the outcome of vetting one destination host. +type Status int + +const ( + // StatusUnknown means no verdict is available yet: the lookup has not run + // or is still in flight. + StatusUnknown Status = iota + // StatusRegistered means the host publishes an _ans-badge record pointing + // at a trusted transparency log whose badge is ACTIVE, WARNING or + // DEPRECATED. + StatusRegistered + // StatusUnregistered means the host publishes no _ans-badge record. + StatusUnregistered + // StatusRevoked means the badge status is EXPIRED or REVOKED. + StatusRevoked + // StatusUntrustedTL means the _ans-badge URL names a transparency log that + // is not in the operator's trusted-TL allowlist. + StatusUntrustedTL + // StatusBelowThreshold means the host is registered but its Trust Index + // score is below the configured minimum. + StatusBelowThreshold + // StatusError means the lookup failed (DNS or TL error) and no cached + // verdict exists. + StatusError + // StatusExempt means the operator excluded the host from vetting + // (vetting.exempt_hosts); it is never denied and never looked up. + StatusExempt +) + +// String returns the lowercase snake_case label used in findings and logs. +func (s Status) String() string { + switch s { + case StatusUnknown: + return "unknown" + case StatusRegistered: + return "registered" + case StatusUnregistered: + return "unregistered" + case StatusRevoked: + return "revoked" + case StatusUntrustedTL: + return "untrusted_tl" + case StatusBelowThreshold: + return "below_threshold" + case StatusError: + return "error" + case StatusExempt: + return "exempt" + } + return fmt.Sprintf("status(%d)", int(s)) +} + +// Verdict is the vetting result for one host. +type Verdict struct { + Host string + Status Status + // AnsName and AgentID come from the badge when the host is registered. + AnsName string + AgentID string + // BadgeStatus is the badge's lifecycle status (ACTIVE, REVOKED, ...). + BadgeStatus string + // Detail is a short audit-safe explanation; it never echoes request content. + Detail string +} + +// Verifier answers whether a host is a registered ANS agent. +type Verifier interface { + // Lookup returns the verdict for host. A cached verdict returns at once. On + // a miss the lookup starts in the background and, when wait is positive, + // Lookup blocks up to wait (bounded by ctx) for it; otherwise it returns + // StatusUnknown and the next call finds the cached result. + Lookup(ctx context.Context, host string, wait time.Duration) Verdict +} + +// Mode is the destination policy a sandbox policy selects. +type Mode int + +const ( + // ModeOff performs no vetting. + ModeOff Mode = iota + // ModeObserve vets every destination and reports findings but never denies. + ModeObserve + // ModeRequire denies destinations that are not registered ANS agents. + ModeRequire +) + +// ParseMode parses the policy spelling of a Mode: "off", "observe" or +// "require_registered". The empty string is ModeOff. +func ParseMode(s string) (Mode, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "", "off": + return ModeOff, nil + case "observe": + return ModeObserve, nil + case "require_registered": + return ModeRequire, nil + } + return ModeOff, fmt.Errorf("unknown destination_policy %q (want off, observe or require_registered)", s) +} + +// String returns the policy spelling of the mode. +func (m Mode) String() string { + switch m { + case ModeOff: + return "off" + case ModeObserve: + return "observe" + case ModeRequire: + return "require_registered" + } + return fmt.Sprintf("mode(%d)", int(m)) +} + +// Reason codes returned to OpenShell on a destination denial. They follow the +// HttpRequestResult.reason_code grammar: lowercase ASCII, digits and +// underscores, at most 64 bytes. +const ( + ReasonUnregistered = "ans_destination_unregistered" + ReasonRevoked = "ans_destination_revoked" + ReasonUntrustedTL = "ans_destination_untrusted_tl" + ReasonBelowThreshold = "ans_destination_below_threshold" + ReasonUnverified = "ans_destination_unverified" +) + +// Decision is what a Mode makes of a Verdict. +type Decision struct { + Deny bool + ReasonCode string +} + +// Decide applies mode to v. Only ModeRequire ever denies; an unknown or errored +// verdict denies as unverified so a lookup that has not finished can never +// let an unregistered destination through. +func Decide(mode Mode, v Verdict) Decision { + if mode != ModeRequire { + return Decision{} + } + switch v.Status { + case StatusRegistered, StatusExempt: + return Decision{} + case StatusUnregistered: + return Decision{Deny: true, ReasonCode: ReasonUnregistered} + case StatusRevoked: + return Decision{Deny: true, ReasonCode: ReasonRevoked} + case StatusUntrustedTL: + return Decision{Deny: true, ReasonCode: ReasonUntrustedTL} + case StatusBelowThreshold: + return Decision{Deny: true, ReasonCode: ReasonBelowThreshold} + case StatusUnknown, StatusError: + return Decision{Deny: true, ReasonCode: ReasonUnverified} + } + return Decision{Deny: true, ReasonCode: ReasonUnverified} +} diff --git a/internal/vetting/verdict_test.go b/internal/vetting/verdict_test.go new file mode 100644 index 0000000..d31d57f --- /dev/null +++ b/internal/vetting/verdict_test.go @@ -0,0 +1,103 @@ +package vetting + +import "testing" + +func TestParseMode(t *testing.T) { + t.Parallel() + tests := []struct { + in string + want Mode + wantErr bool + }{ + {in: "", want: ModeOff}, + {in: "off", want: ModeOff}, + {in: " Observe ", want: ModeObserve}, + {in: "require_registered", want: ModeRequire}, + {in: "REQUIRE_REGISTERED", want: ModeRequire}, + {in: "require", wantErr: true}, + {in: "deny", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + t.Parallel() + got, err := ParseMode(tt.in) + if (err != nil) != tt.wantErr { + t.Fatalf("ParseMode(%q) error = %v, wantErr %v", tt.in, err, tt.wantErr) + } + if !tt.wantErr && got != tt.want { + t.Fatalf("ParseMode(%q) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestStringers(t *testing.T) { + t.Parallel() + statuses := map[Status]string{ + StatusUnknown: "unknown", StatusRegistered: "registered", StatusUnregistered: "unregistered", + StatusRevoked: "revoked", StatusUntrustedTL: "untrusted_tl", StatusBelowThreshold: "below_threshold", + StatusError: "error", StatusExempt: "exempt", Status(42): "status(42)", + } + for s, want := range statuses { + if got := s.String(); got != want { + t.Errorf("Status(%d).String() = %q, want %q", int(s), got, want) + } + } + modes := map[Mode]string{ModeOff: "off", ModeObserve: "observe", ModeRequire: "require_registered", Mode(9): "mode(9)"} + for m, want := range modes { + if got := m.String(); got != want { + t.Errorf("Mode(%d).String() = %q, want %q", int(m), got, want) + } + } +} + +func TestDecide(t *testing.T) { + t.Parallel() + all := []Status{StatusUnknown, StatusRegistered, StatusUnregistered, StatusRevoked, + StatusUntrustedTL, StatusBelowThreshold, StatusError, StatusExempt, Status(99)} + for _, mode := range []Mode{ModeOff, ModeObserve} { + for _, s := range all { + if d := Decide(mode, Verdict{Status: s}); d.Deny || d.ReasonCode != "" { + t.Errorf("Decide(%v, %v) = %+v, want allow", mode, s, d) + } + } + } + tests := []struct { + status Status + deny bool + code string + }{ + {status: StatusRegistered}, + {status: StatusExempt}, + {status: StatusUnregistered, deny: true, code: ReasonUnregistered}, + {status: StatusRevoked, deny: true, code: ReasonRevoked}, + {status: StatusUntrustedTL, deny: true, code: ReasonUntrustedTL}, + {status: StatusBelowThreshold, deny: true, code: ReasonBelowThreshold}, + {status: StatusUnknown, deny: true, code: ReasonUnverified}, + {status: StatusError, deny: true, code: ReasonUnverified}, + {status: Status(99), deny: true, code: ReasonUnverified}, + } + for _, tt := range tests { + t.Run(tt.status.String(), func(t *testing.T) { + t.Parallel() + d := Decide(ModeRequire, Verdict{Status: tt.status}) + if d.Deny != tt.deny || d.ReasonCode != tt.code { + t.Fatalf("Decide(require, %v) = %+v, want deny=%v code=%q", tt.status, d, tt.deny, tt.code) + } + }) + } +} + +func TestReasonCodeGrammar(t *testing.T) { + t.Parallel() + for _, code := range []string{ReasonUnregistered, ReasonRevoked, ReasonUntrustedTL, ReasonBelowThreshold, ReasonUnverified} { + if len(code) > 64 || code[0] < 'a' || code[0] > 'z' { + t.Errorf("reason code %q violates the OpenShell grammar", code) + } + for _, r := range code { + if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '_' { + t.Errorf("reason code %q contains %q", code, r) + } + } + } +} diff --git a/pkg/provenance/external_key_test.go b/pkg/provenance/external_key_test.go new file mode 100644 index 0000000..02d9654 --- /dev/null +++ b/pkg/provenance/external_key_test.go @@ -0,0 +1,149 @@ +package provenance + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "io" + "math/big" + "strings" + "testing" + "time" +) + +// recordingSigner stands in for a key held outside the process: it signs with +// a P-256 key it never exposes and counts the calls. +type recordingSigner struct { + key *ecdsa.PrivateKey + calls int + der []byte + err error +} + +func (r *recordingSigner) Public() crypto.PublicKey { return &r.key.PublicKey } + +func (r *recordingSigner) Sign(random io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + r.calls++ + if r.err != nil { + return nil, r.err + } + if r.der != nil { + return r.der, nil + } + return r.key.Sign(random, digest, opts) +} + +func certFor(t *testing.T, pub any, key crypto.Signer) *x509.Certificate { + t.Helper() + tmpl := &x509.Certificate{SerialNumber: big.NewInt(7), Subject: pkix.Name{CommonName: "external"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)} + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return cert +} + +func TestNewSignerExternalKey(t *testing.T) { + t.Parallel() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ext := &recordingSigner{key: key} + cert := certFor(t, &key.PublicKey, key) + s, err := NewSigner(ext, cert) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + body := []byte(`{"observations":[]}`) + jws, err := s.Sign(body, time.Unix(1_700_000_000, 0), 0, "ans://v1.0.0.ops.example.com") + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + if ext.calls != 1 { + t.Fatalf("external signer called %d times, want 1", ext.calls) + } + claims, got, err := Verify(jws, body) + if err != nil { + t.Fatalf("Verify() error = %v", err) + } + if !got.Equal(cert) || claims.AIMID != "ans://v1.0.0.ops.example.com" || claims.IssuedAt != 1_700_000_000 { + t.Fatalf("claims = %+v, cert match %v", claims, got.Equal(cert)) + } +} + +func TestNewSignerRejections(t *testing.T) { + t.Parallel() + p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + other, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + _, edKey, _ := ed25519.GenerateKey(rand.Reader) + cert := certFor(t, &p256.PublicKey, p256) + tests := []struct { + name string + key crypto.Signer + cert *x509.Certificate + wantErr string + }{ + {name: "nil key", cert: cert, wantErr: "required"}, + {name: "nil cert", key: p256, wantErr: "required"}, + {name: "ed25519 key", key: edKey, cert: cert, wantErr: "not an ECDSA P-256 key"}, + {name: "p384 key", key: p384, cert: certFor(t, &p384.PublicKey, p384), wantErr: "not an ECDSA P-256 key"}, + {name: "certificate for another key", key: other, cert: cert, wantErr: "does not match"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, err := NewSigner(tt.key, tt.cert) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewSigner() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +func TestSignExternalFailures(t *testing.T) { + t.Parallel() + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + cert := certFor(t, &key.PublicKey, key) + tests := []struct { + name string + signer *recordingSigner + wantErr string + }{ + {name: "signer error", signer: &recordingSigner{key: key, err: errors.New("hsm offline")}, wantErr: "hsm offline"}, + {name: "malformed asn1", signer: &recordingSigner{key: key, der: []byte{0x30, 0x01}}, wantErr: "malformed ASN.1"}, + {name: "trailing bytes", signer: &recordingSigner{key: key, der: append(mustDER(t, key), 0x00)}, wantErr: "malformed ASN.1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s, err := NewSigner(tt.signer, cert) + if err != nil { + t.Fatal(err) + } + if _, err := s.Sign([]byte("x"), time.Now(), 1, "a"); err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("Sign() error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +func mustDER(t *testing.T, key *ecdsa.PrivateKey) []byte { + t.Helper() + der, err := ecdsa.SignASN1(rand.Reader, key, make([]byte, 32)) + if err != nil { + t.Fatal(err) + } + return der +} diff --git a/pkg/provenance/signer.go b/pkg/provenance/signer.go new file mode 100644 index 0000000..4042544 --- /dev/null +++ b/pkg/provenance/signer.go @@ -0,0 +1,244 @@ +// Package provenance defines the signed-batch format the ANS OpenShell +// hydrator attaches to Trust Index observation imports: a compact ES256 JWS +// whose claims bind the SHA-256 of the exact request body, the observation +// count, the signing time and the producer's identifier, with the signer's +// certificate in the x5c header. It is a proposal pending Trust Index support +// and lives outside internal/ so a Trust Index can import Verify as is. +package provenance + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "time" + + "github.com/agentnameservice/ans-openshell/internal/pemkeys" +) + +// Protected header values of a signed batch. +const ( + BatchSignatureType = "ans-observations+jws" + BatchSignatureAlg = "ES256" +) + +const ( + es256SignatureLen = 64 + es256CoordinateLen = 32 + jwsPartCount = 3 +) + +// ErrMalformedSignature reports a batch signature whose structure, header or +// claims cannot be used. +var ErrMalformedSignature = errors.New("provenance: malformed batch signature") + +// ErrBadSignature reports a well-formed batch signature that does not verify +// or does not cover the given body. +var ErrBadSignature = errors.New("provenance: batch signature does not verify") + +// SignedBatchClaims is the JWS payload: the base64url SHA-256 of the request +// body, the observation count, the signing time and the signer's identifier. +type SignedBatchClaims struct { + BodySHA256 string `json:"body_sha256"` + Count int `json:"count"` + IssuedAt int64 `json:"iat"` + AIMID string `json:"aim_id,omitempty"` +} + +type jwsHeader struct { + Typ string `json:"typ"` + Alg string `json:"alg"` + X5C []string `json:"x5c"` +} + +// BatchSigner signs one import body. Signer implements it over an in-process +// or external P-256 key; a fake implements it in tests. +type BatchSigner interface { + Sign(body []byte, iat time.Time, count int, aimID string) (string, error) +} + +// Signer signs batches with a P-256 key reachable through crypto.Signer, so the +// key may live in a file, Vault Transit or an HSM, and the X.509 certificate +// that carries its public key. +type Signer struct { + key crypto.Signer + cert *x509.Certificate + header string +} + +// NewSigner binds key to cert. The key must be ECDSA on P-256 and must match +// the certificate's public key. +func NewSigner(key crypto.Signer, cert *x509.Certificate) (*Signer, error) { + if key == nil || cert == nil { + return nil, errors.New("provenance: key and certificate are required") + } + pub, ok := key.Public().(*ecdsa.PublicKey) + if !ok || pub.Curve != elliptic.P256() { + return nil, errors.New("provenance: key: not an ECDSA P-256 key") + } + if !pub.Equal(cert.PublicKey) { + return nil, errors.New("provenance: certificate public key does not match the signing key") + } + header, err := json.Marshal(jwsHeader{ + Typ: BatchSignatureType, + Alg: BatchSignatureAlg, + X5C: []string{base64.StdEncoding.EncodeToString(cert.Raw)}, + }) + if err != nil { + return nil, fmt.Errorf("provenance: encode jws header: %w", err) + } + return &Signer{key: key, cert: cert, header: base64.RawURLEncoding.EncodeToString(header)}, nil +} + +// NewPEMSigner parses a PEM-encoded P-256 private key (PKCS#8 or SEC1) and the +// PEM-encoded certificate for it. +func NewPEMSigner(keyPEM, certPEM []byte) (*Signer, error) { + key, err := pemkeys.ParseP256PrivateKey(keyPEM) + if err != nil { + return nil, fmt.Errorf("provenance: key: %w", err) + } + cert, err := pemkeys.ParseCertificate(certPEM) + if err != nil { + return nil, fmt.Errorf("provenance: certificate: %w", err) + } + return NewSigner(key, cert) +} + +// Certificate returns the certificate sent in the x5c header. +func (s *Signer) Certificate() *x509.Certificate { + return s.cert +} + +// Sign returns a compact JWS whose claims bind body, count and aimID at iat. +func (s *Signer) Sign(body []byte, iat time.Time, count int, aimID string) (string, error) { + payload, err := json.Marshal(SignedBatchClaims{ + BodySHA256: bodyDigest(body), + Count: count, + IssuedAt: iat.Unix(), + AIMID: aimID, + }) + if err != nil { + return "", fmt.Errorf("provenance: encode jws claims: %w", err) + } + signingInput := s.header + "." + base64.RawURLEncoding.EncodeToString(payload) + digest := sha256.Sum256([]byte(signingInput)) + der, err := s.key.Sign(rand.Reader, digest[:], crypto.SHA256) + if err != nil { + return "", fmt.Errorf("provenance: sign batch: %w", err) + } + sig, err := derToP1363(der) + if err != nil { + return "", err + } + return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig), nil +} + +// derToP1363 converts the ASN.1 signature a crypto.Signer returns into the +// fixed-width R||S form JWS uses. +func derToP1363(der []byte) ([]byte, error) { + var parsed struct{ R, S *big.Int } + rest, err := asn1.Unmarshal(der, &parsed) + if err != nil || len(rest) != 0 || parsed.R == nil || parsed.S == nil { + return nil, errors.New("provenance: signer returned a malformed ASN.1 signature") + } + if parsed.R.BitLen() > es256CoordinateLen*8 || parsed.S.BitLen() > es256CoordinateLen*8 { + return nil, errors.New("provenance: signature coordinates exceed P-256") + } + sig := make([]byte, es256SignatureLen) + parsed.R.FillBytes(sig[:es256CoordinateLen]) + parsed.S.FillBytes(sig[es256CoordinateLen:]) + return sig, nil +} + +// Verify checks a compact JWS produced by Sign against body: the structure, +// typ and alg, the x5c leaf certificate's P-256 key, the signature and the +// body digest. It returns the claims and the leaf certificate. Trusting the +// certificate, binding aim_id to it and judging the claims' freshness are the +// caller's decisions. +func Verify(jws string, body []byte) (*SignedBatchClaims, *x509.Certificate, error) { + parts := strings.Split(jws, ".") + if len(parts) != jwsPartCount { + return nil, nil, fmt.Errorf("%w: expected %d parts, got %d", ErrMalformedSignature, jwsPartCount, len(parts)) + } + cert, err := decodeHeader(parts[0]) + if err != nil { + return nil, nil, err + } + pub, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok || pub.Curve != elliptic.P256() { + return nil, nil, fmt.Errorf("%w: x5c certificate key is not ECDSA P-256", ErrMalformedSignature) + } + sig, err := base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil || len(sig) != es256SignatureLen { + return nil, nil, fmt.Errorf("%w: signature is not %d base64url bytes", ErrMalformedSignature, es256SignatureLen) + } + digest := sha256.Sum256([]byte(parts[0] + "." + parts[1])) + r := new(big.Int).SetBytes(sig[:es256CoordinateLen]) + sv := new(big.Int).SetBytes(sig[es256CoordinateLen:]) + if !ecdsa.Verify(pub, digest[:], r, sv) { + return nil, nil, ErrBadSignature + } + claims, err := decodeClaims(parts[1]) + if err != nil { + return nil, nil, err + } + if claims.BodySHA256 != bodyDigest(body) { + return nil, nil, fmt.Errorf("%w: body digest mismatch", ErrBadSignature) + } + return claims, cert, nil +} + +func decodeHeader(encoded string) (*x509.Certificate, error) { + raw, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("%w: header is not base64url", ErrMalformedSignature) + } + var header jwsHeader + if err := json.Unmarshal(raw, &header); err != nil { + return nil, fmt.Errorf("%w: header is not JSON", ErrMalformedSignature) + } + if header.Alg != BatchSignatureAlg { + return nil, fmt.Errorf("%w: alg %q, want %s", ErrMalformedSignature, header.Alg, BatchSignatureAlg) + } + if header.Typ != BatchSignatureType { + return nil, fmt.Errorf("%w: typ %q, want %s", ErrMalformedSignature, header.Typ, BatchSignatureType) + } + if len(header.X5C) == 0 { + return nil, fmt.Errorf("%w: x5c is empty", ErrMalformedSignature) + } + der, err := base64.StdEncoding.DecodeString(header.X5C[0]) + if err != nil { + return nil, fmt.Errorf("%w: x5c[0] is not base64", ErrMalformedSignature) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, fmt.Errorf("%w: x5c[0]: %w", ErrMalformedSignature, err) + } + return cert, nil +} + +func decodeClaims(encoded string) (*SignedBatchClaims, error) { + raw, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("%w: claims are not base64url", ErrMalformedSignature) + } + var claims SignedBatchClaims + if err := json.Unmarshal(raw, &claims); err != nil { + return nil, fmt.Errorf("%w: claims are not JSON", ErrMalformedSignature) + } + return &claims, nil +} + +func bodyDigest(body []byte) string { + sum := sha256.Sum256(body) + return base64.RawURLEncoding.EncodeToString(sum[:]) +} diff --git a/pkg/provenance/signer_test.go b/pkg/provenance/signer_test.go new file mode 100644 index 0000000..eff1820 --- /dev/null +++ b/pkg/provenance/signer_test.go @@ -0,0 +1,308 @@ +package provenance + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "math/big" + "strings" + "testing" + "time" +) + +type testIdentity struct { + key *ecdsa.PrivateKey + cert *x509.Certificate + keyPEM []byte + certPEM []byte +} + +func selfSignedDER(t *testing.T, signer crypto.Signer) []byte { + t.Helper() + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "ans-openshell test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer) + if err != nil { + t.Fatalf("CreateCertificate() error = %v", err) + } + return der +} + +func newTestIdentity(t *testing.T, curve elliptic.Curve) testIdentity { + t.Helper() + key, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + t.Fatalf("GenerateKey() error = %v", err) + } + der := selfSignedDER(t, key) + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("ParseCertificate() error = %v", err) + } + pkcs8, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("MarshalPKCS8PrivateKey() error = %v", err) + } + return testIdentity{ + key: key, + cert: cert, + keyPEM: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}), + certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), + } +} + +func sec1PEM(t *testing.T, key *ecdsa.PrivateKey) []byte { + t.Helper() + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatalf("MarshalECPrivateKey() error = %v", err) + } + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) +} + +func ed25519PKCS8PEM(t *testing.T) []byte { + t.Helper() + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519.GenerateKey() error = %v", err) + } + der, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("MarshalPKCS8PrivateKey() error = %v", err) + } + return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}) +} + +// signParts builds a compact JWS from already-encoded header and payload +// segments, signed with key, so tests can present well-signed but otherwise +// bad tokens to Verify. +func signParts(t *testing.T, encodedHeader, encodedPayload string, key *ecdsa.PrivateKey) string { + t.Helper() + input := encodedHeader + "." + encodedPayload + digest := sha256.Sum256([]byte(input)) + r, s, err := ecdsa.Sign(rand.Reader, key, digest[:]) + if err != nil { + t.Fatalf("ecdsa.Sign() error = %v", err) + } + sig := make([]byte, es256SignatureLen) + r.FillBytes(sig[:es256CoordinateLen]) + s.FillBytes(sig[es256CoordinateLen:]) + return input + "." + base64.RawURLEncoding.EncodeToString(sig) +} + +func b64url(v any) string { + raw, err := json.Marshal(v) + if err != nil { + panic(err) + } + return base64.RawURLEncoding.EncodeToString(raw) +} + +func headerFor(cert *x509.Certificate) map[string]any { + return map[string]any{"typ": BatchSignatureType, "alg": BatchSignatureAlg, "x5c": []string{base64.StdEncoding.EncodeToString(cert.Raw)}} +} + +func TestNewPEMSigner(t *testing.T) { + t.Parallel() + p256 := newTestIdentity(t, elliptic.P256()) + p384 := newTestIdentity(t, elliptic.P384()) + other := newTestIdentity(t, elliptic.P256()) + tests := []struct { + name string + keyPEM []byte + certPEM []byte + wantErr string + }{ + {name: "pkcs8", keyPEM: p256.keyPEM, certPEM: p256.certPEM}, + {name: "sec1", keyPEM: sec1PEM(t, p256.key), certPEM: p256.certPEM}, + {name: "no key pem", keyPEM: []byte("not pem"), certPEM: p256.certPEM, wantErr: "no PEM block"}, + {name: "unsupported key block", keyPEM: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: []byte{1}}), certPEM: p256.certPEM, wantErr: `unsupported PEM block "RSA PRIVATE KEY"`}, + {name: "pkcs8 garbage", keyPEM: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte{1, 2, 3}}), certPEM: p256.certPEM, wantErr: "provenance: key:"}, + {name: "sec1 garbage", keyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: []byte{1, 2, 3}}), certPEM: p256.certPEM, wantErr: "provenance: key:"}, + {name: "ed25519 key", keyPEM: ed25519PKCS8PEM(t), certPEM: p256.certPEM, wantErr: "P-256"}, + {name: "p384 key", keyPEM: p384.keyPEM, certPEM: p384.certPEM, wantErr: "P-256"}, + {name: "no cert pem", keyPEM: p256.keyPEM, certPEM: nil, wantErr: "provenance: certificate"}, + {name: "wrong cert block", keyPEM: p256.keyPEM, certPEM: pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: p256.cert.Raw}), wantErr: "provenance: certificate"}, + {name: "cert garbage", keyPEM: p256.keyPEM, certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte{1, 2, 3}}), wantErr: "provenance: certificate:"}, + {name: "cert for another key", keyPEM: p256.keyPEM, certPEM: other.certPEM, wantErr: "does not match"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + s, err := NewPEMSigner(tt.keyPEM, tt.certPEM) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("NewPEMSigner() error = %v, want containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("NewPEMSigner() error = %v", err) + } + if !s.Certificate().Equal(p256.cert) { + t.Fatal("Certificate() is not the configured certificate") + } + }) + } +} + +func TestSignVerifyRoundTrip(t *testing.T) { + t.Parallel() + id := newTestIdentity(t, elliptic.P256()) + signer, err := NewPEMSigner(id.keyPEM, id.certPEM) + if err != nil { + t.Fatalf("NewPEMSigner() error = %v", err) + } + body := []byte(`{"observations":[{"agentId":"a"}]}`) + iat := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) + + jws, err := signer.Sign(body, iat, 1, "ans://v1.0.0.ops.example.com") + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + parts := strings.Split(jws, ".") + if len(parts) != 3 { + t.Fatalf("jws has %d parts", len(parts)) + } + rawHeader, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + t.Fatalf("header is not base64url: %v", err) + } + var header map[string]any + if err := json.Unmarshal(rawHeader, &header); err != nil { + t.Fatalf("header is not JSON: %v", err) + } + x5c, _ := header["x5c"].([]any) + if header["typ"] != "ans-observations+jws" || header["alg"] != "ES256" || len(header) != 3 || len(x5c) != 1 || + x5c[0] != base64.StdEncoding.EncodeToString(id.cert.Raw) { + t.Fatalf("header = %v", header) + } + if sig, decodeErr := base64.RawURLEncoding.DecodeString(parts[2]); decodeErr != nil || len(sig) != 64 { + t.Fatalf("signature = %d bytes, %v", len(sig), decodeErr) + } + + claims, cert, err := Verify(jws, body) + if err != nil { + t.Fatalf("Verify() error = %v", err) + } + sum := sha256.Sum256(body) + want := SignedBatchClaims{ + BodySHA256: base64.RawURLEncoding.EncodeToString(sum[:]), + Count: 1, + IssuedAt: iat.Unix(), + AIMID: "ans://v1.0.0.ops.example.com", + } + if *claims != want { + t.Fatalf("claims = %+v, want %+v", *claims, want) + } + if !cert.Equal(id.cert) { + t.Fatal("Verify() returned a different certificate") + } + rawClaims, _ := base64.RawURLEncoding.DecodeString(parts[1]) + if !strings.Contains(string(rawClaims), `"body_sha256":"`) || !strings.Contains(string(rawClaims), `"aim_id":"`) || !strings.Contains(string(rawClaims), `"iat":`) { + t.Fatalf("claims wire = %s", rawClaims) + } +} + +func TestVerifyRejects(t *testing.T) { + t.Parallel() + id := newTestIdentity(t, elliptic.P256()) + other := newTestIdentity(t, elliptic.P256()) + p384 := newTestIdentity(t, elliptic.P384()) + _, edKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519.GenerateKey() error = %v", err) + } + edCert, err := x509.ParseCertificate(selfSignedDER(t, edKey)) + if err != nil { + t.Fatalf("ParseCertificate() error = %v", err) + } + signer, err := NewPEMSigner(id.keyPEM, id.certPEM) + if err != nil { + t.Fatalf("NewPEMSigner() error = %v", err) + } + body := []byte(`{"observations":[]}`) + good, err := signer.Sign(body, time.Now(), 0, "aim") + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + goodParts := strings.Split(good, ".") + validClaims := b64url(SignedBatchClaims{BodySHA256: bodyDigest(body)}) + withHeader := func(h map[string]any) string { return signParts(t, b64url(h), validClaims, id.key) } + tamperedClaims := func(h map[string]any) string { + hdr := headerFor(id.cert) + for k, v := range h { + hdr[k] = v + } + return signParts(t, b64url(hdr), b64url(SignedBatchClaims{BodySHA256: bodyDigest(body), Count: 1}), id.key) + } + + tests := []struct { + name string + jws string + body []byte + wantErr error + wantMsg string + }{ + {name: "wrong body", jws: good, body: []byte("other"), wantErr: ErrBadSignature, wantMsg: "digest mismatch"}, + {name: "tampered claims", jws: goodParts[0] + "." + b64url(SignedBatchClaims{BodySHA256: bodyDigest(body), Count: 9}) + "." + goodParts[2], wantErr: ErrBadSignature}, + {name: "signed with another key", jws: signParts(t, b64url(headerFor(id.cert)), validClaims, other.key), wantErr: ErrBadSignature}, + {name: "x5c of another identity", jws: signParts(t, b64url(headerFor(other.cert)), validClaims, id.key), wantErr: ErrBadSignature}, + {name: "two parts", jws: goodParts[0] + "." + goodParts[1], wantErr: ErrMalformedSignature, wantMsg: "expected 3 parts"}, + {name: "header not base64url", jws: "!!!." + goodParts[1] + "." + goodParts[2], wantErr: ErrMalformedSignature, wantMsg: "header is not base64url"}, + {name: "header not json", jws: base64.RawURLEncoding.EncodeToString([]byte("{")) + "." + goodParts[1] + "." + goodParts[2], wantErr: ErrMalformedSignature, wantMsg: "header is not JSON"}, + {name: "bad alg", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": "RS256", "x5c": []string{"x"}}), wantErr: ErrMalformedSignature, wantMsg: `alg "RS256"`}, + {name: "none alg", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": "none", "x5c": []string{"x"}}), wantErr: ErrMalformedSignature, wantMsg: `alg "none"`}, + {name: "bad typ", jws: withHeader(map[string]any{"typ": "JWT", "alg": BatchSignatureAlg, "x5c": []string{"x"}}), wantErr: ErrMalformedSignature, wantMsg: `typ "JWT"`}, + {name: "missing x5c", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": BatchSignatureAlg}), wantErr: ErrMalformedSignature, wantMsg: "x5c is empty"}, + {name: "empty x5c", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": BatchSignatureAlg, "x5c": []string{}}), wantErr: ErrMalformedSignature, wantMsg: "x5c is empty"}, + {name: "x5c not base64", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": BatchSignatureAlg, "x5c": []string{"%%%"}}), wantErr: ErrMalformedSignature, wantMsg: "x5c[0] is not base64"}, + {name: "x5c not a certificate", jws: withHeader(map[string]any{"typ": BatchSignatureType, "alg": BatchSignatureAlg, "x5c": []string{base64.StdEncoding.EncodeToString([]byte("junk"))}}), wantErr: ErrMalformedSignature, wantMsg: "x5c[0]:"}, + {name: "p384 certificate", jws: withHeader(headerFor(p384.cert)), wantErr: ErrMalformedSignature, wantMsg: "not ECDSA P-256"}, + {name: "ed25519 certificate", jws: withHeader(headerFor(edCert)), wantErr: ErrMalformedSignature, wantMsg: "not ECDSA P-256"}, + {name: "signature wrong length", jws: goodParts[0] + "." + goodParts[1] + "." + base64.RawURLEncoding.EncodeToString(make([]byte, 32)), wantErr: ErrMalformedSignature, wantMsg: "64 base64url bytes"}, + {name: "signature not base64url", jws: goodParts[0] + "." + goodParts[1] + ".%%%", wantErr: ErrMalformedSignature, wantMsg: "64 base64url bytes"}, + {name: "claims not base64url", jws: signParts(t, b64url(headerFor(id.cert)), "%%%", id.key), wantErr: ErrMalformedSignature, wantMsg: "claims are not base64url"}, + {name: "claims not json", jws: signParts(t, b64url(headerFor(id.cert)), base64.RawURLEncoding.EncodeToString([]byte("{")), id.key), wantErr: ErrMalformedSignature, wantMsg: "claims are not JSON"}, + {name: "count mismatch is not checked here", jws: tamperedClaims(nil), body: nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + useBody := body + if tt.body != nil { + useBody = tt.body + } + claims, cert, err := Verify(tt.jws, useBody) + if tt.wantErr == nil { + if err != nil || claims == nil || cert == nil { + t.Fatalf("Verify() = %v, %v, %v; want success", claims, cert, err) + } + return + } + if !errors.Is(err, tt.wantErr) { + t.Fatalf("Verify() error = %v, want %v", err, tt.wantErr) + } + if tt.wantMsg != "" && !strings.Contains(err.Error(), tt.wantMsg) { + t.Fatalf("Verify() error = %q, want containing %q", err, tt.wantMsg) + } + if claims != nil || cert != nil { + t.Fatalf("Verify() returned %v, %v alongside an error", claims, cert) + } + }) + } +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..201ffcb --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "go", + "changelog-path": "CHANGELOG.md", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "extra-files": [ + { "type": "yaml", "path": "deploy/helm/ans-openshell/Chart.yaml", "jsonpath": "$.version" }, + { "type": "yaml", "path": "deploy/helm/ans-openshell/Chart.yaml", "jsonpath": "$.appVersion" } + ], + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation" }, + { "type": "chore", "section": "Miscellaneous" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ] + } + } +} diff --git a/third_party/openshell/LICENSE b/third_party/openshell/LICENSE new file mode 100644 index 0000000..f13d24c --- /dev/null +++ b/third_party/openshell/LICENSE @@ -0,0 +1,203 @@ +Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/openshell/README.md b/third_party/openshell/README.md new file mode 100644 index 0000000..d8e81dd --- /dev/null +++ b/third_party/openshell/README.md @@ -0,0 +1,10 @@ +# OpenShell protocol definitions + +The `.proto` files in `proto/` are copied verbatim from +[NVIDIA/OpenShell](https://github.com/NVIDIA/OpenShell) (`proto/` directory) at +the revision recorded in `VERSION`. They are licensed under the Apache License 2.0 +(see `LICENSE`) and are not covered by this repository's MIT license. + +Only the files needed by the supervisor-middleware and gateway-interceptor +contracts, and their transitive imports, are vendored. Go bindings are generated +into `gen/openshell` with `make proto`. diff --git a/third_party/openshell/VERSION b/third_party/openshell/VERSION new file mode 100644 index 0000000..1c0c009 --- /dev/null +++ b/third_party/openshell/VERSION @@ -0,0 +1,3 @@ +commit=c3ca45f1dd2848a01309569944e06977e73ff47b +date=2026-10-15T17:14:54Z + diff --git a/third_party/openshell/proto/datamodel.proto b/third_party/openshell/proto/datamodel.proto new file mode 100644 index 0000000..7506360 --- /dev/null +++ b/third_party/openshell/proto/datamodel.proto @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.datamodel.v1; + +import "options.proto"; +import "google/protobuf/timestamp.proto"; + +// Selects the workspace scope for a public API request. +// +// Requests that operate on one workspace require a non-empty `workspace`. +// Cross-workspace list requests additionally accept `all_workspaces`. The +// containing request documents which selections it supports; an omitted +// selector is invalid for workspace-scoped operations. +message WorkspaceSelector { + oneof selection { + // One explicitly named workspace. Use `default` to select the gateway's + // default workspace; an empty name is invalid. + string workspace = 1; + // All workspaces the caller is authorized to access. Only supported by + // requests that explicitly document cross-workspace behavior. + AllWorkspaces all_workspaces = 2; + } +} + +// Marker for the all-workspaces selector variant. +message AllWorkspaces {} + +// Kubernetes-style metadata shared by all top-level OpenShell domain objects. +// +// This structure provides consistent metadata (identity, labels, annotations, +// timestamps, resource versioning) across Sandbox, Provider, SshSession, and +// other resources. +message ObjectMeta { + reserved 3, 8; + reserved "created_at_ms", "deletion_timestamp_ms"; + // Stable object ID generated by the gateway. + string id = 1; + + // Human-readable object name (unique per object type). + string name = 2; + + // Time when the object was created. + google.protobuf.Timestamp created_time = 103; + + // Key-value labels for filtering and organization. + // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. + map labels = 4; + + // Optimistic concurrency control version. + // Incremented by the gateway on each update. Clients can use this for compare-and-swap operations. + uint64 resource_version = 5; + + // Opaque key-value metadata that is not used for selectors. + // Annotation keys use the same qualified-key shape as labels, but values may be longer. + map annotations = 6; + + // Workspace that owns this resource. Empty is normalized to "default" by the + // gateway. Immutable after creation. + string workspace = 7; + + // Time when graceful deletion was initiated. Absence means the object is + // not being deleted. Once set, this field is + // immutable — the only path forward is completing deletion. + google.protobuf.Timestamp deletion_time = 108; +} + +// Phase of a workspace's lifecycle. +enum WorkspacePhase { + WORKSPACE_PHASE_UNSPECIFIED = 0; + WORKSPACE_PHASE_ACTIVE = 1; + WORKSPACE_PHASE_TERMINATING = 2; +} + +// Status of a workspace. +message WorkspaceStatus { + WorkspacePhase phase = 1; +} + +// Workspace resource. A hard isolation boundary for sandboxes, providers, and +// other workspace-scoped resources. +message Workspace { + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + // The workspace field in this ObjectMeta is unused (a workspace does not + // belong to another workspace). + ObjectMeta metadata = 1; + + // Current lifecycle status. + WorkspaceStatus status = 2; +} + +// Opaque handle for a provider credential stored by gateway credential storage. +// Handles are created by OpenShell and must not be authored by users. +message CredentialHandle { + // Internal storage owner or credential driver that owns this handle. + string driver = 1; + // Owner-owned opaque handle string. + string handle = 2; + // Owner-owned non-secret metadata. + map metadata = 3; +} + +// Provider model stored by OpenShell. +message Provider { + reserved 5; + reserved "credential_expires_at_ms"; + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + ObjectMeta metadata = 1; + // ID of the provider profile this provider was created from, matched + // exactly (for example: "claude-code", "github"). + string type = 2; + // Secret values used for authentication. + map credentials = 3 [(openshell.options.v1.secret) = true]; + // Non-secret provider configuration. + map config = 4; + // Expiration times for credential values, keyed by credential/env var name. + // A missing key means the credential does not expire. + map credential_expiration_times = 105; + // Workspace where this provider's type profile is stored. + // Empty string = platform/global scope. Must be empty or match + // metadata.workspace; cross-workspace references are rejected. + string profile_workspace = 6; + // Opaque handles for secret values stored through gateway credential storage. + // This map is internal gateway state and is not accepted as user-authored input. + map credential_handles = 7; +} diff --git a/third_party/openshell/proto/extension.proto b/third_party/openshell/proto/extension.proto new file mode 100644 index 0000000..7c505f1 --- /dev/null +++ b/third_party/openshell/proto/extension.proto @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.extension.v1; + +// Version of an OpenShell extension protocol contract. +// +// The major version changes when peers cannot safely interoperate. Minor +// versions add optional fields or capabilities and remain compatible when both +// peers' required capabilities are satisfied. +message ProtocolVersion { + uint32 major = 1; + uint32 minor = 2; +} + +// Non-secret metadata exchanged by the gateway and an extension before use. +message PeerMetadata { + // Extension-family protocol contract version, distinct from the build. + ProtocolVersion protocol_version = 1; + + // Stable human-readable implementation identity, such as "openshell/docker". + string implementation_name = 2; + + // Implementation or build version used only for diagnostics. + string implementation_version = 3; + + // Optional namespaced capabilities this peer understands. + repeated string supported_capabilities = 4; + + // Capabilities that the opposite peer must advertise. + repeated string required_capabilities = 5; +} diff --git a/third_party/openshell/proto/gateway_interceptor.proto b/third_party/openshell/proto/gateway_interceptor.proto new file mode 100644 index 0000000..cf55619 --- /dev/null +++ b/third_party/openshell/proto/gateway_interceptor.proto @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.gateway_interceptor.v1; + +import "google/protobuf/struct.proto"; +import "extension.proto"; +import "openshell.proto"; + +// GatewayInterceptor lets an external governance service evaluate gateway +// control-plane operations after OpenShell admission and before or after the +// gateway applies the operation. +service GatewayInterceptor { + // Describe returns the interceptor manifest and declared bindings. + rpc Describe(DescribeRequest) returns (InterceptorManifest); + + // SnapshotProviderProfiles returns the interceptor's current provider + // profile snapshot when the manifest advertises provider_profiles = true. + rpc SnapshotProviderProfiles(ProviderProfileSnapshotRequest) + returns (ProviderProfileSnapshot); + + // Evaluate returns an allow, deny, or mutation decision for one operation + // phase. + rpc Evaluate(InterceptorEvaluation) returns (InterceptorResult); +} + +message DescribeRequest { + // Gateway protocol metadata. Interceptors must reject unmet requirements. + openshell.extension.v1.PeerMetadata gateway = 1; +} + +message ProviderProfileSnapshotRequest {} + +enum GatewayInterceptorPhase { + GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED = 0; + GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION = 2; + GATEWAY_INTERCEPTOR_PHASE_VALIDATE = 3; + GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT = 4; +} + +message InterceptorEvaluation { + // Configured interceptor instance name. + string interceptor_name = 1; + // Manifest binding id selected for this evaluation. + string binding_id = 2; + // Public gRPC service name, e.g. "openshell.v1.OpenShell". + string service = 3; + // Public gRPC method name, e.g. "CreateSandbox". + string method = 4; + // Caller identity summary. Values are intentionally non-secret. + map principal = 5; + + // Exactly one phase payload is set for each evaluation. + oneof phase { + // Evaluation that may modify the prepared gateway operation. + ModifyOperationEvaluation modify_operation = 6; + // Evaluation that may reject, but not mutate, the prepared operation. + ValidateEvaluation validate = 7; + // Evaluation after the gateway operation has completed successfully. + PostCommitEvaluation post_commit = 8; + } +} + +message ModifyOperationEvaluation { + // Gateway-prepared operation the gateway proposes to execute. + google.protobuf.Struct proposed_operation = 1; +} + +message ValidateEvaluation { + // Gateway-prepared operation after earlier interceptor modifications. + google.protobuf.Struct proposed_operation = 1; + // Optional read-only gateway state loaded before applying the operation. + google.protobuf.Struct current_state = 2; +} + +message PostCommitEvaluation { + // Protobuf JSON-shaped response returned by the committed gateway operation. + google.protobuf.Struct committed_response = 1; +} + +message InterceptorResult { + // False denies the operation before side effects for modify_operation and + // validate. Post-commit denial is invalid. + bool allowed = 1; + // Human-readable reason for logs and denied gRPC status messages. + string reason = 2; + // Optional gRPC status code name for denials, e.g. "PERMISSION_DENIED". + string status_code = 3; + // RFC 6902 JSON patches. Only valid during modify_operation. + repeated JsonPatch patches = 4; + // Non-secret annotations included in gateway logs. + map log_annotations = 5; +} + +message InterceptorManifest { + // Human-readable interceptor name declared by the service. + string name = 1; + // Bindings declared by the interceptor service. + repeated InterceptorBinding bindings = 2; + // Optional default failure policy for bindings without their own policy. + // Supported values are "fail_closed" and "fail_open". + string failure_policy = 3; + // True when this interceptor implements SnapshotProviderProfiles. + bool provider_profiles = 4; + // Exact JWT audience this service verifies on inbound OpenShell calls. + // After authenticated Describe succeeds, the gateway refuses to start unless + // this matches the operator-configured audience. A strict verifier may reject + // an incorrect audience before returning this manifest. Empty skips this + // post-authentication consistency check. + string expected_audience = 5; + // Gateway-interceptor protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.gateway-interceptor.contract" in + // supported_capabilities. + openshell.extension.v1.PeerMetadata extension = 6; +} + +message ProviderProfileSnapshot { + // Opaque source revision used for cache freshness and sandbox reload checks. + string revision = 1; + // Complete profile snapshot vended by this source. + repeated openshell.v1.ProviderProfile profiles = 2; +} + +message InterceptorBinding { + // Stable binding id used for config overrides and audit logs. + string id = 1; + // RPC selector. Selectors are intentionally tied to the public API shape. + InterceptorSelector selector = 2; + // Phases this binding wants to evaluate. + repeated GatewayInterceptorPhase phases = 3; + // Optional binding-specific failure policy. + // Supported values are "fail_closed" and "fail_open". + // Bindings that include post_commit must resolve to "fail_open" because the + // gateway operation has already committed before that phase runs. + string failure_policy = 4; +} + +message InterceptorSelector { + // Full selector form: "openshell.v1.OpenShell/CreateSandbox". + string rpc = 1; + // Structured service/method form. If rpc is set, it takes precedence. + string service = 2; + string method = 3; +} + +message JsonPatch { + string op = 1; + string path = 2; + google.protobuf.Value value = 3; + string from = 4; +} diff --git a/third_party/openshell/proto/openshell.proto b/third_party/openshell/proto/openshell.proto new file mode 100644 index 0000000..a4c32bc --- /dev/null +++ b/third_party/openshell/proto/openshell.proto @@ -0,0 +1,3913 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.v1; + +import "datamodel.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "options.proto"; +import "sandbox.proto"; + +// OpenShell service provides sandbox, provider, and runtime management capabilities. +// +// Conventions: +// - This file owns the public API resource model exposed to OpenShell clients. +// - `Sandbox`, `SandboxSpec`, `SandboxStatus`, and `SandboxPhase` are gateway-owned +// public types. Internal compute drivers must not import or return them directly. +// - The gateway translates internal compute-driver observations into these public +// resource messages before persisting or returning them to clients. +service OpenShell { + // Check the health of the service. + rpc Health(HealthRequest) returns (HealthResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "unauthenticated" + }; + } + + // Return the authenticated caller identity established by the gateway. + rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + }; + } + + // Fetch elevated live gateway runtime metadata. + rpc GetGatewayInfo(GetGatewayInfoRequest) returns (GetGatewayInfoResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:read" + global_role: "platform_admin" + }; + } + + // Create a new sandbox. + rpc CreateSandbox(CreateSandboxRequest) returns (SandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Allocate a gateway-owned staging slot for a local rootfs tar archive. + // + // The gateway creates a request-scoped directory inside the compute driver's + // staging root and returns an opaque single-use token plus the absolute path + // the client must write the archive to. The token is then passed as + // `template.driver_config..rootfs_tar_staging_token` on + // CreateSandbox; callers never name a filesystem path themselves. Only a + // client sharing the gateway's filesystem can complete the upload. + rpc BeginRootfsTarStaging(BeginRootfsTarStagingRequest) + returns (BeginRootfsTarStagingResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Fetch a sandbox by name. + rpc GetSandbox(GetSandboxRequest) returns (SandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // List sandboxes. + rpc ListSandboxes(ListSandboxesRequest) returns (ListSandboxesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Create a reusable sandbox workload template. + rpc CreateSandboxTemplate(CreateSandboxTemplateRequest) + returns (SandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "admin" + }; + } + + // Fetch a reusable sandbox workload template by name. + rpc GetSandboxTemplate(GetSandboxTemplateRequest) + returns (SandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // List reusable sandbox workload templates. + rpc ListSandboxTemplates(ListSandboxTemplatesRequest) + returns (ListSandboxTemplatesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Delete a reusable sandbox workload template by name. + rpc DeleteSandboxTemplate(DeleteSandboxTemplateRequest) + returns (DeleteSandboxTemplateResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "admin" + }; + } + + // List provider records attached to a sandbox. + rpc ListSandboxProviders(ListSandboxProvidersRequest) + returns (ListSandboxProvidersResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Attach a provider record to an existing sandbox. + rpc AttachSandboxProvider(AttachSandboxProviderRequest) + returns (AttachSandboxProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Detach a provider record from an existing sandbox. + rpc DetachSandboxProvider(DetachSandboxProviderRequest) + returns (DetachSandboxProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Inspect the installed authority for one sandbox provider mutation. + rpc GetSandboxProviderStatus(GetSandboxProviderStatusRequest) + returns (GetSandboxProviderStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Delete a sandbox by name. + rpc DeleteSandbox(DeleteSandboxRequest) returns (DeleteSandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Stop a sandbox while retaining its persistent state. + rpc StopSandbox(StopSandboxRequest) returns (SandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Start a previously stopped sandbox. + rpc StartSandbox(StartSandboxRequest) returns (SandboxResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Create a short-lived SSH session for a sandbox. + rpc CreateSshSession(CreateSshSessionRequest) returns (CreateSshSessionResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Create or update a sandbox HTTP service endpoint for local routing. + rpc ExposeService(ExposeServiceRequest) returns (ServiceEndpointResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Fetch one sandbox HTTP service endpoint. + rpc GetService(GetServiceRequest) returns (ServiceEndpointResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // List sandbox HTTP service endpoints. + rpc ListServices(ListServicesRequest) returns (ListServicesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Delete one sandbox HTTP service endpoint. + rpc DeleteService(DeleteServiceRequest) returns (DeleteServiceResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Revoke a previously issued SSH session. + rpc RevokeSshSession(RevokeSshSessionRequest) returns (RevokeSshSessionResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Execute a command in a ready sandbox and stream output. + rpc ExecSandbox(ExecSandboxRequest) returns (stream ExecSandboxEvent) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + rpc ForwardTcp(stream TcpForwardFrame) returns (stream TcpForwardFrame) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + rpc ExecSandboxInteractive(stream ExecSandboxInput) returns (stream ExecSandboxEvent) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Create a provider. + rpc CreateProvider(CreateProviderRequest) returns (ProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Fetch a provider by name. + rpc GetProvider(GetProviderRequest) returns (ProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // 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) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // List available provider type profiles. + rpc ListProviderProfiles(ListProviderProfilesRequest) + returns (ListProviderProfilesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // Fetch one provider type profile by id. + rpc GetProviderProfile(GetProviderProfileRequest) + returns (ProviderProfileResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // Import custom provider type profiles. + rpc ImportProviderProfiles(ImportProviderProfilesRequest) + returns (ImportProviderProfilesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Update an existing custom provider type profile. + rpc UpdateProviderProfiles(UpdateProviderProfilesRequest) + returns (UpdateProviderProfilesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Validate provider type profiles without registering them. + rpc LintProviderProfiles(LintProviderProfilesRequest) + returns (LintProviderProfilesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // Update an existing provider by name. + rpc UpdateProvider(UpdateProviderRequest) returns (ProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Fetch refresh status for one provider or provider credential. + rpc GetProviderRefreshStatus(GetProviderRefreshStatusRequest) + returns (GetProviderRefreshStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + workspace_role: "user" + }; + } + + // Configure gateway-owned refresh material for one provider credential. + rpc ConfigureProviderRefresh(ConfigureProviderRefreshRequest) + returns (ConfigureProviderRefreshResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Record a gateway-owned refresh request for one provider credential. + rpc RotateProviderCredential(RotateProviderCredentialRequest) + returns (RotateProviderCredentialResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Delete gateway-owned refresh configuration for one provider credential. + rpc DeleteProviderRefresh(DeleteProviderRefreshRequest) + returns (DeleteProviderRefreshResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Delete a provider by name. + rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Delete a custom provider type profile by id. + rpc DeleteProviderProfile(DeleteProviderProfileRequest) + returns (DeleteProviderProfileResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + workspace_role: "admin" + }; + } + + // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + rpc GetSandboxConfig(openshell.sandbox.v1.GetSandboxConfigRequest) + returns (openshell.sandbox.v1.GetSandboxConfigResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "dual" + scope: "config:read" + workspace_role: "user" + }; + } + + // Get gateway-global settings (read-only runtime configuration; any + // authenticated user may read these without requiring Platform Admin). + // + // Scope-only (no role): scopes are granted by the IdP at token issuance, + // orthogonal to workspace membership. Deployments that enable scope + // enforcement configure the IdP to grant config:read (or openshell:all) + // to all sandbox users, so this does not block least-privilege flows. + rpc GetGatewayConfig(openshell.sandbox.v1.GetGatewayConfigRequest) + returns (openshell.sandbox.v1.GetGatewayConfigResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:read" + }; + } + + // Update settings or policy at sandbox or global scope. + rpc UpdateConfig(UpdateConfigRequest) + returns (UpdateConfigResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "dual" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Get the load status of a specific policy version. + rpc GetSandboxPolicyStatus(GetSandboxPolicyStatusRequest) + returns (GetSandboxPolicyStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // List policy history for a sandbox. + rpc ListSandboxPolicies(ListSandboxPoliciesRequest) + returns (ListSandboxPoliciesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Report policy load result (called by sandbox after reload attempt). + rpc ReportPolicyStatus(ReportPolicyStatusRequest) + returns (ReportPolicyStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Replace the gateway's observed tool server endpoint status for one sandbox. + rpc ReportEndpointStatus(ReportEndpointStatusRequest) + returns (ReportEndpointStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Report installed provider state for the current ConnectSupervisor session. + // Replacing or losing that session invalidates its observations. + rpc ReportProviderReadiness(ReportProviderReadinessRequest) + returns (ReportProviderReadinessResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Register startup and acknowledge an exact validated runtime configuration. + rpc ReportSandboxConfiguration(ReportSandboxConfigurationRequest) + returns (ReportSandboxConfigurationResponse) { + option (openshell.options.v1.authorization) = { auth_mode: "sandbox" }; + } + + // Get provider environment for a sandbox (called by sandbox supervisor at startup). + rpc GetSandboxProviderEnvironment(GetSandboxProviderEnvironmentRequest) + returns (GetSandboxProviderEnvironmentResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + rpc ExchangeProviderSubjectToken(ExchangeProviderSubjectTokenRequest) + returns (ExchangeProviderSubjectTokenResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Fetch recent sandbox logs (one-shot). + rpc GetSandboxLogs(GetSandboxLogsRequest) returns (GetSandboxLogsResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Push sandbox supervisor logs to the server (client-streaming). + rpc PushSandboxLogs(stream PushSandboxLogsRequest) returns (PushSandboxLogsResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Persistent supervisor-to-gateway session (bidirectional streaming). + // + // The supervisor opens this stream at startup and keeps it alive for the + // sandbox lifetime. The gateway uses it to coordinate relay channels for + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. + rpc ConnectSupervisor(stream SupervisorMessage) returns (stream GatewayMessage) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Persist the canonical main process result before the supervisor exits. + rpc ReportMainProcessExit(ReportMainProcessExitRequest) returns (ReportMainProcessExitResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Confirm that foreground terminal delivery completed naturally. + rpc FinalizeMainProcessExit(FinalizeMainProcessExitRequest) + returns (FinalizeMainProcessExitResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Raw byte relay between supervisor and gateway. + // + // The supervisor initiates this call after receiving a RelayOpen message + // on its ConnectSupervisor stream. The first RelayFrame carries a + // RelayInit with the channel_id to associate the new HTTP/2 stream with + // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. + // + // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — + // no new TLS handshake, no reverse HTTP CONNECT. + rpc RelayStream(stream RelayFrame) returns (stream RelayFrame) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Internal gateway-to-gateway relay forwarding. + // + // A gateway replica that receives a user request for a sandbox whose + // supervisor session is owned by a different replica opens this stream to the + // owner. The first frame carries PeerRelayInit; subsequent frames carry raw + // bytes in either direction. This RPC is authenticated as a gateway peer, not + // as a user or sandbox supervisor. + rpc PeerRelay(stream PeerRelayFrame) returns (stream PeerRelayFrame) { + option (openshell.options.v1.authorization) = { + auth_mode: "peer" + }; + } + + // Internal gateway-to-owner forwarding for supervisor-bound state. + rpc PeerReportProviderReadiness(ReportProviderReadinessRequest) + returns (ReportProviderReadinessResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "peer" + }; + } + + rpc PeerReportEndpointStatus(ReportEndpointStatusRequest) + returns (ReportEndpointStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "peer" + }; + } + + rpc PeerGetSandboxProviderStatus(GetSandboxProviderStatusRequest) + returns (GetSandboxProviderStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "peer" + }; + } + + // Watch a sandbox and stream updates. + // + // This stream can include: + // - Sandbox status snapshots (phase/status) + // - OpenShell server process logs correlated by sandbox_id + // - Platform events correlated to the sandbox + rpc WatchSandbox(WatchSandboxRequest) returns (stream SandboxStreamEvent) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // --------------------------------------------------------------------------- + // Draft policy recommendation RPCs + // --------------------------------------------------------------------------- + + // Submit denial analysis results from sandbox (summaries + proposed chunks). + rpc SubmitPolicyAnalysis(SubmitPolicyAnalysisRequest) + returns (SubmitPolicyAnalysisResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Get draft policy recommendations for a sandbox. + rpc GetDraftPolicy(GetDraftPolicyRequest) returns (GetDraftPolicyResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "dual" + scope: "config:read" + workspace_role: "user" + }; + } + + // Approve a single draft policy chunk (merges into active policy). + rpc ApproveDraftChunk(ApproveDraftChunkRequest) + returns (ApproveDraftChunkResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Reject a single draft policy chunk. + rpc RejectDraftChunk(RejectDraftChunkRequest) + returns (RejectDraftChunkResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Approve all pending draft chunks (skips security-flagged unless forced). + rpc ApproveAllDraftChunks(ApproveAllDraftChunksRequest) + returns (ApproveAllDraftChunksResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Edit a pending draft chunk in-place (e.g. narrow allowed_ips). + rpc EditDraftChunk(EditDraftChunkRequest) returns (EditDraftChunkResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Reverse an approval (remove merged rule from active policy). + rpc UndoDraftChunk(UndoDraftChunkRequest) returns (UndoDraftChunkResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Clear all pending draft chunks for a sandbox. + rpc ClearDraftChunks(ClearDraftChunksRequest) + returns (ClearDraftChunksResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:write" + workspace_role: "admin" + }; + } + + // Get decision history for a sandbox's draft policy. + rpc GetDraftHistory(GetDraftHistoryRequest) returns (GetDraftHistoryResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "config:read" + workspace_role: "user" + }; + } + + // Exchange a sandbox-bootstrap credential (e.g. a Kubernetes projected + // ServiceAccount token) for a gateway-minted JWT bound to the calling + // sandbox's UUID. Used by the Kubernetes driver path; singleplayer + // drivers receive the gateway JWT directly from the create-sandbox flow + // and never call this RPC. + rpc IssueSandboxToken(IssueSandboxTokenRequest) returns (IssueSandboxTokenResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // Renew the calling sandbox's gateway JWT. Older tokens remain valid + // until their own expiry; deployments should keep token TTLs short to + // bound replay exposure. The supervisor calls this from a background + // task at ~80% of the token's lifetime; the new token is cached in + // memory only — the on-disk bootstrap file is intentionally not + // rewritten. + rpc RefreshSandboxToken(RefreshSandboxTokenRequest) + returns (RefreshSandboxTokenResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + + // --------------------------------------------------------------------------- + // Workspace management RPCs + // --------------------------------------------------------------------------- + + // Create a workspace. + rpc CreateWorkspace(CreateWorkspaceRequest) returns (CreateWorkspaceResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:write" + global_role: "platform_admin" + }; + } + + // Fetch a workspace by name. + rpc GetWorkspace(GetWorkspaceRequest) returns (GetWorkspaceResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:read" + workspace_role: "user" + }; + } + + // List workspaces. + rpc ListWorkspaces(ListWorkspacesRequest) returns (ListWorkspacesResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:read" + workspace_role: "user" + }; + } + + // Delete a workspace by name. + rpc DeleteWorkspace(DeleteWorkspaceRequest) returns (DeleteWorkspaceResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:write" + global_role: "platform_admin" + }; + } + + // Add a member to a workspace. + rpc AddWorkspaceMember(AddWorkspaceMemberRequest) returns (AddWorkspaceMemberResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:write" + workspace_role: "admin" + }; + } + + // Remove a member from a workspace. + rpc RemoveWorkspaceMember(RemoveWorkspaceMemberRequest) returns (RemoveWorkspaceMemberResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:write" + workspace_role: "admin" + }; + } + + // List members of a workspace. + rpc ListWorkspaceMembers(ListWorkspaceMembersRequest) returns (ListWorkspaceMembersResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "workspace:read" + workspace_role: "user" + }; + } +} + +// IssueSandboxToken request. Empty body; identity is established by the +// authentication credentials carried in the request headers (a projected +// Kubernetes ServiceAccount JWT in the K8s driver path). +message IssueSandboxTokenRequest {} + +// IssueSandboxToken response. The supervisor caches the returned token in +// memory and presents it as `Authorization: Bearer` on every subsequent +// gateway RPC. +message IssueSandboxTokenResponse { + reserved 2; + reserved "expires_at_ms"; + // Gateway-minted session JWT bound to the calling sandbox's UUID, active + // runtime generation, authorization epoch, and durable token lineage. + string token = 1 [(openshell.options.v1.secret) = true]; + // Absolute expiry of the issued token. Absence means the token is non-expiring. + google.protobuf.Timestamp expiration_time = 102; +} + +// RefreshSandboxToken request. The calling principal must already be a +// sandbox principal (i.e. the request carries a still-valid gateway-minted +// JWT in its Authorization header). Extension service names are resolved +// against server-owned registrations and the sandbox's effective policy; +// callers never choose token audiences directly. +message RefreshSandboxTokenRequest { + // Operator registration names for extension services selected by the + // sandbox's effective policy. + repeated string extension_service_names = 1; +} + +// RefreshSandboxToken response. The new token replaces the supervisor's +// in-memory bearer credential. +message RefreshSandboxTokenResponse { + reserved 2, 5; + reserved "expires_at_ms", "sandbox_expires_at_ms"; + // Fresh gateway-minted JWT bound to the same sandbox UUID. + string token = 1 [(openshell.options.v1.secret) = true]; + // Absolute expiry of the new token. Absence means the token is non-expiring. + google.protobuf.Timestamp expiration_time = 102; + // Fresh credentials for the requested, policy-authorized extension + // services. These remain in supervisor memory and are never persisted. + repeated ExtensionServiceCredential extension_credentials = 3; + // Fresh Sandbox Protocol bearer token from the same atomic refresh. + string sandbox_token = 4 [(openshell.options.v1.secret) = true]; + // Absolute Sandbox Protocol token expiry. Absence means the token is non-expiring. + google.protobuf.Timestamp sandbox_expiration_time = 105; + // Launch generation to which both refreshed credentials are bound. + string session_id = 6; + // Durable authorization epoch shared by the gateway and Sandbox Runtime. + uint64 credential_epoch = 7; +} + + +// Health check request. +message HealthRequest {} + +// Health check response. +message HealthResponse { + // Service status. + ServiceStatus status = 1; + + // Service version. + string version = 2; +} + +// Current-user request. The identity comes from the authenticated request. +message GetCurrentUserRequest {} + +// Authenticated user identity as validated by the gateway. +message GetCurrentUserResponse { + // Stable identity subject (for example, the OIDC `sub` claim). + string subject = 1; + + // Human-readable identity name when supplied by the authentication provider. + string display_name = 2; + + // Roles granted to the authenticated identity. + repeated string roles = 3; + + // OAuth2 scopes granted to the authenticated identity. + repeated string scopes = 4; + + // Authentication provider that established the identity. + string identity_provider = 5; +} + +// Gateway info request. +message GetGatewayInfoRequest {} + +// Gateway info response. +message GetGatewayInfoResponse { + // Service status. + ServiceStatus status = 1; + + // OpenShell gateway binary version. + string gateway_version = 2; + + // Compute driver runtimes initialized by this gateway. Current gateways + // return exactly one entry. + repeated ComputeDriverInfo compute_drivers = 3; + + // Negotiated non-secret metadata for every initialized extension. + repeated NegotiatedExtensionInfo extensions = 4; +} + +enum ExtensionKind { + EXTENSION_KIND_UNSPECIFIED = 0; + EXTENSION_KIND_COMPUTE_DRIVER = 1; + EXTENSION_KIND_CREDENTIAL_DRIVER = 2; + EXTENSION_KIND_GATEWAY_INTERCEPTOR = 3; + EXTENSION_KIND_SUPERVISOR_MIDDLEWARE = 4; +} + +// Public, non-secret snapshot of one successful startup negotiation. +message NegotiatedExtensionInfo { + ExtensionKind kind = 1; + // Gateway/operator-selected registration name. + string configured_name = 2; + // Extension-reported implementation identity. + string implementation_name = 3; + // Extension build version, distinct from the protocol version. + string implementation_version = 4; + uint32 protocol_major = 5; + uint32 protocol_minor = 6; + // Extension-supported optional capabilities, sorted for stable output. + repeated string supported_capabilities = 7; + // Capabilities the extension requires from the gateway. + repeated string required_capabilities = 8; +} + +// Info for one initialized compute driver runtime. +message ComputeDriverInfo { + // Gateway-selected driver name used for routing and driver_config keys. + string name = 1; + + // Capabilities reported by the driver during gateway runtime initialization. + ComputeDriverCapabilities capabilities = 2; +} + +// Public compute driver capability snapshot. +message ComputeDriverCapabilities { + // Driver-reported human-readable name from the startup capability snapshot. + string driver_name = 1; + + // Driver-reported implementation version from the startup capability snapshot. + string driver_version = 2; + + // Static portable resource request forms reported by the driver. + ResourceCapabilities resource_capabilities = 3; +} + +// Static portable resource request forms reported by a compute driver. +// An omitted domain means the driver does not report that domain. +message ResourceCapabilities { + CpuResourceCapabilities cpu = 1; + MemoryResourceCapabilities memory = 2; + GpuResourceCapabilities gpu = 3; +} + +message CpuResourceCapabilities { + // The driver accepts and enforces a portable CPU limit. + bool limit_supported = 1; +} + +message MemoryResourceCapabilities { + // The driver accepts and enforces a portable memory limit. + bool limit_supported = 1; +} + +message GpuResourceCapabilities { + // The driver accepts a GPU request with no explicit count. + bool default_selection_supported = 1; + // The driver accepts an explicit `gpu.count` request. + bool count_selection_supported = 2; +} + +// Public sandbox resource exposed by the OpenShell API. +// +// This is the canonical gateway-owned view of a sandbox. It merges user intent +// (`spec`) with gateway-managed metadata and status derived from internal +// compute-driver observations. +// +// Note: The `namespace` field has been removed from the public API. It remains +// in the internal `DriverSandbox` message as a compute-driver implementation detail. +message Sandbox { + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + openshell.datamodel.v1.ObjectMeta metadata = 1; + // Desired sandbox configuration submitted through the API. + SandboxSpec spec = 2; + // Latest user-facing observed status derived by the gateway. + SandboxStatus status = 3; + // Read-only SSH host identity in OpenSSH SHA256: form. Stable for + // this sandbox's lifetime, including runtime restarts. Empty on legacy + // runtimes that do not provide a gateway-managed SSH identity. + string host_key_fingerprint = 6; + // Read-only provenance for sandboxes created from a reusable workload template. + SandboxWorkloadTemplateProvenance created_from_workload_template = 20; + + reserved 4, 5; + reserved "phase", "current_policy_version"; +} + +// Desired sandbox configuration provided through the public API. +message SandboxSpec { + // Log level exposed to processes running inside the sandbox. + string log_level = 1; + // Environment variables injected into the sandbox runtime. + map environment = 5; + // Container or VM template used to provision the sandbox. + SandboxTemplate template = 6; + // Required sandbox policy configuration. + openshell.sandbox.v1.SandboxPolicy policy = 7; + // Provider names to attach to this sandbox. + repeated string providers = 8; + // Portable resource requirements used by the gateway for driver selection + // and by drivers for provisioning. + ResourceRequirements resource_requirements = 9; + reserved 10; + reserved "gpu_device"; + // Field 11 was `proposal_approval_mode`. The approval mode is now a + // runtime setting (gateway or sandbox scope) read via UpdateConfig / + // GetSandboxConfig, so it can be flipped on a running sandbox and + // managed fleet-wide. + reserved 11; + reserved "proposal_approval_mode"; + // Canonical command launched once by the sandbox supervisor. No shell + // parsing is performed. The gateway normalizes an omitted command to the + // portable scratch login shell before persistence. + repeated string command = 12; + // Allocate a retained pseudo-terminal for the main process. + bool tty = 13; + // Gateway-owned attachment identity, changed atomically with the provider set. + // Equality only: detach and reattach must not revive an older receipt. + string provider_attachment_epoch = 14; + // Gateway-owned policy for replacing the sandbox runtime after the canonical + // main process exits. Unspecified is normalized to Never before persistence. + SandboxRestartPolicy restart_policy = 15; +} + +message ResourceRequirements { + // GPU requirements for the sandbox. Presence indicates a GPU request. + GpuResourceRequirements gpu = 1; +} + +// Public GPU resource requirements. +message GpuResourceRequirements { + // Optional number of GPUs requested. When omitted, the request is for one + // GPU using the selected driver's default assignment behavior. + optional uint32 count = 1; +} + +// Historical inline compute template mapped onto compute-driver template inputs. +// +// Despite its name, this is not a reusable named sandbox template resource. It +// is an inline part of `SandboxSpec` kept for v1 compatibility. A future +// breaking API cleanup may rename this message to free `SandboxTemplate` for +// the reusable template resource now represented by `SandboxWorkloadTemplate`. +message SandboxTemplate { + // Fully-qualified OCI image reference used to boot the sandbox. + string image = 1; + // Optional runtime class name requested from the compute platform. + string runtime_class_name = 2; + // Optional agent socket path exposed to the workload. + string agent_socket = 3; + // Labels applied to compute-platform resources for this sandbox. + map labels = 4; + // Annotations applied to compute-platform resources for this sandbox. + map annotations = 5; + // Additional environment variables injected by the template. + map environment = 6; + // Platform-specific compute resource requirements and limits. + google.protobuf.Struct resources = 7; + reserved 9; + reserved "volume_claim_templates"; + // Enable Kubernetes user namespace isolation (hostUsers: false). + // When true, container UID 0 maps to a non-root host UID and capabilities + // become namespaced. Requires Kubernetes 1.33+ with user namespace support + // available (beta through 1.35, GA in 1.36+) and a supporting runtime. + // When unset, the cluster-wide default is used. + optional bool user_namespaces = 10; + // Driver-keyed opaque config envelope supplied by the caller. + // The gateway selects the block matching the active compute driver and + // forwards only that inner Struct to DriverSandboxTemplate.driver_config. + // The selected driver owns nested schema validation. + google.protobuf.Struct driver_config = 11; +} + +// Reusable named sandbox workload template resource. +// +// This is the actual workspace-scoped template resource used to create +// sandboxes by reference. It uses the longer name in v1 to avoid colliding with +// the historical inline `SandboxTemplate` message. A future breaking API +// cleanup may rename this resource to `SandboxTemplate`. +message SandboxWorkloadTemplate { + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + openshell.datamodel.v1.ObjectMeta metadata = 1; + // Desired reusable workload shape and template-owned driver config. + SandboxWorkloadTemplateSpec spec = 2; +} + +message SandboxWorkloadTemplateSpec { + // Portable workload shape. + SandboxWorkloadConfig workload = 1; + // Driver-keyed opaque config envelope supplied by the template owner. + google.protobuf.Struct driver_config = 2; + // Desired service level associated with this template. + SandboxServiceLevel desired_service_level = 3; +} + +message SandboxWorkloadConfig { + // Fully-qualified OCI image reference used to boot the sandbox. + string image = 1; + // Environment variables injected into the sandbox runtime. + map environment = 2; + // Portable resource requirements for sandboxes created from this workload. + SandboxResources resources = 3; +} + +message SandboxResources { + // Portable CPU quantity, for example "500m" or "2". + string cpu = 1; + // Portable memory quantity, for example "512Mi" or "2Gi". + string memory = 2; + // GPU requirements for the sandbox workload. Presence indicates a GPU + // request. When count is omitted, the request uses the selected driver's + // default GPU assignment behavior. + GpuResourceRequirements gpu = 3; +} + +message SandboxServiceLevel { + SandboxStartup startup = 1; +} + +message SandboxStartup { + google.protobuf.Duration ready_within = 1; + uint32 max_burst = 2; +} + +message SandboxWorkloadTemplateProvenance { + string name = 1; + string resource_version = 2; +} + +// User-facing sandbox status derived by the gateway from compute-driver observations. +// +// Public status does not embed driver-only flags such as `deleting`. +message SandboxStatus { + // Name of the agent pod or equivalent runtime instance. + string agent_pod = 2; + // File descriptor or endpoint for reaching the agent service, when available. + string agent_fd = 3; + // File descriptor or endpoint for reaching the sandbox service, when available. + string sandbox_fd = 4; + // Latest user-facing readiness and lifecycle conditions. + repeated SandboxCondition conditions = 5; + // Gateway-derived lifecycle summary. + SandboxPhase phase = 6; + // Currently active policy version (updated when sandbox reports loaded). + uint32 current_policy_version = 7; + // Supervisor instance currently associated with the canonical main process. + // The gateway uses this to reject stale exit reports after a restart. + string main_process_instance_id = 8; + // Most recent normalized main process result. Signal exits use 128 + signal number. + // Cleared when a replacement main process becomes ready. + optional int32 exit_code = 9; + // Last accepted network result for each configured tool server endpoint. + // Currently populated for MCP-over-HTTP endpoints. These passive results + // remain separate from sandbox lifecycle conditions and readiness. + repeated EndpointStatus endpoint_statuses = 10; + // Independent of infrastructure phase; retained across driver observations. + SandboxConfigurationAdmission configuration_admission = 11; + // Durable first-acceptance marker. Absent on legacy records; never reset by restart. + optional bool configuration_activated = 12; + // Gateway-owned provisioning deadlines, retained after timeout for inspection and retry. + SandboxProvisioning provisioning = 13; + // Consecutive policy-driven restart number in the current crash loop. + uint32 restart_count = 14; + // Deadline for the next restart attempt or recovery check. + google.protobuf.Timestamp next_restart_time = 15; + // Time when the current main process became ready. + google.protobuf.Timestamp main_process_started_time = 16; +} + +// User-facing sandbox condition derived from platform or gateway observations. +message SandboxCondition { + reserved 5; + reserved "last_transition_time"; + // Condition class, typically mirroring the underlying platform condition type. + string type = 1; + // Condition status value such as `True`, `False`, or `Unknown`. + string status = 2; + // Short machine-readable reason associated with the condition. + string reason = 3; + // Human-readable condition message. + string message = 4; + // Timestamp reported by the condition owner for the last transition. + google.protobuf.Timestamp transition_time = 105; +} + +// High-level sandbox lifecycle phase derived by the gateway. +// +// Clients should rely on this normalized lifecycle summary for readiness and +// deletion decisions instead of interpreting raw conditions. +enum SandboxPhase { + SANDBOX_PHASE_UNSPECIFIED = 0; + SANDBOX_PHASE_PROVISIONING = 1; + SANDBOX_PHASE_READY = 2; + SANDBOX_PHASE_ERROR = 3; + SANDBOX_PHASE_DELETING = 4; + SANDBOX_PHASE_UNKNOWN = 5; + SANDBOX_PHASE_STOPPING = 6; + SANDBOX_PHASE_STOPPED = 7; + SANDBOX_PHASE_STARTING = 8; + // The canonical main process exited successfully and its result is final. + SANDBOX_PHASE_COMPLETED = 9; + reserved 10; + reserved "SANDBOX_PHASE_RESTARTING"; +} + +// Public platform event exposed on the sandbox watch stream. +message PlatformEvent { + reserved 1; + reserved "timestamp_ms"; + // Time when the event occurred. + google.protobuf.Timestamp event_time = 101; + // Event source (e.g. "kubernetes", "docker", "process"). + string source = 2; + // Event type/severity (e.g. "Normal", "Warning"). + string type = 3; + // Short reason code (e.g. "Started", "Pulled", "Failed"). + string reason = 4; + // Human-readable event message. + string message = 5; + // Optional metadata as key-value pairs. + map metadata = 6; +} + +// Create sandbox request. +message CreateSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 7; + SandboxSpec spec = 1; + // Optional user-supplied sandbox name. When empty the server generates one. + string name = 2; + // Optional labels for the sandbox (key-value metadata). + map labels = 3; + // Optional annotations for the sandbox (non-selector metadata). + map annotations = 4; + // One-shot launch hint indicating that the creating client will attach to + // the canonical main process. The supervisor keeps the terminal transport + // alive until that attachment connects and closes naturally. + bool await_main_process_attachment = 5; + // Workspace-scoped SandboxWorkloadTemplate name to resolve at creation time. + string workload_template = 6; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 8; + // HTTP services to expose when the sandbox is created. Endpoints are + // registered after the sandbox has been persisted and route only while the + // sandbox is ready. + repeated SandboxServiceExposure service_exposures = 9; +} + +message CreateSandboxTemplateRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + SandboxWorkloadTemplate template = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +message GetSandboxTemplateRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string name = 1; +} + +message ListSandboxTemplatesRequest { + // Workspace scope. Named and all-workspaces selections are accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // The maximum number of templates to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 1; + // Token from a previous ListSandboxTemplates response. All other request + // parameters except page_size must match the request that produced it. + string page_token = 2; + // Optional label selector in key=value comma-separated form. + string label_selector = 3; +} + +message DeleteSandboxTemplateRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string name = 1; + // Succeed with ALREADY_ABSENT if the target is missing. Authorization and + // parent-workspace checks still apply. + bool allow_missing = 3; + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + string request_id = 4; +} + +message SandboxTemplateResponse { + SandboxWorkloadTemplate template = 1; +} + +message ListSandboxTemplatesResponse { + repeated SandboxWorkloadTemplate templates = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +message DeleteSandboxTemplateResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// Request a gateway-owned staging slot for a local rootfs tar archive. +message BeginRootfsTarStagingRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Base file name of the local archive. The gateway uses it only to name the + // staged file; path separators and traversal components are rejected. + string file_name = 1; + // Size of the local archive in bytes, checked against the driver limit + // before the gateway allocates a slot. + uint64 size_bytes = 2; +} + +// Gateway-issued staging slot. +message BeginRootfsTarStagingResponse { + reserved 4; + reserved "expires_at_ms"; + // Opaque single-use token. Pass it as + // `template.driver_config..rootfs_tar_staging_token` on + // CreateSandbox. The first CreateSandbox presenting it consumes it. + string staging_token = 1; + // Absolute path on the gateway host the client must write the archive to. + string upload_path = 2; + // Maximum accepted archive size in bytes, enforced again by the driver. + uint64 max_bytes = 3; + // Wall-clock deadline after which the gateway reclaims the slot. + google.protobuf.Timestamp expiration_time = 104; +} + +// Get sandbox request. +message GetSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string name = 1; +} + +// List sandboxes request. +message ListSandboxesRequest { + // Workspace scope. Named and all-workspaces selections are accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // The maximum number of sandboxes to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 1; + // Token from a previous ListSandboxes response. All other request parameters + // except page_size must match the request that produced it. + string page_token = 2; + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + string label_selector = 3; +} + +// List providers attached to a sandbox request. +message ListSandboxProvidersRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string sandbox = 1; + // The maximum number of providers to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 3; + // Token from a previous ListSandboxProviders response. All other request + // parameters except page_size must match the request that produced it. + string page_token = 4; +} + +// Attach provider to sandbox request. +message AttachSandboxProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + string sandbox = 1; + // Provider name to attach. + string provider = 2; + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + uint64 expected_resource_version = 3; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +// Detach provider from sandbox request. +message DetachSandboxProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + string sandbox = 1; + // Provider name to detach. + string provider = 2; + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + uint64 expected_resource_version = 3; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +// Delete sandbox request. +message DeleteSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + // Canonical sandbox name. + string name = 1; + // Succeed with ALREADY_ABSENT if the target is missing. Does not wait for + // asynchronous cleanup and does not suppress authorization or parent errors. + bool allow_missing = 3; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 4; +} + +// Stop sandbox request. +message StopSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string name = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +// Start sandbox request. +message StartSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string name = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +// Sandbox response. +message SandboxResponse { + Sandbox sandbox = 1; + // Service URLs created by CreateSandbox, keyed by service name. The empty + // key identifies the unnamed service. Other sandbox RPCs return an empty map. + map service_urls = 2; +} + +// List sandboxes response. +message ListSandboxesResponse { + repeated Sandbox sandboxes = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// List providers attached to a sandbox response. +message ListSandboxProvidersResponse { + repeated openshell.datamodel.v1.Provider providers = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Attach provider to sandbox response. +message AttachSandboxProviderResponse { + Sandbox sandbox = 1; + // True when the provider was newly attached. False means it was already attached. + bool attached = 2; + // Persisted intent; readiness requires current supervisor observations. + ProviderMutationReceipt receipt = 3; +} + +// Detach provider from sandbox response. +message DetachSandboxProviderResponse { + Sandbox sandbox = 1; + // True when the provider was removed. False means it was not attached. + bool detached = 2; + // Revocation is complete only when this receipt reports REVOKED. + ProviderMutationReceipt receipt = 3; +} + +// Operation whose installed authority is tracked by a receipt. +enum ProviderMutationKind { + PROVIDER_MUTATION_KIND_UNSPECIFIED = 0; + PROVIDER_MUTATION_KIND_ATTACH = 1; + PROVIDER_MUTATION_KIND_DETACH = 2; + PROVIDER_MUTATION_KIND_UPDATE = 3; + // Reconstructed status for existing desired state without a mutation receipt. + PROVIDER_MUTATION_KIND_OBSERVE = 4; +} + +// Readiness states describe persisted intent separately from installed state. +enum ProviderReadinessState { + PROVIDER_READINESS_STATE_UNSPECIFIED = 0; + PROVIDER_READINESS_STATE_PERSISTED = 1; + PROVIDER_READINESS_STATE_PENDING = 2; + PROVIDER_READINESS_STATE_READY = 3; + PROVIDER_READINESS_STATE_WITHHELD = 4; + PROVIDER_READINESS_STATE_REVOKED = 5; + PROVIDER_READINESS_STATE_FAILED = 6; + PROVIDER_READINESS_STATE_SUPERSEDED = 7; +} + +// Closed reason categories are safe to display. Raw installation errors are +// never part of the readiness protocol. +enum ProviderReadinessReason { + PROVIDER_READINESS_REASON_UNSPECIFIED = 0; + PROVIDER_READINESS_REASON_WAITING_FOR_SUPERVISOR = 1; + PROVIDER_READINESS_REASON_WAITING_FOR_CREDENTIALS = 2; + PROVIDER_READINESS_REASON_WAITING_FOR_POLICY = 3; + PROVIDER_READINESS_REASON_WAITING_FOR_PROCESS = 4; + PROVIDER_READINESS_REASON_UNSUPPORTED_SUPERVISOR = 5; + PROVIDER_READINESS_REASON_CREDENTIALS_WITHHELD = 6; + PROVIDER_READINESS_REASON_CREDENTIAL_INSTALL_FAILED = 7; + PROVIDER_READINESS_REASON_POLICY_ACTIVATION_FAILED = 8; + PROVIDER_READINESS_REASON_PROCESS_INSTALL_FAILED = 9; + PROVIDER_READINESS_REASON_SUPERVISOR_DISCONNECTED = 10; + PROVIDER_READINESS_REASON_SUPERVISOR_LEASE_EXPIRED = 11; + PROVIDER_READINESS_REASON_DESIRED_STATE_CHANGED = 12; + PROVIDER_READINESS_REASON_CREDENTIAL_EXPIRED = 13; + PROVIDER_READINESS_REASON_LOCAL_POLICY = 14; + PROVIDER_READINESS_REASON_SNAPSHOT_MISMATCH = 15; +} + +// Exact desired authority. Revisions are opaque identities, never ordered. +message ProviderDesiredIdentity { + string sandbox_id = 1; + string sandbox = 2; + string attachment_epoch = 3; + // Empty for a detached provider. + string provider_id = 4; + uint64 provider_resource_version = 5; + uint64 provider_env_revision = 6; + uint64 config_revision = 7; + string policy_hash = 8; +} + +// Component whose desired state is tracked by a durable update operation. +enum ConfigComponent { + CONFIG_COMPONENT_UNSPECIFIED = 0; + CONFIG_COMPONENT_SANDBOX_CONFIG = 1; + CONFIG_COMPONENT_PROVIDER_ENVIRONMENT = 2; +} + +// Identifies one component snapshot revision. Revisions are equality tokens, +// not members of one shared ordering domain. +message ConfigSnapshotRevision { + oneof component { + SandboxConfigRevision sandbox_config = 1; + uint64 provider_environment = 2; + // Complete desired authority for one sandbox-scoped provider mutation. + ProviderDesiredIdentity provider_target = 3; + } +} + +// Identity needed to correlate effective sandbox configuration with the +// policy-history row whose apply status the gateway records. +message SandboxConfigRevision { + uint64 config_revision = 1; + uint32 policy_version = 2; + openshell.sandbox.v1.PolicySource policy_source = 3; + uint32 global_policy_version = 4; + // Monotonic revision of the sandbox-scoped settings row. This disambiguates + // setting operations whose effective config fingerprint is equality-only. + uint64 settings_revision = 5; +} + +// Result of applying a component revision at its owning runtime boundary. +enum ConfigApplyOutcome { + CONFIG_APPLY_OUTCOME_UNSPECIFIED = 0; + CONFIG_APPLY_OUTCOME_APPLIED = 1; + CONFIG_APPLY_OUTCOME_IGNORED_DUPLICATE = 2; + CONFIG_APPLY_OUTCOME_IGNORED_STALE = 3; + CONFIG_APPLY_OUTCOME_RETAINED_LOCAL_OVERRIDE = 4; + CONFIG_APPLY_OUTCOME_DEGRADED = 5; + CONFIG_APPLY_OUTCOME_FAILED_RETAINED_LAST_KNOWN_GOOD = 6; + CONFIG_APPLY_OUTCOME_FAILED_CLOSED = 7; + CONFIG_APPLY_OUTCOME_UNSUPPORTED = 8; +} + +// Durable lifecycle of one desired-state update operation. +enum ConfigUpdateOperationState { + CONFIG_UPDATE_OPERATION_STATE_UNSPECIFIED = 0; + CONFIG_UPDATE_OPERATION_STATE_PENDING = 1; + CONFIG_UPDATE_OPERATION_STATE_APPLIED = 2; + CONFIG_UPDATE_OPERATION_STATE_INACTIVE = 3; + CONFIG_UPDATE_OPERATION_STATE_FAILED = 4; + CONFIG_UPDATE_OPERATION_STATE_SUPERSEDED = 5; + CONFIG_UPDATE_OPERATION_STATE_CANCELLED = 6; +} + +// Durable progress for one sandbox-scoped desired-state mutation. Snapshot +// contents and credentials are never stored in this resource. +message ConfigUpdateOperation { + string operation_id = 1; + string sandbox_id = 2; + ConfigComponent component = 3; + ConfigSnapshotRevision target_revision = 4; + ConfigUpdateOperationState state = 5; + ConfigApplyOutcome outcome = 6; + string sanitized_error = 7; + reserved 8, 9, 10; + reserved "created_at_ms", "updated_at_ms", "completed_at_ms"; + google.protobuf.Timestamp created_time = 108; + google.protobuf.Timestamp updated_time = 109; + // Absent until the operation reaches a terminal state. + google.protobuf.Timestamp completed_time = 110; +} + +// Immutable, secret-free record of one sandbox's intended provider mutation. +message ProviderMutationReceipt { + string receipt_id = 1; + // Shared by all sandbox receipts from one provider update. + string mutation_id = 2; + string provider = 3; + string workspace = 4; + ProviderMutationKind kind = 5; + ProviderDesiredIdentity desired = 6; + reserved 7; + reserved "persisted_at_ms"; + google.protobuf.Timestamp persisted_time = 107; +} + +// Installed state reported by the current supervisor. Process installation is +// acknowledged by the authenticated sandbox boundary after replacing its +// environment for future process launches. +message ProviderReadinessObservation { + // Gateway-issued identifier from the current ConnectSupervisor response. + string session_id = 1; + // Monotonic only within this connection; unrelated to revision fingerprints. + uint64 sequence = 2; + string attachment_epoch = 3; + uint64 provider_env_revision = 4; + uint64 config_revision = 5; + string policy_hash = 6; + bool credentials_installed = 7; + bool policy_active = 8; + bool launch_environment_installed = 9; + string process_instance_id = 10; + ProviderReadinessReason reason = 11; +} + +// Operator view of desired and observed state; contains no credential material. +message ProviderReadinessStatus { + ProviderMutationReceipt receipt = 1; + ProviderReadinessState state = 2; + ProviderReadinessReason reason = 3; + ProviderReadinessObservation observed = 4; + string network_instance_id = 5; + reserved 6, 7; + reserved "observed_at_ms", "evaluated_at_ms"; + // Absent until the current supervisor session supplies accepted evidence. + google.protobuf.Timestamp observed_time = 106; + google.protobuf.Timestamp evaluated_time = 107; + // Durable operation for the receipt, including its terminal apply outcome. + ConfigUpdateOperation operation = 8; +} + +// Query an immutable receipt, or reconstruct the current desired state when +// receipt_id is empty. The sandbox identity must match the receipt. +message GetSandboxProviderStatusRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + string sandbox = 1; + string provider = 2; + string receipt_id = 3; +} + +message GetSandboxProviderStatusResponse { + ProviderReadinessStatus status = 1; +} + +// Installation evidence from the authenticated supervisor for this sandbox. +// A caller-provided instance identifier alone never establishes authority. +message ReportProviderReadinessRequest { + string sandbox_id = 1; + ProviderReadinessObservation observation = 2; +} + +// Acknowledges accepted evidence without granting a separate session authority. +// Identical retries do not extend the evidence's original acceptance time. +message ReportProviderReadinessResponse { + uint64 accepted_sequence = 1; + reserved 2, 3; + reserved "report_interval_seconds", "observation_ttl_seconds"; + google.protobuf.Duration report_interval = 102; + google.protobuf.Duration observation_ttl = 103; +} + +// Delete sandbox response. +message DeleteSandboxResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; + // Immutable identity of the targeted sandbox, empty for ALREADY_ABSENT. + // A same-name replacement is not part of this deletion. + string sandbox_id = 3; +} + +// Create SSH session request. +message CreateSshSessionRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + string sandbox = 1; +} + +// Create SSH session response. +// +// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH +// executes through `/bin/sh -c` on the caller's workstation. Servers MUST +// uphold the charset contract below; clients MUST reject responses that +// violate it. The client's own escaping provides defense-in-depth, but +// narrow charsets close injection vectors at the trust boundary. +message CreateSshSessionResponse { + reserved 8; + reserved "expires_at_ms"; + // Sandbox id. [A-Za-z0-9._-]{1,128}. + string sandbox_id = 1; + + // Session token for the gateway tunnel. URL-safe ASCII + // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or + // whitespace. + string token = 2 [(openshell.options.v1.secret) = true]; + + // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 + // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus + // `.-:[]` only, up to 253 bytes. + string gateway_host = 3; + + // Gateway port for SSH proxy connection. Must be in range 1..=65535. + uint32 gateway_port = 4; + + // Gateway scheme. Must be exactly "http" or "https". + string gateway_scheme = 5; + + // Expected sandbox SSH host identity in OpenSSH SHA256: form. + // Stable for the sandbox's lifetime. Empty only for legacy runtimes. + string host_key_fingerprint = 7; + + // Absolute expiry. Absence means no expiry. + google.protobuf.Timestamp expiration_time = 108; +} + +// Request to expose an HTTP service running inside a sandbox. +message ExposeServiceRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; + // Service name within the sandbox. + string name = 2; + // Loopback TCP port inside the sandbox. + uint32 target_port = 3; + // Whether to print/use the browser-facing service URL. + bool domain = 4; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 6; + // Application authorization behavior. Omission resolves to STRIP. + ServiceAuthorizationMode authorization_mode = 7; +} + +// Request to fetch an exposed sandbox service endpoint. +message GetServiceRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Service name within the sandbox. Empty selects the unnamed endpoint. + string name = 2; + string sandbox = 1; +} + +// Request to list exposed sandbox service endpoints. +message ListServicesRequest { + // Workspace scope. Named and all-workspaces selections are accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // The maximum number of services to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 2; + // Token from a previous ListServices response. All other request parameters + // except page_size must match the request that produced it. + string page_token = 3; + // Optional sandbox name. Empty lists endpoints for all sandboxes. + string sandbox = 1; +} + +// Response containing exposed sandbox service endpoints. +message ListServicesResponse { + repeated ServiceEndpointResponse services = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Request to delete an exposed sandbox service endpoint. +message DeleteServiceRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Service name within the sandbox. Empty selects the unnamed endpoint. + string name = 2; + string sandbox = 1; + bool allow_missing = 4; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +// Response for deleting an exposed sandbox service endpoint. +message DeleteServiceResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// Persisted sandbox service endpoint. +message ServiceEndpoint { + // Kubernetes-style metadata. + openshell.datamodel.v1.ObjectMeta metadata = 1; + // Sandbox object ID. + string sandbox_id = 2; + // Sandbox name. + string sandbox = 3; + // Service name within the sandbox. + string name = 4; + // Loopback TCP port inside the sandbox. + uint32 target_port = 5; + // Whether browser-facing service routing is enabled for this endpoint. + bool domain = 6; + // Effective application authorization behavior for ingress requests. + ServiceAuthorizationMode authorization_mode = 7; +} + +// Response containing a service endpoint and, when available, its local URL. +message ServiceEndpointResponse { + ServiceEndpoint endpoint = 1; + string url = 2; +} + +// Revoke SSH session request. +message RevokeSshSessionRequest { + // Session token to revoke. + string token = 1 [(openshell.options.v1.secret) = true]; + // A missing token is NOT_FOUND unless this is true. Revoking an existing, + // already-revoked session succeeds with COMPLETED. + bool allow_missing = 2; +} + +// Revoke SSH session response. +message RevokeSshSessionResponse { + reserved 1; + reserved "revoked"; + DeletionOutcome outcome = 2; +} + +// Execute command request. +message ExecSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 12; + reserved 5; + reserved "timeout_seconds"; + // Canonical sandbox name. + string sandbox = 1; + // Command and arguments. + repeated string command = 2; + + // Optional working directory. + string workdir = 3; + + // Optional environment overrides. + map environment = 4; + + // Optional execution timeout. Absence means no timeout. + google.protobuf.Duration execution_timeout = 105; + + // Optional stdin payload passed to the command. + bytes stdin = 6; + + // Request a pseudo-terminal for the remote command. + bool tty = 7; + + // Initial terminal columns (used when tty=true, 0 = use default). + uint32 cols = 8; + + // Initial terminal rows (used when tty=true, 0 = use default). + uint32 rows = 9; + + // Skip sourcing shell login/profile startup files before running the command. + // When false (the default), the command runs through a login shell + // (`bash -lc`) so user startup files (.bash_profile/.profile, and .bashrc if + // sourced by them) are applied. When true, the command runs without those + // files (`bash -c`), for automation that needs predictable startup behavior. + bool no_login_shell = 10; + + // Optional nonzero UUID for durable launch admission. Also applies to the + // initial ExecSandboxInteractive start message. Duplicates never relaunch, + // reattach, or replay output/stdin. Unconfirmed executions remain fenced; + // confirmed terminal executions retain the fence for 24 hours. + string request_id = 11; +} + +// One stdout chunk from a sandbox exec. +message ExecSandboxStdout { + bytes data = 1; +} + +// One stderr chunk from a sandbox exec. +message ExecSandboxStderr { + bytes data = 1; +} + +// Final exit status for a sandbox exec. +message ExecSandboxExit { + int32 exit_code = 1; +} + +// One event in a sandbox exec stream. +message ExecSandboxEvent { + oneof payload { + ExecSandboxStdout stdout = 1; + ExecSandboxStderr stderr = 2; + ExecSandboxExit exit = 3; + } +} + +// Initial frame for one TCP forward stream. +message TcpForwardInit { + string sandbox = 1; + string workspace = 3; + // Optional service identifier for audit/correlation. + string service_id = 4; + // Target the gateway should request from the supervisor. + oneof target { + SshRelayTarget ssh = 5; + TcpRelayTarget tcp = 6; + } + // Optional target-specific authorization token. SSH targets use this as the + // short-lived SSH session token issued by CreateSshSession. + string authorization_token = 7 [(openshell.options.v1.secret) = true]; +} + +// A single frame on the CLI-to-gateway TCP forward stream. +message TcpForwardFrame { + oneof payload { + TcpForwardInit init = 1; + bytes data = 2; + } +} + +// Client-to-server message for interactive exec. +message ExecSandboxInput { + oneof payload { + // First message: exec request metadata. + ExecSandboxRequest start = 1; + // Subsequent messages: raw stdin bytes. + bytes stdin = 2; + // Terminal window size change. + ExecSandboxWindowResize resize = 3; + } +} + +// Terminal window resize event for interactive exec. +message ExecSandboxWindowResize { + uint32 cols = 1; + uint32 rows = 2; +} + + +// SSH session record stored in persistence. +message SshSession { + reserved 4; + reserved "expires_at_ms"; + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + openshell.datamodel.v1.ObjectMeta metadata = 1; + + // Sandbox id. + string sandbox_id = 2; + + // Session token. + string token = 3 [(openshell.options.v1.secret) = true]; + + // Absolute expiry. Absence means no expiry. + google.protobuf.Timestamp expiration_time = 104; + + // Revoked flag. + bool revoked = 5; +} + +// Watch sandbox request. +message WatchSandboxRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 11; + reserved 8; + reserved "log_since_ms"; + // Canonical sandbox name. + string sandbox = 1; + + // Stream sandbox status snapshots. + bool follow_status = 2; + + // Stream openshell-server process logs correlated to this sandbox. + bool follow_logs = 3; + + // Stream platform events correlated to this sandbox. + bool follow_events = 4; + + // Replay the last N log lines (best-effort) before following. + uint32 log_tail_lines = 5; + + // Replay the last N platform events (best-effort) before following. + uint32 event_tail = 6; + + // Stop streaming once the sandbox reaches READY or a terminal result phase + // (COMPLETED, STOPPED, or ERROR). + bool stop_on_terminal = 7; + + // Only include log lines at or after this time. Absence means no time filter. + // Applies to both tail replay and live streaming. + google.protobuf.Timestamp since_time = 108; + + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + repeated string log_sources = 9; + + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + string log_min_level = 10; + + // Resume streaming after this cursor. Empty means no cursor resume: the + // server falls back to tail-limited replay controlled by log_tail_lines and + // event_tail. Otherwise set it to the highest `SandboxStreamEvent.cursor` + // already processed; the server replays only log and platform events after + // it, merged in cursor order, before resuming live delivery. If the requested + // cursor has already been trimmed from the server's buffer, the resume is + // unrecoverable and the stream terminates with OUT_OF_RANGE (see + // SandboxStreamWarning for the recoverable case). + // + // A cursor is bound to the cursor space that issued it. A gateway restart, + // teardown of the sandbox's buffers, or a reconnect to a different gateway + // replica starts a new space, and cursors from the previous one are rejected + // with OUT_OF_RANGE rather than silently suppressing live events beneath + // them. OUT_OF_RANGE is terminal for that cursor: restart the watch with an + // empty resume_after_cursor, because retrying the same token fails + // identically. A cursor this server could not have issued is rejected with + // INVALID_ARGUMENT. + string resume_after_cursor = 12; +} + +// One event in a sandbox watch stream. +message SandboxStreamEvent { + oneof payload { + // Latest sandbox snapshot. + Sandbox sandbox = 1; + // One server log line/event. + SandboxLogLine log = 2; + // One platform event. + PlatformEvent event = 3; + // Recoverable warning from the server, e.g. messages dropped because a + // broadcast receiver lagged. The stream continues after this warning; the + // client can detect the gap from the warning itself. + SandboxStreamWarning warning = 4; + // Draft policy update notification. + DraftPolicyUpdate draft_policy_update = 5; + } + // Opaque position in this sandbox's cursor space, shared across the resumable + // log and platform event sources. Empty for non-resumable events (status + // snapshots, warnings). + // + // Do not parse this token; its encoding is not part of the contract. The only + // supported operation is comparing two non-empty cursors observed on the same + // stream and keeping the greater one, then passing it as + // WatchSandboxRequest.resume_after_cursor to resume without loss or + // duplication. That comparison is a plain byte-wise string comparison. It is + // well defined only within one stream: a stream never spans two cursor + // spaces, because a reset ends it. + string cursor = 6; +} + +// Log line correlated to a sandbox. +message SandboxLogLine { + reserved 2; + reserved "timestamp_ms"; + string sandbox_id = 1; + google.protobuf.Timestamp event_time = 102; + string level = 3; + string target = 4; + string message = 5; + // Log source: "gateway" (server-side) or "sandbox" (supervisor). + // Empty is treated as "gateway" for backward compatibility. + string source = 6; + // Structured key-value fields from the tracing event (e.g. dst_host, action). + map fields = 7; +} + +// Recoverable loss notification on a watch stream. Emitted when the server +// skips ahead after a broadcast lag instead of terminating; the stream keeps +// running. Cursors are opaque, so this message is the only signal that events +// were skipped. Unrecoverable loss (a trimmed or foreign resume cursor) is +// reported as an OUT_OF_RANGE stream status, not this message. +message SandboxStreamWarning { + string message = 1; +} + +// Create provider request. +message CreateProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + openshell.datamodel.v1.Provider provider = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +// Get provider request. +message GetProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + 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. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // The maximum number of providers to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 1; + // Token from a previous ListProviders response. All other request parameters + // except page_size must match the request that produced it. + string page_token = 2; + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + string label_selector = 4; +} + +// Update provider request. +message UpdateProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + reserved 2; + reserved "credential_expires_at_ms"; + openshell.datamodel.v1.Provider provider = 1; + // Optional per-credential expiry timestamps to merge into the provider. + // Omitted keys are unchanged. Use clear_credential_expiration_keys to remove + // an existing expiry. + map credential_expiration_times = 102; + // Credential keys whose existing expiry should be removed. + repeated string clear_credential_expiration_keys = 103; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 4; +} + +// Delete provider request. +message DeleteProviderRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + string name = 1; + bool allow_missing = 4; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +// Provider response. +message ProviderResponse { + openshell.datamodel.v1.Provider provider = 1; + // Selection-time sandbox target set for an update, with one receipt per target. + // Sandboxes attached later are outside this operation's readiness result. + repeated ProviderMutationReceipt target_receipts = 2; + // Identifies the update even when its target set is empty. + string mutation_id = 3; +} + +// List providers response. +message ListProvidersResponse { + repeated openshell.datamodel.v1.Provider providers = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// List provider type profiles request. +message ListProviderProfilesRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // The maximum number of profiles to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 1; + // Token from a previous ListProviderProfiles response. All other request + // parameters except page_size must match the request that produced it. + string page_token = 2; +} + +// Fetch provider type profile request. +message GetProviderProfileRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string id = 1; +} + +// Provider profile payload with optional source metadata for diagnostics. +message ProviderProfileImportItem { + ProviderProfile profile = 1; + string source = 2; +} + +// Provider profile validation diagnostic. +message ProviderProfileDiagnostic { + string source = 1; + string profile_id = 2; + string field = 3; + string message = 4; + string severity = 5; +} + +// Endpoint selector for token grant audience overrides. +message ProviderCredentialTokenGrantAudienceOverride { + // Optional: endpoint host selector. If omitted, inherits the profile endpoint host. + string host = 1; + + // Optional: endpoint port selector. If omitted, matches the expanded profile endpoint port. + uint32 port = 2; + + // Optional: endpoint path selector. If omitted, inherits the profile endpoint path. + string path = 3; + + // Resource audience to request for matching endpoints. + string audience = 4; + + // Optional: OAuth2 scopes to request. If omitted, inherits the token grant scopes. + repeated string scopes = 5; +} + +// Provider credential token grant configuration. +// When present, the credential is obtained dynamically via OAuth2 grant when needed. +enum ProviderCredentialTokenGrantType { + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS = 1; + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE = 2; +} + +message ProviderCredentialTokenGrantSubjectToken { + // Source for the token exchange subject token. Phase one supports + // "provider_credential". + string source = 1; + + // Provider credential key that stores the subject token. + string credential = 2; + + // OAuth2 subject_token_type. If omitted, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + string subject_token_type = 3; +} + +message ProviderCredentialTokenGrant { + reserved 4; + reserved "cache_ttl_seconds"; + // OAuth2 token endpoint URL (e.g., https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token) + string token_endpoint = 1; + + // Optional: default resource audience to request from the token service + string audience = 2; + + // Optional: audience to request when fetching the JWT-SVID from SPIRE. + // If omitted, the sandbox derives this from token_endpoint. + string jwt_svid_audience = 6; + + // Optional: OAuth2 scopes to request + repeated string scopes = 3; + + // Optional token cache TTL override. If absent, use expires_in from the token response. + google.protobuf.Duration cache_ttl = 104; + + // Optional: endpoint-specific resource audience overrides. + repeated ProviderCredentialTokenGrantAudienceOverride audience_overrides = 5; + + // Optional: OAuth2 client_assertion_type value. If omitted, OpenShell uses + // urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + string client_assertion_type = 7; + + // Grant type. If omitted/unspecified, OpenShell treats this as client_credentials + // for backwards compatibility. + ProviderCredentialTokenGrantType grant_type = 8; + + // Subject token metadata for token_exchange grants. + ProviderCredentialTokenGrantSubjectToken subject_token = 9; + + // OAuth2 requested_token_type. If omitted for token_exchange, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + string requested_token_type = 10; +} + +// Provider credential declaration. +message ProviderProfileCredential { + string name = 1; + string description = 2; + repeated string env_vars = 3; + bool required = 4; + string auth_style = 5; + string header_name = 6; + string query_param = 7; + ProviderCredentialRefresh refresh = 8; + string path_template = 9; + ProviderCredentialTokenGrant token_grant = 10; + // Output-only gateway-derived endpoint authorities for resolved token grants. + // For inspected L7 requests, empty means no endpoint may obtain this grant. + // L4 injection uses selectors without checking these authorities. + // Profile-authored values are ignored; normal policies name the originating + // provider endpoint, while a global policy uses its own endpoint authorities. + repeated string token_grant_owners = 11; + // Opt in to an opaque identity-bound environment placeholder for every env + // var declared by this credential. Use this only when an external credential + // updater requires one placeholder across value-only provider revisions. + // Gateway-managed refresh and token-grant credentials cannot opt in because + // they require identity-bearing handles for authorization-epoch revocation. + bool stable_placeholder = 12; +} + +enum ProviderCredentialRefreshStrategy { + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC = 1; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_EXTERNAL = 2; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_REFRESH_TOKEN = 3; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_OAUTH2_CLIENT_CREDENTIALS = 4; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_GOOGLE_SERVICE_ACCOUNT_JWT = 5; + PROVIDER_CREDENTIAL_REFRESH_STRATEGY_AWS_STS_ASSUME_ROLE = 6; +} + +message ProviderCredentialRefreshMaterial { + string name = 1; + string description = 2; + bool required = 3; + bool secret = 4; +} + +// Declares that a single refresh operation mints more than one credential. +// The refresh is attached to a primary credential; each additional output +// maps a strategy-defined semantic output id to a sibling credential whose +// env_vars receive the minted value. +message ProviderCredentialRefreshOutput { + string output = 1; // strategy-defined semantic output id (e.g. "session_token") + string credential = 2; // sibling credential name whose env_vars receive this output +} + +message ProviderCredentialRefresh { + reserved 4, 5; + reserved "refresh_before_seconds", "max_lifetime_seconds"; + ProviderCredentialRefreshStrategy strategy = 1; + string token_url = 2; + repeated string scopes = 3; + google.protobuf.Duration refresh_before = 104; + google.protobuf.Duration max_lifetime = 105; + repeated ProviderCredentialRefreshMaterial material = 6; + repeated ProviderCredentialRefreshOutput additional_outputs = 7; +} + +message ProviderCredentialRefreshStatus { + reserved 6, 7, 8, 13; + reserved "expires_at_ms", "next_refresh_at_ms", "last_refresh_at_ms", "last_error_at_ms"; + string provider = 1; + string provider_id = 2; + string credential_key = 3; + ProviderCredentialRefreshStrategy strategy = 4; + string status = 5; + google.protobuf.Timestamp expiration_time = 106; + // Next automatic refresh time. Absence means no automatic retry is scheduled; + // use recovery_action to determine the required recovery workflow. + google.protobuf.Timestamp next_refresh_time = 107; + google.protobuf.Timestamp last_refresh_time = 108; + string last_error = 9; + ProviderCredentialRefreshRecoveryAction recovery_action = 10; + // Stable gateway-owned failure identifier, for example + // "oauth_invalid_grant". This is not provider-controlled prose and + // incorporates any recognized top-level OAuth error classification. + string failure_code = 11; + // A bounded, recognized provider subtype that refines failure_code; clients + // do not need a separate provider_error field. Unknown provider-controlled + // values are not persisted or returned. + string provider_error_subtype = 12; + google.protobuf.Timestamp last_error_time = 113; +} + +// Provider profile local discovery declaration. +message ProviderProfileDiscovery { + // Credential names from ProviderProfile.credentials eligible for local discovery. + repeated string credentials = 1; +} + +message GetProviderRefreshStatusRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + string provider = 1; + string credential_key = 2; +} + +message GetProviderRefreshStatusResponse { + repeated ProviderCredentialRefreshStatus credentials = 1; +} + +message ConfigureProviderRefreshRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 7; + reserved 6; + reserved "expires_at_ms"; + string provider = 1; + string credential_key = 2; + ProviderCredentialRefreshStrategy strategy = 3; + map material = 4 [(openshell.options.v1.secret) = true]; + // Additional material names the caller requests be stored as secrets. Every + // name must be present in material. The server also classifies secrets from + // the authoritative provider profile and refresh strategy. + repeated string secret_material_keys = 5; + google.protobuf.Timestamp expiration_time = 106; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 8; +} + +message ConfigureProviderRefreshResponse { + ProviderCredentialRefreshStatus status = 1; +} + +message RotateProviderCredentialRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + string provider = 1; + string credential_key = 2; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 4; +} + +message RotateProviderCredentialResponse { + ProviderCredentialRefreshStatus status = 1; +} + +message DeleteProviderRefreshRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + string provider = 1; + string credential_key = 2; + bool allow_missing = 5; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 6; +} + +message DeleteProviderRefreshResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// Stable provider profile categories used by clients for grouping and filtering. +enum ProviderProfileCategory { + PROVIDER_PROFILE_CATEGORY_UNSPECIFIED = 0; + PROVIDER_PROFILE_CATEGORY_OTHER = 1; + PROVIDER_PROFILE_CATEGORY_INFERENCE = 2; + PROVIDER_PROFILE_CATEGORY_AGENT = 3; + PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL = 4; + PROVIDER_PROFILE_CATEGORY_MESSAGING = 5; + PROVIDER_PROFILE_CATEGORY_DATA = 6; + PROVIDER_PROFILE_CATEGORY_KNOWLEDGE = 7; +} + +// Provider type profile metadata exposed to clients. +message ProviderProfile { + string id = 1; + string display_name = 2; + string description = 3; + ProviderProfileCategory category = 4; + repeated ProviderProfileCredential credentials = 5; + repeated openshell.sandbox.v1.NetworkEndpoint endpoints = 6; + repeated openshell.sandbox.v1.NetworkBinary binaries = 7; + bool inference_capable = 8; + ProviderProfileDiscovery discovery = 9; + // Storage resource version for custom profiles. Built-in profiles and new + // profile files use 0. Gateway responses set this for stored custom profiles. + // Update calls use this for optimistic concurrency. + uint64 resource_version = 10; + // Optional non-secret annotations attached by profile sources or importers. + map annotations = 11; + // Server-set provenance: "builtin", "user", or "interceptor/{name}". + // Ignored on import/update payloads. + string source = 12; + // Server-set visibility: "platform", "workspace", or empty for + // non-scoped sources. Ignored on import/update payloads. + string scope = 13; + // EXPERIMENTAL: Non-secret files rendered from provider configuration for the + // workload. This API and its behavior may change or be removed. + repeated ProviderProfileFile files = 14; +} + +// EXPERIMENTAL: Provider file templates may change or be removed. +message ProviderProfileFile { + // One virtual file name below /run/openshell/providers//. + string path = 1; + // UTF-8 template. Only {{config.KEY}} references are supported. + string content = 2; + // Optional environment variable containing the virtual absolute path. + string env_var = 3; +} + +// Provider profile response. +message ProviderProfileResponse { + ProviderProfile profile = 1; +} + +// List provider profiles response. +message ListProviderProfilesResponse { + repeated ProviderProfile profiles = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Import custom provider profiles request. +message ImportProviderProfilesRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + repeated ProviderProfileImportItem profiles = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +// Import custom provider profiles response. +message ImportProviderProfilesResponse { + repeated ProviderProfileDiagnostic diagnostics = 1; + repeated ProviderProfile profiles = 2; + bool imported = 3; +} + +// Update one custom provider profile request. +message UpdateProviderProfilesRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + ProviderProfileImportItem profile = 1; + // Expected storage resource version for optimistic concurrency control. + // If 0, the server uses the resource_version embedded in profile.profile. + // Updates without a non-zero version are rejected to prevent stale files from + // silently overwriting newer profile definitions. + uint64 expected_resource_version = 2; + // Existing custom provider profile ID to update. The payload ID must match. + string id = 3; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +// Update one custom provider profile response. +message UpdateProviderProfilesResponse { + repeated ProviderProfileDiagnostic diagnostics = 1; + ProviderProfile profile = 2; + bool updated = 3; +} + +// Lint provider profiles request. +message LintProviderProfilesRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + repeated ProviderProfileImportItem profiles = 1; +} + +// Lint provider profiles response. +message LintProviderProfilesResponse { + repeated ProviderProfileDiagnostic diagnostics = 1; + bool valid = 2; + // Clients enabling stable_placeholder check this before import or update, + // since older gateways silently discard unknown credential fields. + bool supports_stable_placeholder = 3; +} + +// Delete provider response. +message DeleteProviderResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// Delete custom provider profile request. +message DeleteProviderProfileRequest { + // Workspace scope. Omit for platform profiles; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string id = 1; + bool allow_missing = 3; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 4; +} + +// Delete custom provider profile response. +message DeleteProviderProfileResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// Get sandbox provider environment request. +message GetSandboxProviderEnvironmentRequest { + // The sandbox ID. + string sandbox_id = 1; + // Whether the requesting supervisor enforces endpoint bindings for static + // provider credentials. Gateways withhold static credential material when + // this capability is absent. + bool supports_static_credential_bindings = 2; + // Whether the requesting supervisor understands stable external-placeholder + // metadata. Gateways withhold credentials that require this behavior when + // the capability is absent. + bool supports_stable_placeholder_environment_keys = 3; +} + +// One network endpoint at which a static provider credential may be resolved. +message StaticCredentialEndpointBinding { + string host = 1; + uint32 port = 2; + string path = 3; +} + +// Endpoint allowlist for one static provider credential environment variable. +message StaticCredentialBinding { + repeated StaticCredentialEndpointBinding endpoints = 1; + // Stable identity of the provider credential that produced this binding. + // Supervisors use it to retain old revision placeholders only across + // rotations of the same provider credential. + string credential_identity = 2; + // Opaque gateway-issued handle for a stable credential lifecycle. When + // non-empty, supervisors keep the workload placeholder stable across value + // updates and replace only the resolver value. The handle changes when the + // sandbox, provider, credential key, endpoint authorization, or an applicable + // refresh authorization epoch changes. + string workload_credential_handle = 3; +} + +// Get sandbox provider environment response. +message GetSandboxProviderEnvironmentResponse { + reserved 3; + reserved "credential_expires_at_ms"; + // Provider credential environment variables. + map environment = 1 [(openshell.options.v1.secret) = true]; + // Fingerprint for the provider credential inputs that produced environment. + uint64 provider_env_revision = 2; + // Expiration timestamps for returned environment variables. + map credential_expiration_times = 103; + // Dynamic credentials that require token grants or other runtime injection. + // Maps endpoint-bound provider metadata to credential metadata. + // Supervisor uses this to inject Authorization headers for token grant credentials. + map dynamic_credentials = 4; + // Endpoint allowlists for static credential environment variables. Metadata + // can be incomplete when a provider profile is invalid; capable supervisors + // reject all static material in that snapshot while preserving independently + // endpoint-bound dynamic credentials. + map static_credential_bindings = 5; + // Environment variables that contain provider configuration rather than + // credentials and therefore do not require endpoint-scoped resolution. + repeated string non_secret_environment_keys = 6; + // Attachment identity captured with the returned provider records. + string provider_attachment_epoch = 7; + // Effective policy identity used to derive this snapshot's endpoint bindings. + string policy_hash = 8; + // Nonzero when material was withheld; installing an empty map is not readiness. + ProviderReadinessReason readiness_reason = 9; + // Complete desired set of non-secret managed files, keyed by absolute path. + map files = 10; + // Credential environment variables whose opaque resolver handle remains + // stable across value-only provider revisions. These keys are still secrets; + // this metadata only selects the stable external-placeholder behavior. + repeated string stable_placeholder_environment_keys = 11; + // Supporting gateways always acknowledge the stable-placeholder delivery + // contract, including responses without opted-in credentials. Supervisors + // require acknowledgment for declared or previously activated external + // stable credentials; ordinary legacy delivery does not require it. + bool supports_stable_placeholder_environment_keys = 12; +} + +message ExchangeProviderSubjectTokenRequest { + // The sandbox ID. Must match the authenticated sandbox principal. + string sandbox_id = 1; + + // Attached provider record holding the configured subject token credential. + string provider = 2; + + // Provider profile credential that declares the token_exchange grant. + string credential_key = 3; + + // Supervisor JWT-SVID. The gateway verifies this and uses its `sub` claim + // as the requested audience for the intermediate token. + string supervisor_jwt_svid = 4 [(openshell.options.v1.secret) = true]; +} + +message ExchangeProviderSubjectTokenResponse { + reserved 2; + reserved "expires_in"; + string access_token = 1 [(openshell.options.v1.secret) = true]; + google.protobuf.Duration expires_after = 102; + string token_type = 3; +} + +// --------------------------------------------------------------------------- +// Policy update messages +// --------------------------------------------------------------------------- + +// Update sandbox policy request. +message UpdateConfigRequest { + // Workspace scope. Omit for global updates; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 10; + // The new policy to apply. + // + // Sandbox scope (`global=false`): + // - only network_policies may differ from create-time + // policy; static fields must match version 1. + // + // Global scope (`global=true`): + // - applies to all sandboxes in full (no merge). + openshell.sandbox.v1.SandboxPolicy policy = 2; + // Optional single setting key to mutate. + string setting_key = 3; + // Setting value for upsert operations. + openshell.sandbox.v1.SettingValue setting_value = 4; + // Delete the setting key from scope. + // Sandbox-scoped deletes are rejected; only global delete is supported. + bool delete_setting = 5; + // Apply mutation at gateway-global scope. + bool global = 6; + // Batched incremental policy merge operations. Sandbox-scoped only. + repeated PolicyMergeOperation merge_operations = 7; + // Expected resource version for optimistic concurrency control (sandbox-scoped only). + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + // Ignored for global-scoped updates. + uint64 expected_resource_version = 8; + // Caller-provided annotations associated with a sandbox-scoped update. Values + // must not contain secrets; the gateway treats them as opaque metadata and does + // not interpret or verify their semantics. For policy updates, the gateway + // stores the annotations immutably with the revision and merges them into + // sandbox metadata as a convenience projection. For setting-only updates, it + // only merges them into sandbox metadata. + map annotations = 9; + // Required for sandbox-scoped updates and empty for global updates. + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 11; +} + +message PolicyMergeOperation { + oneof operation { + AddNetworkRule add_rule = 1; + RemoveNetworkEndpoint remove_endpoint = 2; + RemoveNetworkRule remove_rule = 3; + AddDenyRules add_deny_rules = 4; + AddAllowRules add_allow_rules = 5; + RemoveNetworkBinary remove_binary = 6; + } +} + +message AddNetworkRule { + string rule_name = 1; + openshell.sandbox.v1.NetworkPolicyRule rule = 2; +} + +message RemoveNetworkEndpoint { + string rule_name = 1; + string host = 2; + uint32 port = 3; +} + +message RemoveNetworkRule { + string rule_name = 1; +} + +// Exact endpoint and complete authorization scope affected by an L7 append. +// All ports and binaries must match the stored target; omitted scope is invalid. +message L7RuleTarget { + string rule_name = 1; + string host = 2; + repeated uint32 ports = 3; + // An absent path requires a unique endpoint. An empty path selects an + // endpoint without a path selector. This is not the appended request path. + optional string path = 4; + // Declare either a nonempty binary list or any_binary, never both. + repeated openshell.sandbox.v1.NetworkBinary binaries = 5; + bool any_binary = 6; +} + +message AddDenyRules { + reserved 1, 2; + reserved "host", "port"; + repeated openshell.sandbox.v1.L7DenyRule deny_rules = 3; + L7RuleTarget target = 4; +} + +message AddAllowRules { + reserved 1, 2; + reserved "host", "port"; + repeated openshell.sandbox.v1.L7Rule rules = 3; + L7RuleTarget target = 4; +} + +message RemoveNetworkBinary { + string rule_name = 1; + string binary_path = 2; +} + +// Update sandbox policy response. +message UpdateConfigResponse { + // Assigned policy version (monotonically increasing per sandbox). + uint32 version = 1; + // SHA-256 hash of the serialized policy payload. + string policy_hash = 2; + // Settings revision for the scope that was modified. + uint64 settings_revision = 3; + // True when a setting delete operation removed an existing key. + bool deleted = 4; + // Sandbox metadata annotations after the update. Empty for global updates. + map annotations = 5; +} + +// Get sandbox policy status request. +message GetSandboxPolicyStatusRequest { + // Workspace scope. Omit for global queries; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // The specific policy version to query. 0 means latest. + uint32 version = 2; + // Query global policy revisions instead of a sandbox-scoped one. + bool global = 3; + string sandbox = 1; +} + +// Get sandbox policy status response. +message GetSandboxPolicyStatusResponse { + // The queried policy revision. + SandboxPolicyRevision revision = 1; + // The currently active (loaded) policy version for this sandbox. + uint32 active_version = 2; +} + +// List sandbox policies request. +message ListSandboxPoliciesRequest { + // Workspace scope. Omit for global queries; otherwise select one named workspace. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 5; + // The maximum number of revisions to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 2; + // Token from a previous ListSandboxPolicies response. All other request + // parameters except page_size must match the request that produced it. + string page_token = 3; + // List global policy revisions instead of sandbox-scoped ones. + bool global = 4; + string sandbox = 1; +} + +// List sandbox policies response. +message ListSandboxPoliciesResponse { + // Invalid historical payloads remain visible as failed projections so one + // legacy row cannot hide the rest of the policy history. + repeated SandboxPolicyRevision revisions = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Report policy load status (called by sandbox runtime after reload attempt). +message ReportPolicyStatusRequest { + // Sandbox id. + string sandbox_id = 1; + // The policy version that was attempted. + uint32 version = 2; + // Load result status. + PolicyStatus status = 3; + // Error message if status is FAILED. + string load_error = 4; +} + +// Report policy status response. +message ReportPolicyStatusResponse {} + +enum ConfigurationAdmissionState { + CONFIGURATION_ADMISSION_STATE_UNSPECIFIED = 0; + CONFIGURATION_ADMISSION_STATE_PENDING = 1; + CONFIGURATION_ADMISSION_STATE_ACCEPTED = 2; + CONFIGURATION_ADMISSION_STATE_REJECTED = 3; +} + +message SandboxConfigurationAdmission { + string instance_id = 1; + ConfigurationAdmissionState state = 2; + uint32 policy_version = 3; + string policy_hash = 4; + uint64 config_revision = 5; + uint64 provider_env_revision = 6; + string error = 7; +} + +message ReportSandboxConfigurationRequest { + string sandbox_id = 1; + SandboxConfigurationAdmission admission = 2; + // Pending registration replaces only this previously observed instance. + string expected_instance_id = 3; +} + +message ReportSandboxConfigurationResponse {} + +// A versioned policy revision with metadata. +message SandboxPolicyRevision { + reserved 5, 6; + reserved "created_at_ms", "loaded_at_ms"; + // Policy version (monotonically increasing per sandbox). + uint32 version = 1; + // SHA-256 hash of the canonical serialized policy payload. Empty in a + // ListSandboxPolicies projection when the stored payload is invalid under + // the current schema and therefore has no trusted canonical identity. + string policy_hash = 2; + // Load status of this revision. ListSandboxPolicies reports FAILED when a + // stored historical payload is invalid under the current schema, regardless + // of its persisted sandbox load status. + PolicyStatus status = 3; + // Sandbox load error, or the schema-validation diagnostic for an invalid + // historical row returned by ListSandboxPolicies. + string load_error = 4; + // Time when this revision was created. + google.protobuf.Timestamp created_time = 105; + // Time when this revision was loaded by the sandbox. Absent if not loaded. + google.protobuf.Timestamp loaded_time = 106; + // The full policy (only populated when explicitly requested). + openshell.sandbox.v1.SandboxPolicy policy = 7; + // Immutable provenance supplied with this policy revision. + map provenance = 8; +} + +// Policy load status. +enum PolicyStatus { + POLICY_STATUS_UNSPECIFIED = 0; + // Server received the update; sandbox has not yet loaded it. + POLICY_STATUS_PENDING = 1; + // Sandbox successfully applied this policy version. + POLICY_STATUS_LOADED = 2; + // Sandbox attempted to apply but failed; LKG policy remains active. + // ListSandboxPolicies also uses FAILED for historical payloads that are + // invalid under the current schema; load_error contains the diagnostic. + POLICY_STATUS_FAILED = 3; + // A newer version was persisted before the sandbox loaded this one. + POLICY_STATUS_SUPERSEDED = 4; +} + +// --------------------------------------------------------------------------- +// Sandbox logs messages +// --------------------------------------------------------------------------- + +// Get sandbox logs request (one-shot fetch). +message GetSandboxLogsRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; + reserved 3; + reserved "since_ms"; + // Canonical sandbox name. + string sandbox = 1; + // Maximum number of log lines to return. 0 means use default (2000). + uint32 lines = 2; + // Only include logs at or after this time. Absence means no filter. + google.protobuf.Timestamp since_time = 103; + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + repeated string sources = 4; + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + string min_level = 5; +} + +// Batch of log lines pushed from sandbox to server. +message PushSandboxLogsRequest { + // The sandbox ID. + string sandbox_id = 1; + // Log lines to ingest. + repeated SandboxLogLine logs = 2; +} + +// Push sandbox logs response. +message PushSandboxLogsResponse {} + +// Get sandbox logs response. +message GetSandboxLogsResponse { + // Log lines in chronological order. + repeated SandboxLogLine logs = 1; + // Total number of lines in the server's buffer for this sandbox. + uint32 buffer_total = 2; +} + +// --------------------------------------------------------------------------- +// Supervisor session messages +// --------------------------------------------------------------------------- + +// Envelope for supervisor-to-gateway messages on the ConnectSupervisor stream. +message SupervisorMessage { + oneof payload { + SupervisorHello hello = 1; + SupervisorHeartbeat heartbeat = 2; + RelayOpenResult relay_open_result = 3; + RelayClose relay_close = 4; + } +} + +// Envelope for gateway-to-supervisor messages on the ConnectSupervisor stream. +message GatewayMessage { + oneof payload { + SessionAccepted session_accepted = 1; + SessionRejected session_rejected = 2; + GatewayHeartbeat heartbeat = 3; + RelayOpen relay_open = 4; + RelayClose relay_close = 5; + SessionRedirect session_redirect = 6; + } +} + +// Supervisor identifies itself and the sandbox it manages. +message SupervisorHello { + // Sandbox ID this supervisor manages. + string sandbox_id = 1; + // Supervisor instance ID (e.g. boot id or process epoch). + string instance_id = 2; + // Monotonic counter scoped to instance_id. Incremented for each reconnect so + // gateways can distinguish a fresh supervisor connection from stale cleanup. + uint64 connection_epoch = 3; + // The supervisor can report credential, policy, and launch-environment installation. + bool supports_provider_readiness = 4; + // Set when this connection is the result of following a SessionRedirect. The + // receiving gateway serves a redirected hello locally instead of redirecting + // again, so a disagreement about membership cannot bounce the supervisor + // between replicas. A gateway that is shutting down may still redirect it. + bool redirected = 5; + // The supervisor understands SessionRedirect. Gateways never redirect a + // supervisor that does not set this. + bool supports_session_redirect = 6; +} + +// Gateway accepts the supervisor session. +message SessionAccepted { + reserved 2; + reserved "heartbeat_interval_secs"; + // Gateway-assigned session ID for this connection. + string session_id = 1; + // Recommended heartbeat interval. + google.protobuf.Duration heartbeat_interval = 102; +} + +// Gateway rejects the supervisor session. +message SessionRejected { + // Human-readable rejection reason. + string reason = 1; +} + +// Gateway declines to own this session and names the replica that should. +// +// Sent instead of SessionAccepted when the placement ring puts the sandbox on +// a different live replica, and to established sessions when their gateway +// shuts down. The supervisor reconnects to peer_endpoint once with +// SupervisorHello.redirected set, and falls back to its configured gateway +// address if that fails. +message SessionRedirect { + // Peer endpoint of the replica that should own this sandbox. + string peer_endpoint = 1; + // Replica ID that should own this sandbox, for logging. + string owner_replica_id = 2; +} + +// Supervisor heartbeat. +message SupervisorHeartbeat {} + +// Gateway heartbeat. +message GatewayHeartbeat {} + +// Terminal result reported before the supervisor shuts down. A successful RPC +// response confirms that the result was durably handled by the gateway. +message ReportMainProcessExitRequest { + string sandbox_id = 1; + string instance_id = 2; + // Normalized process result. Signal exits use 128 + signal number. + int32 exit_code = 3; +} + +message ReportMainProcessExitResponse {} + +// Terminal-delivery completion reported after all expected foreground SSH +// attachments close. A successful response permits ephemeral cleanup. +message FinalizeMainProcessExitRequest { + string sandbox_id = 1; + string instance_id = 2; +} + +message FinalizeMainProcessExitResponse {} + +// Gateway requests the supervisor to open a relay channel. +// +// On receiving this, the supervisor should initiate a RelayStream RPC to +// the gateway, sending a RelayInit in the first RelayFrame to associate +// the new HTTP/2 stream with the pending relay slot. The supervisor +// bridges that stream to the requested local target. +message RelayOpen { + // Gateway-allocated channel identifier (UUID). + string channel_id = 1; + // Target the supervisor should dial inside the sandbox. + // If absent, supervisors treat the relay as SSH for compatibility. + oneof target { + SshRelayTarget ssh = 2; + TcpRelayTarget tcp = 3; + } + // Optional service identifier for audit/correlation. + string service_id = 5; +} + +// Built-in SSH relay target. +message SshRelayTarget {} + +// TCP target dialed by the supervisor from inside the sandbox. +message TcpRelayTarget { + // Phase 1 accepts loopback only: 127.0.0.1, ::1, or localhost. + string host = 1; + // Target port. Must fit in u16 and be non-zero. + uint32 port = 2; +} + +// Initial RelayStream frame sent by the supervisor to claim a pending relay. +message RelayInit { + // Gateway-allocated channel identifier (UUID). + string channel_id = 1; +} + +// A single frame on the RelayStream RPC. +// +// The supervisor MUST send `init` as the first frame. All subsequent frames +// in either direction carry raw bytes in `data`. +message RelayFrame { + oneof payload { + RelayInit init = 1; + bytes data = 2; + } +} + +// Initial frame for gateway peer relay forwarding. +message PeerRelayInit { + // Stable sandbox UUID whose supervisor relay should be opened. + string sandbox_id = 1; + // Relay target to ask the owning gateway to open on its local supervisor + // session. The channel_id is assigned by the forwarding gateway. + RelayOpen relay_open = 2; + // Gateway replica id that initiated the peer relay. + string requester_replica_id = 3; +} + +// A single frame on the gateway-to-gateway peer relay RPC. +message PeerRelayFrame { + oneof payload { + PeerRelayInit init = 1; + bytes data = 2; + } +} + +// Supervisor reports the result of a relay open request. +message RelayOpenResult { + // Channel identifier from the RelayOpen request. + string channel_id = 1; + // True if the relay was successfully established. + bool success = 2; + // Error message if success is false. + string error = 3; +} + +// Either side requests closure of a relay channel. +message RelayClose { + // Channel identifier to close. + string channel_id = 1; + // Optional reason for closure. + string reason = 2; +} + +// --------------------------------------------------------------------------- +// Service status +// --------------------------------------------------------------------------- + +// Service status enum. +enum ServiceStatus { + SERVICE_STATUS_UNSPECIFIED = 0; + SERVICE_STATUS_HEALTHY = 1; + SERVICE_STATUS_DEGRADED = 2; + SERVICE_STATUS_UNHEALTHY = 3; +} + +// --------------------------------------------------------------------------- +// Draft policy recommendation messages +// --------------------------------------------------------------------------- + +// Observed HTTP method+path pattern from L7 inspection. +message L7RequestSample { + // HTTP method: GET, POST, PUT, DELETE, etc. + string method = 1; + // HTTP path: /v1/models, /repos/myorg/issues + string path = 2; + // L7 decision: "audit" or "deny" (allowed requests not collected). + string decision = 3; + // Number of times this (method, path) was observed. + uint32 count = 4; +} + +// Structured denial summary from sandbox aggregator. +message DenialSummary { + reserved 7, 8; + reserved "first_seen_ms", "last_seen_ms"; + // Sandbox ID that produced this summary. + string sandbox_id = 1; + // Denied destination host. + string host = 2; + // Denied destination port. + uint32 port = 3; + // Binary that attempted the connection. + string binary = 4; + // Process ancestor chain. + repeated string ancestors = 5; + // Denial reason from OPA evaluation. + string deny_reason = 6; + // Time of the first denial. + google.protobuf.Timestamp first_seen_time = 107; + // Time of the most recent denial. + google.protobuf.Timestamp last_seen_time = 108; + // Number of denials in the current window. + uint32 count = 9; + // Events dropped during aggregator cooldown. + uint32 suppressed_count = 10; + // Cumulative lifetime count (never resets). + uint32 total_count = 11; + // Distinct cmdline strings observed (sanitized of credentials). + repeated string sample_cmdlines = 12; + // SHA-256 of the binary for audit trail. + string binary_sha256 = 13; + // True if emitted by stale-flush rather than threshold. + bool persistent = 14; + // Denial category: "l4_deny", "l7_deny", "l7_audit", "ssrf". + string denial_stage = 15; + // Observed HTTP request patterns (from L7 inspection). + repeated L7RequestSample l7_request_samples = 16; + // True if L7 inspection was active during observation window. + bool l7_inspection_active = 17; +} + +// Count of denied actions grouped only by sanitized telemetry category. +message DenialGroupCount { + // Sanitized denial category, e.g. "connect_policy", "l7_policy", "ssrf". + string deny_group = 1; + // Number of denied actions in this category. + uint32 denied_count = 2; +} + +// Anonymous sandbox network activity counters. This intentionally excludes +// hosts, paths, binaries, raw deny reasons, sandbox IDs, and user content. +message NetworkActivitySummary { + // Total observed network activities in the current window. + uint32 network_activity_count = 1; + // Total denied actions in the current window. + uint32 denied_action_count = 2; + // Denied action counts grouped by sanitized category. + repeated DenialGroupCount denials_by_group = 3; +} + +// A proposed policy rule with rationale and approval status. +message PolicyChunk { + reserved 9, 10, 14, 15; + reserved "created_at_ms", "decided_at_ms", "first_seen_ms", "last_seen_ms"; + // Unique chunk identifier. + string id = 1; + // Approval status: "pending", "approved", "rejected". + string status = 2; + // Proposed network_policies map key. + string rule_name = 3; + // The proposed network policy rule. + openshell.sandbox.v1.NetworkPolicyRule proposed_rule = 4; + // Human-readable explanation of why this rule is proposed. + string rationale = 5; + // Security concerns flagged by analysis (empty if none). + string security_notes = 6; + // Analysis confidence (0.0-1.0). 0 for mechanistic mode. + float confidence = 7; + // IDs of denial summaries that led to this chunk. + repeated string denial_summary_ids = 8; + // Time when this chunk was created. + google.protobuf.Timestamp created_time = 109; + // Time when the user approved or rejected the chunk. Absent if undecided. + google.protobuf.Timestamp decided_time = 110; + // Recommendation stage: "initial" or "refined" (progressive L7 visibility). + string stage = 11; + // For stage="refined": the initial chunk this replaces. + string supersedes_chunk_id = 12; + // How many times this endpoint has been seen across denial flush cycles. + int32 hit_count = 13; + // First time this endpoint was proposed. + google.protobuf.Timestamp first_seen_time = 114; + // Most recent time this endpoint was proposed again. + google.protobuf.Timestamp last_seen_time = 115; + // Binary path that triggered the denial (denormalized for display convenience). + string binary = 16; + // Validation verdict from gateway-side static checks (prover output). + // Free-form summary string for human consumption in the inbox card. + // Empty until the prover has run for this chunk. + string validation_result = 17; + // Operator-supplied free-form text accompanying a rejection. Populated + // when the reviewer rejects via `RejectDraftChunkRequest.reason`; surfaced + // back to the in-sandbox agent so it can revise the proposal. + // Empty for non-rejected chunks. + string rejection_reason = 18; + // Gateway-side merge/application preflight failure. Kept separate from + // prover output and operator rejection so clients can explain why a + // prover-clean proposal is not currently applicable. + string application_error = 19; + // Opaque digest binding review to the exact live inputs and complete + // effective candidate evaluated by the gateway. + string review_token = 20; + // Deterministic hashes for compact candidate display and diagnostics. + string current_effective_policy_hash = 21; + string candidate_effective_policy_hash = 22; + // Complete effective policies used for review. These contain policy + // configuration only; credential secret values are never materialized. + openshell.sandbox.v1.SandboxPolicy current_effective_policy = 23; + openshell.sandbox.v1.SandboxPolicy candidate_effective_policy = 24; +} + +// Notification that the draft policy was updated. +message DraftPolicyUpdate { + // Current draft version. + uint64 draft_version = 1; + // Number of new chunks added in this update. + uint32 new_chunks = 2; + // Total pending chunks awaiting approval. + uint32 total_pending = 3; + // Brief description of what changed. + string summary = 4; +} + +// Submit analysis results from sandbox to gateway. +message SubmitPolicyAnalysisRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 6; + // Aggregated denial summaries. + repeated DenialSummary summaries = 1; + // Proposed policy chunks (validated by sandbox OPA engine). + repeated PolicyChunk proposed_chunks = 2; + // Analysis mode. `mechanistic` is the observation-driven path from the + // denial aggregator — chunks targeting the same host|port|binary fold + // into one row with hit_count incremented. `agent_authored` is an + // intentional proposal from an in-sandbox agent — each submission lands + // as its own chunk so the redraft-after-rejection loop has a stable id + // to watch. Other values are treated as agent-style (no dedup) so a new + // mode does not silently collapse proposals. + string analysis_mode = 3; + // Sandbox name. The authenticated sandbox principal remains authoritative + // for this internal callback. + string name = 4; + // Anonymous network activity counters. + repeated NetworkActivitySummary network_activity_summaries = 5; +} + +message SubmitPolicyAnalysisResponse { + // Number of chunks accepted by the gateway. + uint32 accepted_chunks = 1; + // Number of chunks rejected by gateway validation. + uint32 rejected_chunks = 2; + // Reasons for each rejected chunk. + repeated string rejection_reasons = 3; + // Server-assigned chunk IDs for the accepted chunks, in submission order. + // Agents use these to watch proposal state via policy.local's + // GET /v1/proposals/{id} and /wait endpoints. + repeated string accepted_chunk_ids = 4; +} + +// Get draft policy for a sandbox. +message GetDraftPolicyRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Optional status filter: "pending", "approved", "rejected", or "" for all. + string status_filter = 2; + string sandbox = 1; +} + +message GetDraftPolicyResponse { + reserved 4; + reserved "last_analyzed_at_ms"; + // Draft policy chunks. + repeated PolicyChunk chunks = 1; + // LLM-generated summary of all analysis (empty in mechanistic mode). + string rolling_summary = 2; + // Current draft version. + uint64 draft_version = 3; + // Time when the last analysis completed. + google.protobuf.Timestamp last_analyzed_time = 104; +} + +// Approve a single draft chunk. +message ApproveDraftChunkRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Chunk ID to approve. + string chunk_id = 2; + // Token returned with the reviewed PolicyChunk. Approval fails with + // FAILED_PRECONDITION if live decision inputs no longer match it. + string review_token = 4; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +message ApproveDraftChunkResponse { + // New policy version after merge. + uint32 policy_version = 1; + // SHA-256 hash of the new policy. + string policy_hash = 2; +} + +// Reject a single draft chunk. +message RejectDraftChunkRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // Chunk ID to reject. + string chunk_id = 2; + // Optional reason for rejection (fed to LLM context in future analysis). + string reason = 3; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +message RejectDraftChunkResponse {} + +// Approve all pending chunks. +message DraftChunkApproval { + string chunk_id = 1; + string review_token = 2; +} + +message ApproveAllDraftChunksRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // Include chunks with security_notes (default false: skips them). + bool include_security_flagged = 2; + // Exact reviewed chunks and tokens. The server validates them against one + // live snapshot, stages compatible operations in order, and writes once. + repeated DraftChunkApproval approvals = 3; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +message ApproveAllDraftChunksResponse { + // New policy version after merge. + uint32 policy_version = 1; + // SHA-256 hash of the new policy. + string policy_hash = 2; + // Number of chunks approved. + uint32 chunks_approved = 3; + // Number of chunks skipped for any reason, including security flags, + // stale or invalid candidates, and conflicts within the staged batch. + uint32 chunks_skipped = 4; +} + +// Edit a pending chunk in-place. +message EditDraftChunkRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 4; + // Chunk ID to edit. + string chunk_id = 2; + // The modified rule (replaces existing proposed_rule). + openshell.sandbox.v1.NetworkPolicyRule proposed_rule = 3; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 5; +} + +message EditDraftChunkResponse {} + +// Reverse an approval (remove merged rule from active policy). +message UndoDraftChunkRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + // Chunk ID to undo. + string chunk_id = 2; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 4; +} + +message UndoDraftChunkResponse { + // New policy version after removal. + uint32 policy_version = 1; + // SHA-256 hash of the updated policy. + string policy_hash = 2; +} + +// Clear all pending draft chunks for a sandbox. +message ClearDraftChunksRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string sandbox = 1; + // Optional nonzero UUID for durable at-most-once admission. Successful results + // can be replayed for 24 hours; see the API errors and retries reference. + string request_id = 3; +} + +message ClearDraftChunksResponse { + // Number of chunks cleared. + uint32 chunks_cleared = 1; +} + +// Get decision history for a sandbox's draft policy. +message GetDraftHistoryRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 2; + string sandbox = 1; +} + +message DraftHistoryEntry { + reserved 1; + reserved "timestamp_ms"; + // Time when the event occurred. + google.protobuf.Timestamp event_time = 101; + // Event type: "denial_detected", "analysis_cycle", "approved", + // "rejected", "edited", "undone", "cleared". + string event_type = 2; + // Human-readable description. + string description = 3; + // Associated chunk ID (if applicable). + string chunk_id = 4; +} + +message GetDraftHistoryResponse { + // Chronological decision history. + repeated DraftHistoryEntry entries = 1; +} + +// --------------------------------------------------------------------------- +// Workspace messages +// --------------------------------------------------------------------------- + +// Create workspace request. +message CreateWorkspaceRequest { + // Workspace name. Must be a valid DNS-1123 label. + string name = 1; + // Optional labels for the workspace (key-value metadata). + map labels = 2; + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + string request_id = 3; +} + +// Create workspace response. +message CreateWorkspaceResponse { + openshell.datamodel.v1.Workspace workspace = 1; +} + +// Get workspace request. +message GetWorkspaceRequest { + // Workspace name (canonical lookup key). + string name = 1; +} + +// Get workspace response. +message GetWorkspaceResponse { + openshell.datamodel.v1.Workspace workspace = 1; +} + +// List workspaces request. +message ListWorkspacesRequest { + // The maximum number of workspaces to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 1; + // Token from a previous ListWorkspaces response. All other request parameters + // except page_size must match the request that produced it. + string page_token = 2; + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + string label_selector = 3; +} + +// List workspaces response. +message ListWorkspacesResponse { + repeated openshell.datamodel.v1.Workspace workspaces = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Delete workspace request. +message DeleteWorkspaceRequest { + // Workspace name (canonical lookup key). + string name = 1; + bool allow_missing = 2; + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + string request_id = 3; +} + +// Delete workspace response. +message DeleteWorkspaceResponse { + reserved 1; + reserved "deleted"; + DeletionOutcome outcome = 2; +} + +// --------------------------------------------------------------------------- +// Workspace membership messages +// --------------------------------------------------------------------------- + +// Workspace-scoped role for members. +enum WorkspaceRole { + WORKSPACE_ROLE_UNSPECIFIED = 0; + WORKSPACE_ROLE_USER = 1; + WORKSPACE_ROLE_ADMIN = 2; +} + +// Stable recovery action for the most recent provider credential refresh +// failure. Kept after the pre-existing enums so adding it does not renumber +// their generated descriptors. Clients should use this field instead of +// parsing last_error. +enum ProviderCredentialRefreshRecoveryAction { + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY = 1; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE = 2; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION = 3; + PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE = 4; +} + +// Policy applied when the canonical main process exits outside an intentional +// stop or delete operation. Kept after existing enums so generated enum +// descriptors remain stable. +enum SandboxRestartPolicy { + SANDBOX_RESTART_POLICY_UNSPECIFIED = 0; + SANDBOX_RESTART_POLICY_NEVER = 1; + SANDBOX_RESTART_POLICY_ON_FAILURE = 2; + SANDBOX_RESTART_POLICY_ALWAYS = 3; +} + +// Workspace membership record. +message WorkspaceMember { + openshell.datamodel.v1.ObjectMeta metadata = 1; + // OIDC subject claim identifying the principal. + string principal_subject = 2; + // Role assigned to the principal within the workspace. + WorkspaceRole role = 3; +} + +// Add workspace member request. +message AddWorkspaceMemberRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 1; + // OIDC subject claim identifying the principal. + string principal_subject = 2; + // Role to assign. + WorkspaceRole role = 3; + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + string request_id = 4; +} + +// Add workspace member response. +message AddWorkspaceMemberResponse { + WorkspaceMember member = 1; +} + +// Remove workspace member request. +message RemoveWorkspaceMemberRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 1; + // OIDC subject claim identifying the principal to remove. + string principal_subject = 2; + bool allow_missing = 3; + // Optional nonzero UUID. Same ID and payload replay success for 24 hours. + string request_id = 4; +} + +// Remove workspace member response. +message RemoveWorkspaceMemberResponse { + reserved 1; + reserved "removed"; + DeletionOutcome outcome = 2; +} + +// Result of a public delete, membership removal, or session revocation. +// Default requests return NOT_FOUND for a missing target. With allow_missing, +// only a missing target becomes ALREADY_ABSENT; parent lookup, authorization, +// validation, precondition, and backend errors retain their normal status. +// These results describe the targeted resource, not a same-name replacement. +enum DeletionOutcome { + // No outcome was supplied. Never infer completion from this value. + DELETION_OUTCOME_UNSPECIFIED = 0; + // The targeted gateway resource is removed (or the SSH session is revoked). + // Downstream platform garbage collection may still be finishing. + DELETION_OUTCOME_COMPLETED = 1; + // Sandbox deletion is accepted but its gateway record still exists. + // Observe the targeted sandbox ID until it disappears for completion. + DELETION_OUTCOME_ACCEPTED = 2; + // The target did not exist and allow_missing was true. + DELETION_OUTCOME_ALREADY_ABSENT = 3; +} + +// List workspace members request. +message ListWorkspaceMembersRequest { + // Workspace scope. Only a named workspace selection is accepted. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 1; + // The maximum number of members to return. Zero uses 100. Values above + // 1000 are coerced to 1000; negative values are invalid. + int32 page_size = 2; + // Token from a previous ListWorkspaceMembers response. All other request + // parameters except page_size must match the request that produced it. + string page_token = 3; +} + +// List workspace members response. +message ListWorkspaceMembersResponse { + repeated WorkspaceMember members = 1; + // Token for the next page. Empty when there are no subsequent pages. + string next_page_token = 2; +} + +// Short-lived credential for one policy-authorized extension service. +// Kept at the end of the file so adding it does not renumber existing +// generated message descriptors. +message ExtensionServiceCredential { + reserved 3; + reserved "expires_at_ms"; + // Operator registration name used to correlate the credential with the + // stable service registration delivered by GetSandboxConfig. + string service_name = 1; + // Gateway-minted JWT with an audience derived from the registration. + string token = 2 [(openshell.options.v1.secret) = true]; + // Absolute expiry of the token. + google.protobuf.Timestamp expiration_time = 103; +} + +// Last observed network result for a configured external tool endpoint. +// Results describe accepted traffic observations, not present availability. +enum EndpointResult { + ENDPOINT_RESULT_UNSPECIFIED = 0; + // No exchange has been observed under the current configuration and session. + ENDPOINT_RESULT_NO_OBSERVED_EXCHANGE = 1; + // An upstream HTTP status below 400 was received. Its body can still contain + // an MCP error; this result does not establish tool-call success. + ENDPOINT_RESULT_HTTP_RESPONSE_RECEIVED = 2; + // OpenShell policy denied the request locally. + ENDPOINT_RESULT_POLICY_DENIED = 3; + // An applicable OpenShell-managed credential was unavailable. + ENDPOINT_RESULT_CREDENTIAL_UNAVAILABLE = 4; + // TLS setup for the upstream connection failed. + ENDPOINT_RESULT_TLS_FAILED = 5; + // The upstream transport failed before an HTTP response arrived. + ENDPOINT_RESULT_TRANSPORT_FAILED = 6; + // The upstream service returned an HTTP rejection. + ENDPOINT_RESULT_UPSTREAM_REJECTED = 7; +} + +// One redacted endpoint result in a supervisor's complete status report. +message EndpointObservation { + // Stable identifier derived from the configured host, ports, and path. + string endpoint_id = 1; + // Latest result under the reported configuration and supervisor session. + EndpointResult result = 2; +} + +// Complete endpoint status report for the caller's current configuration. +message ReportEndpointStatusRequest { + // Sandbox id. Must match the authenticated sandbox principal. + string sandbox_id = 1; + // Hash from the active effective policy delivered by the gateway. + string policy_hash = 2; + // Provider environment revision delivered with the active configuration. + uint64 provider_env_revision = 3; + // Exactly one result for every distinct observed endpoint in the policy. + repeated EndpointObservation observations = 4; + // Endpoints with a new observation in this batch. Omitted endpoints retain + // their prior report time; an identical retry never advances report time. + repeated string observed_endpoint_ids = 5; + // Active ConnectSupervisor session that owns these observations. + string supervisor_session_id = 6; + // Monotonically increasing sequence within the authenticated session. Gaps + // are allowed when an inventory reset supersedes a frozen snapshot. Retrying + // a report preserves its complete body and sequence for idempotent acknowledgement. + uint64 report_sequence = 7; +} + +// Empty acknowledgement for a persisted endpoint status report. +message ReportEndpointStatusResponse {} + +// A configured endpoint and its last accepted network result in one record. +// Address fields contain policy selectors, never request URLs or credentials. +message EndpointStatus { + reserved 6; + reserved "last_reported_at"; + // Stable identifier for selecting this endpoint without parsing display text. + string endpoint_id = 1; + // Lowercase configured endpoint host. + string host = 2; + // Sorted, deduplicated effective endpoint ports. Validated endpoints have at + // least one port. + repeated uint32 ports = 3; + // Canonical configured path selector; an unrestricted path is /**. + string path = 4; + // Last accepted result, aggregated across configured callers and ports. + // NoObservedExchange retains the address and has no report timestamp. + EndpointResult last_result = 5; + // Time when the gateway accepted the observation. This is not the request + // time: still-valid evidence can be reaccepted after a reset. Identical + // same-sequence retries do not advance it. Absent until a result is reported. + google.protobuf.Timestamp last_reported_time = 106; +} + +// Durable provisioning attempt, independent of supervisor registration and polling. +message SandboxProvisioning { + string attempt_id = 1; + string configuration_change_id = 2; + google.protobuf.Timestamp configuration_change_time = 3; + google.protobuf.Timestamp first_rejection_time = 4; + // Active preparation or admission-repair deadline. Absent after Ready/timeout. + google.protobuf.Timestamp deadline = 5; + google.protobuf.Timestamp timeout_time = 6; + // Set only after both supervisor and workload compute have been reclaimed. + google.protobuf.Timestamp cleanup_completed_time = 7; + // A safe gateway-authored diagnostic; never a raw driver error. + string cleanup_error = 8; + // Durable backoff for interrupted or failed reclamation. + google.protobuf.Timestamp cleanup_retry_time = 9; + // Attachment edits have their own durable clock; status writes do not change it. + string attachment_change_id = 10; + google.protobuf.Timestamp attachment_change_time = 11; + // Absolute ceiling for image preparation and initial supervisor startup. + // Set once per new attempt; progress, configuration writes, and restarts + // cannot extend it. Absent on attempts created before preparation timing. + google.protobuf.Timestamp preparation_deadline = 12; + // First authenticated supervisor configuration report for this attempt. + // Starts the admission repair window. Absent while preparing, including + // after a preparation timeout. Duplicate reports never change this value. + google.protobuf.Timestamp admission_start_time = 13; + // An owned driver create/start future is still pending. Claimed before + // dispatch and cleared on its actual success or error response. Monitor + // interruption, owner loss, and StopSandbox/NotFound do not clear it. + // While set, cleanup cannot complete and retry/deletion remain blocked. + // This does not strengthen a driver's transport-error settlement contract. + bool driver_operation_pending = 14; + // Unique for each claimed driver operation, including recovery and automatic + // restart. Retained after settlement to reject delayed callbacks from an + // earlier operation on the same attempt and runtime generation. + string driver_operation_id = 15; +} + +// Create-time request to expose one loopback HTTP service in a sandbox. +message SandboxServiceExposure { + // Service name within the sandbox. Empty selects the unnamed endpoint. + string service = 1; + // Loopback TCP port inside the sandbox. + uint32 target_port = 2; + // Application authorization behavior. Omission resolves to STRIP. + ServiceAuthorizationMode authorization_mode = 3; +} + +// Controls whether an exposed service receives the request's application +// Authorization header. +enum ServiceAuthorizationMode { + // Omission preserves the secure legacy behavior and resolves to STRIP. + SERVICE_AUTHORIZATION_MODE_UNSPECIFIED = 0; + // Remove Authorization before forwarding to the sandbox service. + SERVICE_AUTHORIZATION_MODE_STRIP = 1; + // Forward one syntactically valid bearer Authorization header unchanged. + SERVICE_AUTHORIZATION_MODE_BEARER_PASSTHROUGH = 2; +} diff --git a/third_party/openshell/proto/options.proto b/third_party/openshell/proto/options.proto new file mode 100644 index 0000000..6f127cb --- /dev/null +++ b/third_party/openshell/proto/options.proto @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.options.v1; + +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", "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 + // global_role. + string workspace_role = 2; + // Global role required (checked by middleware via OIDC claims): + // "platform_admin". Mutually exclusive with workspace_role. + string global_role = 3; + // Required OIDC scope on the bearer path (e.g. "sandbox:read"). + string scope = 4; +} + +extend google.protobuf.MethodOptions { + // Authorization metadata for a gRPC method. + AuthorizationRule authorization = 50000; +} + +// Marks a protobuf field whose value must not cross generic observation or +// extension boundaries such as gateway interceptors. +extend google.protobuf.FieldOptions { + bool secret = 50001; +} diff --git a/third_party/openshell/proto/sandbox.proto b/third_party/openshell/proto/sandbox.proto new file mode 100644 index 0000000..7f7f407 --- /dev/null +++ b/third_party/openshell/proto/sandbox.proto @@ -0,0 +1,481 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.sandbox.v1; + +import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; +import "datamodel.proto"; + +// Sandbox-supervisor configuration and policy messages. +// +// Conventions: +// - This file owns messages exchanged between the gateway and the sandbox +// supervisor/runtime. +// - Public sandbox resource types live in `openshell.proto`. +// - Internal compute-driver sandbox observation types live in `compute_driver.proto`. + +// Sandbox security policy configuration. +message SandboxPolicy { + // Policy version. + uint32 version = 1; + // Filesystem access policy. + FilesystemPolicy filesystem = 2; + // Landlock configuration. + LandlockPolicy landlock = 3; + // Process execution policy. + ProcessPolicy process = 4; + // Network access policies keyed by name (e.g. "claude_code", "gitlab"). + map network_policies = 5; + // Reusable supervisor middleware configs for network egress, keyed by their + // policy-local names. At most 10 configs are accepted, and at most 10 stages + // can be selected per request. + map network_middlewares = 6; +} + +// Filesystem access policy. +message FilesystemPolicy { + // Automatically include the workdir as read-write. + bool include_workdir = 1; + // Read-only directory allow list. + repeated string read_only = 2; + // Read-write directory allow list. + repeated string read_write = 3; +} + +// Landlock policy configuration. +message LandlockPolicy { + // Compatibility mode (e.g. "best_effort", "hard_requirement"). + string compatibility = 1; +} + +// Process execution policy. +message ProcessPolicy { + // User name to run the sandboxed process as. + string run_as_user = 1; + // Group name to run the sandboxed process as. + string run_as_group = 2; +} + +// A named network access policy rule. +message NetworkPolicyRule { + // Human-readable name for this policy rule. + string name = 1; + // Allowed endpoint (host:port) pairs. + repeated NetworkEndpoint endpoints = 2; + // Allowed binary identities. + repeated NetworkBinary binaries = 3; +} + +// A reusable middleware config selected for admitted egress by host. +message NetworkMiddlewareConfig { + // Human-readable name for this middleware config. + string name = 1; + // Built-in middleware name or operator-owned registration name. + string middleware = 2; + // Service-specific configuration. + google.protobuf.Struct config = 3; + // Failure behavior: "fail_closed" (default) or "fail_open". + string on_error = 4; + // Host selector controlling which admitted destinations use this config. + MiddlewareEndpointSelector endpoints = 5; + // Execution order. Values must be unique within a policy; lower values run first. + int32 order = 6; +} + +// Host selector controlling which admitted destinations use a middleware config. +message MiddlewareEndpointSelector { + // Exact host or DNS glob patterns included in the selection. Include and + // exclude accept at most 32 combined patterns. + repeated string include = 1; + // Exact host or DNS glob patterns removed from the selection. + // Exclusions take precedence over inclusions. + repeated string exclude = 2; +} + +// Binds a policy endpoint to static credentials from a sandbox provider. +message NetworkCredentialBinding { + // Name of the provider attached to this sandbox whose static credentials + // may be resolved for requests admitted by this endpoint. + string provider = 1; +} + +// TLS handling for a network endpoint. +enum NetworkTlsMode { + // Auto-detect TLS and terminate it when inspection is configured. + NETWORK_TLS_MODE_UNSPECIFIED = 0; + // Disable TLS detection and relay the connection without inspection. + NETWORK_TLS_MODE_SKIP = 1; + // Rejected by policy validation; use NETWORK_TLS_MODE_UNSPECIFIED. + NETWORK_TLS_MODE_TERMINATE = 2 [deprecated = true]; + // Rejected by policy validation; use NETWORK_TLS_MODE_UNSPECIFIED. + NETWORK_TLS_MODE_PASSTHROUGH = 3 [deprecated = true]; +} + +// Enforcement behavior for inspected network traffic. +enum NetworkEnforcementMode { + // Preserve the existing default: log violations and allow traffic. + NETWORK_ENFORCEMENT_MODE_UNSPECIFIED = 0; + // Deny traffic that does not satisfy the endpoint policy. + NETWORK_ENFORCEMENT_MODE_ENFORCE = 1; + // Log policy violations and allow the traffic. + NETWORK_ENFORCEMENT_MODE_AUDIT = 2; +} + +// Shorthand access policy for an inspected network endpoint. +enum NetworkAccessPreset { + // No access preset; explicit rules may define allowed traffic. + NETWORK_ACCESS_PRESET_UNSPECIFIED = 0; + // Allow read-only operations for the selected protocol. + NETWORK_ACCESS_PRESET_READ_ONLY = 1; + // Allow read and write operations for the selected protocol. + NETWORK_ACCESS_PRESET_READ_WRITE = 2; + // Allow every operation supported by the selected protocol. + NETWORK_ACCESS_PRESET_FULL = 3; +} + +// A network endpoint (host + port) with optional L7 inspection config. +message NetworkEndpoint { + // Hostname or host glob pattern. Exact match is case-insensitive. + // Glob patterns use "." as delimiter: "*.example.com" matches a single + // subdomain label, "**.example.com" matches across labels. + string host = 1; + // Single port (backwards compat). Use `ports` for multiple ports. + // Mutually exclusive with `ports` — if both are set, `ports` takes precedence. + uint32 port = 2; + // Endpoint protocol. "tcp" and "" select L4-only handling; "rest", + // "websocket", "graphql", "sql", "json-rpc", and "mcp" select L7 inspection. + string protocol = 3; + // TLS handling. Unspecified enables automatic detection and termination. + NetworkTlsMode tls = 4; + // Enforcement mode. Unspecified preserves the audit default. + NetworkEnforcementMode enforcement = 5; + // Access preset shorthand. Unspecified means no preset. + // Mutually exclusive with rules. + NetworkAccessPreset access = 6; + // Explicit L7 rules (mutually exclusive with access). + repeated L7Rule rules = 7; + // Allowed resolved IP addresses or CIDR ranges for this endpoint. + // When non-empty, the SSRF internal-IP check is replaced by an allowlist check: + // - If host is also set: domain must resolve to an IP in this list. + // - If host is empty: any domain is allowed as long as it resolves to an IP in this list. + // Supports exact IPs ("10.0.5.20") and CIDR notation ("10.0.5.0/24"). + // Loopback (127.0.0.0/8) and link-local (169.254.0.0/16) are always blocked + // regardless of this field. + repeated string allowed_ips = 8; + // Multiple ports. When non-empty, this endpoint covers all listed ports. + // If `port` is set and `ports` is empty, `port` is normalized to `ports: [port]`. + // If both are set, `ports` takes precedence. + repeated uint32 ports = 9; + // Explicit L7 deny rules. When present, requests matching any deny rule + // are blocked even if they match an allow rule or access preset. + // Deny rules take precedence over allow rules. + repeated L7DenyRule deny_rules = 10; + // When true, percent-encoded '/' (%2F) is preserved in path segments + // rather than rejected by the L7 path canonicalizer. Required for + // upstreams like GitLab that embed %2F in namespaced resource paths. + // Defaults to false (strict). + bool allow_encoded_slash = 11; + // GraphQL persisted-query behavior for hash-only/saved-query requests: + // "deny" (default) or "allow_registered". + string persisted_queries = 12; + // Trusted GraphQL persisted-query registry keyed by hash or service-specific ID. + // Only used when persisted_queries is "allow_registered". + map graphql_persisted_queries = 13; + // Maximum GraphQL request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + uint32 graphql_max_body_bytes = 14; + // Optional HTTP path glob that scopes this L7 endpoint on shared host:port APIs. + // Example: use path "/graphql" for protocol "graphql" and "/repos/**" for + // protocol "rest" when both surfaces live under api.example.com:443. + // Empty means all paths. + string path = 15; + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside client-to-server WebSocket text messages after an allowed HTTP 101 + // upgrade. Defaults to false. + bool websocket_credential_rewrite = 16; + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside supported textual HTTP request bodies before forwarding upstream. + // Defaults to false. + bool request_body_credential_rewrite = 17; + // Internal provenance marker for policy-advisor generated endpoints. + // Advisor-proposed endpoints must not satisfy exact-host SSRF trust unless + // they are converted through an explicit user-authored policy path. + bool advisor_proposed = 18; + // Proxy-side credential signing mode: "sigv4" for AWS SigV4 re-signing. + // When set, the proxy strips the client's Authorization header and computes + // a fresh SigV4 signature using real credentials from the provider. + string credential_signing = 19; + // AWS signing service name override. Required when credential_signing is + // "sigv4" — e.g. "bedrock" for bedrock-runtime endpoints. + string signing_service = 20; + // AWS region override for SigV4 signing. When set, takes precedence over + // hostname-based region extraction. Required for non-standard endpoints. + string signing_region = 21; + // Maximum JSON-RPC-over-HTTP request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + uint32 json_rpc_max_body_bytes = 22; + // MCP-only policy and inspection options. Only used when protocol is "mcp". + McpOptions mcp = 23; + // Explicit binding authority for static credentials from an attached + // endpointless provider profile. Profiles that already define endpoints + // continue to use those profile endpoints as their credential boundary. + NetworkCredentialBinding credential_binding = 24; + // Explicitly permits credential-bearing traffic to use paths that OpenShell + // cannot inspect or rewrite. Defaults to false. This is a security-sensitive + // escape hatch and must be explicitly approved. + bool allow_uninspected_credentials = 25; + // Internal gateway-derived marker indicating that this endpoint belongs to + // an attached credentialed provider. User-authored values are ignored. + bool provider_credentialed = 26; + // Internal gateway-derived identity of this endpoint's token-grant authority. + // It binds the provider record and complete profile policy, or the active + // global policy, to this endpoint. User-authored values are ignored. + string token_grant_owner = 27; +} + +// MCP options are grouped so MCP-specific policy can grow without adding more +// top-level NetworkEndpoint fields. OpenShell owns the supported revision +// profiles instead of treating dependency enums as the policy contract. +// +// Sources: +// - https://modelcontextprotocol.io/specification/2025-03-26/basic/transports +// - https://modelcontextprotocol.io/specification/2025-06-18/basic/transports +// - https://modelcontextprotocol.io/specification/2025-11-25/basic/transports +message McpOptions { + // Hardening boundary for tools/call params.name. When unset or true, the + // supervisor enforces the MCP recommended tool-name syntax + // ^[A-Za-z0-9_.-]{1,128}$ before policy evaluation. Set false only for + // compatibility with servers that intentionally use non-recommended names. + // + // Source: + // - https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names + optional bool strict_tool_names = 1; + // Method-layer default for MCP endpoints. When true, OpenShell allows parsed + // MCP-family methods at the method layer unless a tool-name policy narrows + // tools/call. When unset or false, explicit method rules are required. + optional bool allow_all_known_mcp_methods = 2; + // Exact MCP protocol revisions accepted by the endpoint's policy schema. + // Authors may omit this field. Because proto3 repeated fields do not retain + // presence, omission reaches protobuf ingress as an empty list. Checked + // normalization materializes OpenShell's pinned default revision + // "2025-11-25" before producing canonical downstream state, which is always + // nonempty. + // + // An explicit nonempty list remains an exact allowlist. Duplicates, moving + // aliases such as "draft" and "latest", and unknown dates are invalid. + // Normalization stores valid values in canonical semantic order. These + // values identify core revisions; they do not enable separate SEP overlays + // or select runtime parsing and forwarding behavior. + repeated string versions = 3; +} + +// Trusted GraphQL operation classification. +message GraphqlOperation { + // Operation type: "query", "mutation", or "subscription". + string operation_type = 1; + // Operation name, if known. + string operation_name = 2; + // Root field names selected by the operation. + repeated string fields = 3; +} + +// An L7 deny rule that blocks specific requests. +// Mirrors L7Allow — same fields, same matching semantics, inverted effect. +// Deny rules are evaluated after allow rules and take precedence. +message L7DenyRule { + // Protocol method: HTTP method (REST/WebSocket), JSON-RPC method name, or + // "*" for any when supported by the protocol. + string method = 1; + // URL path glob pattern (REST): "/repos/*/pulls/*/reviews", "**" for any. + string path = 2; + // SQL command (SQL): SELECT, INSERT, etc. or "*" for any. + string command = 3; + // Query parameter matcher map (REST). + // Same semantics as L7Allow.query. + map query = 4; + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + string operation_type = 5; + // GraphQL operation name glob. "*" matches any operation name. + string operation_name = 6; + // GraphQL root field globs. Deny rules match when any selected root field + // matches any configured glob. + repeated string fields = 7; + reserved 8; + // MCP params matcher map. Currently only params.name is supported for + // tools/call filtering. Generic protocol "json-rpc" rejects params matchers. + map params = 9; +} + +// An L7 policy rule (allow-only). +message L7Rule { + L7Allow allow = 1; +} + +// Allowed action definition for L7 rules. +message L7Allow { + // Protocol method: HTTP method (REST/WebSocket), JSON-RPC method name, or + // "*" for any when supported by the protocol. + string method = 1; + // URL path glob pattern (REST): "/repos/**", "**" for any. + string path = 2; + // SQL command (SQL): SELECT, INSERT, etc. or "*" for any. + string command = 3; + // Query parameter matcher map (REST). + // Key is the decoded query parameter name (case-sensitive). + // Value supports either a single glob (`glob`) or a list (`any`). + map query = 4; + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + string operation_type = 5; + // GraphQL operation name glob. "*" matches any operation name. + string operation_name = 6; + // GraphQL root field globs. Allow rules match only when every selected root + // field matches one of the configured globs. Omit to match all fields. + repeated string fields = 7; + reserved 8; + // MCP params matcher map. Currently only params.name is supported for + // tools/call filtering. Generic protocol "json-rpc" rejects params matchers. + map params = 9; +} + +// Query value matcher for one query parameter key. +message L7QueryMatcher { + // Single glob pattern. + string glob = 1; + // Any-of glob patterns. + repeated string any = 2; +} + +// A binary identity for network policy matching. +message NetworkBinary { + string path = 1; + reserved 2; + reserved "harness"; +} + +// Request to get sandbox settings by sandbox name. +message GetSandboxConfigRequest { + // Workspace scope. Select one named workspace; authenticated sandbox callers may omit it during bootstrap. + openshell.datamodel.v1.WorkspaceSelector workspace_scope = 3; + string name = 1; +} + +// Request to get gateway-global settings. +message GetGatewayConfigRequest {} + +// Response containing gateway-global settings. +message GetGatewayConfigResponse { + // Gateway-global settings map excluding the reserved policy key. + // Registered keys without a configured value are returned with an empty SettingValue. + map settings = 1; + // Monotonically increasing revision for gateway-global settings. + uint64 settings_revision = 2; +} + +// Scope that currently controls a setting. +enum SettingScope { + SETTING_SCOPE_UNSPECIFIED = 0; + SETTING_SCOPE_SANDBOX = 1; + SETTING_SCOPE_GLOBAL = 2; +} + +// Type-aware setting value for sandbox/gateway settings. +message SettingValue { + oneof value { + string string_value = 1; + bool bool_value = 2; + int64 int_value = 3; + bytes bytes_value = 4; + } +} + +// Effective setting value and the scope it was resolved from. +message EffectiveSetting { + SettingValue value = 1; + SettingScope scope = 2; +} + +// Source used for the policy payload in GetSandboxConfigResponse. +enum PolicySource { + POLICY_SOURCE_UNSPECIFIED = 0; + POLICY_SOURCE_SANDBOX = 1; + POLICY_SOURCE_GLOBAL = 2; +} + +// Response containing effective sandbox settings and policy. +message GetSandboxConfigResponse { + // The sandbox policy configuration. + SandboxPolicy policy = 1; + // Current policy version (monotonically increasing per sandbox). + uint32 version = 2; + // SHA-256 hash of the serialized policy payload. + string policy_hash = 3; + // Effective settings resolved for this sandbox, excluding the reserved policy key. + // Registered keys without a configured value are returned with an empty EffectiveSetting.value. + map settings = 4; + // Fingerprint for effective config (policy + settings). Changes when any effective input changes. + uint64 config_revision = 5; + // Source of the policy payload for this response. + PolicySource policy_source = 6; + // When policy_source is GLOBAL, the version of the global policy revision. + // Zero when no global policy is active or when policy_source is SANDBOX. + uint32 global_policy_version = 7; + // Fingerprint for provider credential inputs attached to this sandbox. + // Changes when attached provider names or attached provider records change. + uint64 provider_env_revision = 8; + // Operator-registered supervisor middleware services required by the + // effective policy. Built-in middleware is not included. + repeated SupervisorMiddlewareService supervisor_middleware_services = 9; + // Workspace the sandbox belongs to. Allows the supervisor to learn its + // workspace context for subsequent workspace-scoped RPCs. + string workspace = 10; + // Gateway-configured posture for rejected policy generations. Valid values + // are "fail_closed" and "retain_last_valid". Unknown or empty values must + // be treated as fail_closed by the supervisor. + string policy_validation_failure_mode = 11; + // Whether this gateway can mint authenticated extension credentials. + // False also covers older gateways that do not advertise this capability; + // supervisors preserve their legacy unauthenticated connection behavior. + bool extension_authentication_enabled = 12; + // Gateway-owned attachment identity captured with this desired configuration. + // Compare for equality; reattachment invalidates previous installation evidence. + string provider_attachment_epoch = 14; + // True only after validating this complete policy/provider composition. + // Missing (older gateway) is deliberately not admission. + bool configuration_admitted = 13; + // Bounded, credential-free admission diagnostic. Empty for admitted policy. + string configuration_error = 16; + // Registration fence for a new supervisor; capture once and retain on retry. + string configuration_instance_id = 15; +} + +// Connection details for one operator-registered supervisor middleware service. +// V1 supports plaintext and server-authenticated TLS gRPC. +message SupervisorMiddlewareService { + reserved 4; + reserved "timeout"; + // Operator-owned registration name used by policy attachments and diagnostics. + string name = 1; + // gRPC endpoint reachable from the sandbox supervisor. + string grpc_endpoint = 2; + // Operator-owned logical payload limit applied to every binding exposed by + // the service. This caps HTTP bodies and complete WebSocket messages. + uint64 max_payload_bytes = 3; + // Default RPC timeout for this service. Absence uses the platform default of + // 500ms. Values must be between 10ms and 30s. + google.protobuf.Duration request_timeout = 104; + // PEM-encoded trust roots loaded by the gateway from the operator-configured + // tls_ca_cert_path. Empty uses the platform trust store. + bytes tls_ca_cert_pem = 5; + // Exact JWT audience for this service. The gateway resolves an omitted + // operator value to a kind-scoped audience derived from the registration + // name before sending sandbox config. + string audience = 6; + // Operator opt-out from extension authentication for this registration. + // When true the service may use a plaintext endpoint and OpenShell attaches + // no bearer credential; supervisors must not request one. Intended only for + // trusted-network development deployments. + bool allow_insecure_transport = 7; +} diff --git a/third_party/openshell/proto/supervisor_middleware.proto b/third_party/openshell/proto/supervisor_middleware.proto new file mode 100644 index 0000000..6c0cc94 --- /dev/null +++ b/third_party/openshell/proto/supervisor_middleware.proto @@ -0,0 +1,722 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package openshell.middleware.v1; + +import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; +import "extension.proto"; + +// SupervisorMiddleware discovers and configures one operator-run middleware. +// It evaluates HTTP requests and WebSocket messages before credentials. +// Phase-specific services share the same registration. +service SupervisorMiddleware { + // Describe returns the service manifest and declared bindings. + rpc Describe(MiddlewareDescribeRequest) returns (MiddlewareManifest); + + // ValidateConfig checks service-specific configuration for one binding. + rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse); + + // EvaluateHttpRequest returns an allow, deny, or mutation decision for one + // buffered HTTP request. + rpc EvaluateHttpRequest(HttpRequestEvaluation) returns (HttpRequestResult); + + // EvaluateWebSocketSession opens one ordered, phase-specific stream for a + // single middleware stage and WebSocket upgrade attempt. The current + // implementation supports client-to-upstream text messages at + // PRE_CREDENTIALS; PRE_RETURN is reserved for upstream-to-client messages. + // A request may go unanswered when the session terminates. For every opened + // stage stream, OpenShell attempts at most one session_end before closing the + // stream when its transport is still writable. + rpc EvaluateWebSocketSession(stream WebSocketSessionEvent) + returns (stream WebSocketSessionEventResult); +} + +message MiddlewareDescribeRequest { + // Gateway or supervisor protocol metadata. Middleware must reject unmet + // requirements before accepting traffic. + openshell.extension.v1.PeerMetadata gateway = 1; +} + +// HttpResponsePreReturn evaluates one response for one middleware stage before +// OpenShell returns it to the sandbox. +service HttpResponsePreReturn { + // Evaluate starts with preflight and may continue with selected body units + // and trailers. A body unit marked end_of_stream ends body inspection, not + // the event stream. Trailers and one best-effort session_end may follow. + rpc Evaluate(stream HttpResponseEvent) + returns (stream HttpResponseEventResult); +} + +// MiddlewareManifest describes one middleware service and its bindings. +message MiddlewareManifest { + // Human-readable middleware service name used only for diagnostics. This is + // not required to match an operator-owned registration name. + string name = 1; + // Deprecated diagnostic compatibility field. Use + // extension.implementation_version. + string service_version = 2; + // Bindings exposed by this middleware service. + repeated MiddlewareBinding bindings = 3; + // Exact JWT audience this service verifies on inbound OpenShell calls. + // After authenticated Describe succeeds, OpenShell rejects the registration + // unless this matches the operator-configured audience. A strict verifier may + // reject an incorrect audience before returning this manifest. Empty skips + // this post-authentication consistency check. + string expected_audience = 4; + // Supervisor-middleware protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.supervisor-middleware.contract" + // in supported_capabilities. + openshell.extension.v1.PeerMetadata extension = 5; +} + +// MiddlewareBinding declares one operation and phase supported by a service. +message MiddlewareBinding { + reserved 4; + reserved "timeout"; + // Supported operation. + SupervisorMiddlewareOperation operation = 1; + // Supported phase. + SupervisorMiddlewarePhase phase = 2; + // Maximum request body, WebSocket message, or response body unit/replacement. + // Required for payload-bearing operations. + uint64 max_payload_bytes = 3; + // Optional binding-specific RPC timeout. Empty uses the operator-configured + // service timeout, or the 500ms platform default when that is also omitted. + // A non-empty value may shorten but cannot extend the operator timeout. + // Values must be between 10ms and 30s. + google.protobuf.Duration request_timeout = 104; +} + +// ValidateConfigRequest contains one policy configuration to validate. +message ValidateConfigRequest { + // Service-specific policy configuration. + google.protobuf.Struct config = 1; + // Built-in middleware name or operator-owned registration name. + string middleware_name = 2; +} + +// ValidateConfigResponse reports whether a policy configuration is accepted. +message ValidateConfigResponse { + // True when the service accepts the configuration. + bool valid = 1; + // Human-readable validation failure reason. Empty when valid is true. + string reason = 2; +} + +// HttpRequestEvaluation contains one buffered HTTP request to evaluate. +message HttpRequestEvaluation { + // Evaluation phase selected for this request. + SupervisorMiddlewarePhase phase = 1; + // Sandbox and request identity available to the supervisor. + // The encoded context is limited to 4 KiB. + RequestContext context = 2; + // Validated service-specific policy configuration. + // The encoded configuration is limited to 64 KiB. + google.protobuf.Struct config = 3; + // Destination and HTTP request target. + // The encoded target is limited to 32 KiB. + HttpRequestTarget target = 4; + // HTTP request headers before OpenShell injects credentials, in wire + // order. Repeated header names are preserved as separate entries. Protected + // credential, routing, framing, and hop-by-hop headers are omitted. + // At most 128 lines and 64 KiB of encoded headers are included. + repeated HttpHeader headers = 5; + // Buffered request body, limited to 4 MiB. Empty for a bodyless request. + bytes body = 6; + // Built-in middleware name or operator-owned registration name. + string middleware_name = 7; +} + +// HttpHeader is one HTTP header line. +message HttpHeader { + // Lowercased header name. + string name = 1; + // Header value with surrounding whitespace trimmed. + string value = 2; +} + +// One ordered response event. A stream starts with preflight, may continue with +// body units ending in end_of_stream, may then include trailers, and may end +// with one best-effort session_end. +message HttpResponseEvent { + oneof event { + // Initial response head and request context. + HttpResponsePreflight preflight = 1; + // Next normalized body unit. + HttpResponseBodyUnit body = 2; + // Normalized trailers after the final body result. + HttpResponseTrailers trailers = 4; + // Optional terminal notification. + MiddlewareSessionEnd session_end = 3; + } +} + +// Each preflight, body, and trailers event requires one ordered result. +// session_end has no result. +message HttpResponseEventResult { + oneof result { + // Result for preflight. + HttpResponsePreflightResult preflight_result = 1; + // Result for the next body unit. + HttpResponseBodyResult body_result = 2; + // Result for response trailers. + HttpResponseTrailersResult trailers_result = 3; + } +} + +// HttpResponsePreflight exposes the current final response head to one stage. +message HttpResponsePreflight { + // Request identity. request_id links request and response evaluations. + // Limited to 4 KiB encoded. + RequestContext context = 1; + // Admitted request target with a redacted query. Limited to 32 KiB encoded. + HttpRequestTarget target = 2; + // Final non-informational upstream status. Upgrades are not evaluated. + uint32 status_code = 3; + // Response headers after prior stages, in wire order. Repeated names remain + // separate. Credential, routing, and hop-by-hop headers are omitted. + // Content-Length, Content-Encoding, and Content-Range retain their read-only + // upstream values. OpenShell may recompute or remove Content-Length later. + // Limited to 128 lines and 64 KiB encoded. + repeated HttpHeader headers = 4; + // Built-in middleware name or operator-owned registration name. + string middleware_name = 5; + // Validated service configuration. Limited to 64 KiB encoded. + google.protobuf.Struct config = 6; + // Effective minimum of platform, registration, and binding limits. Applies to + // whole-body input/replacement and each stream input/replacement. Stream + // inputs use at most min(64 KiB, max_payload_bytes). + uint64 max_payload_bytes = 7; + // Modes derived independently for this stage. OpenShell first determines + // response-shape eligibility from the original final response head, then + // applies this stage's effective max_payload_bytes. Different stages may + // receive different lists. HEADERS_ONLY is always present and is the only + // mode for bodyless, partial, encoded, or no-transform responses. For an + // otherwise eligible response, a known body larger than this stage's limit + // omits WHOLE_BODY_BYTES. An eligible unknown-length response may select + // WHOLE_BODY_BYTES and later fail with whole_body_over_capacity according to + // this stage's on_error. STREAM_BYTES is omitted when + // max_payload_bytes is zero. Selecting an unlisted mode fails according to + // on_error. + repeated HttpResponseBodyMode permitted_body_modes = 8; +} + +// Selects skip, inspect, or block. Diagnostic fields apply to every action. +// Invalid diagnostics make the entire result a middleware failure handled +// according to on_error. +message HttpResponsePreflightResult { + oneof action { + // Deliver unchanged without invoking on_error. + HttpResponsePreflightSkip skip = 1; + // Inspect with the selected body mode and mutations. + HttpResponsePreflightInspect inspect = 2; + // Prevent delivery to the sandbox. + HttpResponseBlockDelivery block_delivery = 7; + } + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + string reason = 3; + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. Returned to the sandbox only for block_delivery. + string reason_code = 4; + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + repeated Finding findings = 5; + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + map metadata = 6; +} + +// Ends this stage successfully without body inspection. +message HttpResponsePreflightSkip {} + +// Blocks delivery as a successful decision regardless of on_error. OpenShell +// evaluates results in policy order. Once it accepts a valid block, it stops +// later middleware evaluation and ends every still-writable opened stage with +// MIDDLEWARE_DENIAL. A failure handled earlier may already have stopped +// evaluation, so a later block does not override it. An invalid block result +// is a middleware failure handled according to on_error. The upstream request +// has already run; blocking its response does not reject or roll back that +// request. +// +// Before response commitment, including at preflight and during +// WHOLE_BODY_BYTES, OpenShell replaces the upstream response with the canonical +// 403 Forbidden middleware-denial response. Its JSON body has +// error = "middleware_denied" and includes a validated reason_code when the +// result supplies one. OpenShell never returns the free-form reason or writes it +// to security logs. For HEAD, OpenShell sends the canonical response headers +// and Content-Length but no body. It closes the downstream connection after the +// denial response. +// +// After response commitment, including during STREAM_BYTES, OpenShell aborts +// downstream delivery. It does not inject an error body, a terminating chunk, +// or an error trailer. OpenShell does not reuse the upstream connection. +message HttpResponseBlockDelivery {} + +// Selects body inspection and response-header mutations. +message HttpResponsePreflightInspect { + // Required mode from permitted_body_modes. Invalid values fail according to + // on_error. + HttpResponseBodyMode body_mode = 1; + // Ordered mutations applied atomically before the next stage. Only visible + // end-to-end headers may change. Routing, credential, framing, coding, range, + // and hop-by-hop headers are protected; integrity headers may only be removed. + // Limited to 64 operations, 32 KiB of name/value data, and 64 KiB encoded. + repeated HeaderMutation header_mutations = 2; +} + +// Controls which response-body units a stage receives. +enum HttpResponseBodyMode { + // Invalid value handled according to on_error. + HTTP_RESPONSE_BODY_MODE_UNSPECIFIED = 0; + // Inspect only the response head. + HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY = 1; + // Buffer the normalized body as one final unit before committing the head. + // Input and replacement must fit max_payload_bytes. Capacity failures use + // whole_body_over_capacity and follow this stage's on_error. + HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES = 2; + // Receive normalized units ending with end_of_stream. Each input is at most + // min(64 KiB, max_payload_bytes), and each replacement must fit + // max_payload_bytes. Each result fully accounts for its input unit; V1 does + // not permit retaining input across units. The full body may exceed the + // limit. STREAM_BYTES has no total response-lifetime deadline. + HTTP_RESPONSE_BODY_MODE_STREAM_BYTES = 3; +} + +// One normalized body unit. Boundaries have no transport or application +// meaning. +message HttpResponseBodyUnit { + // Contiguous and stage-local, starting at 1. + uint64 sequence = 1; + oneof payload { + // Bytes without transfer framing. A body-capable response with no body bytes + // has present empty data in sequence 1. STREAM_BYTES input size is at most + // min(64 KiB, max_payload_bytes). A unit may be shorter to preserve + // flushing. + bytes data = 2; + } + // Marks the final body unit. Every normally completed body inspection receives + // exactly one. For a body-capable response with no body bytes, this is the + // empty sequence-1 unit. OpenShell does not read ahead, so it may send an empty + // final unit after the last nonempty unit. Trailers and session_end may + // follow. A stage ended by skip_remaining, block, or failure receives no + // later final unit. + bool end_of_stream = 3; +} + +// Result for one body unit. Units are processed in lockstep; V1 does not +// support ownership transfer or cross-unit retention. Diagnostic fields apply +// to every action. Invalid diagnostics make the entire result a middleware +// failure handled according to on_error. OpenShell retains the current input +// until it validates the result, so fail-open can continue from the last input +// OpenShell still owns. +message HttpResponseBodyResult { + // Must match the next unit. Zero, gaps, duplicates, and regressions fail. + uint64 sequence = 1; + // Exactly one explicit action is required. + oneof action { + // Forward the input unit unchanged. + HttpResponseBodyPassThrough pass_through = 2; + // Replace the complete input unit. + HttpResponseBodyTransform transform = 3; + // Stop delivery. See HttpResponseBlockDelivery. + HttpResponseBlockDelivery block_delivery = 8; + // Finalize this unit and stop inspecting. + HttpResponseBodySkipRemaining skip_remaining = 9; + } + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + string reason = 4; + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. When OpenShell accepts block_delivery before response + // commitment, it includes this code in the canonical denial response. It is + // never returned after commitment. + string reason_code = 5; + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + repeated Finding findings = 6; + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + map metadata = 7; +} + +// Preserves the input unit. +message HttpResponseBodyPassThrough {} + +// Finalizes this unit and ends the stage. This stage receives no later body or +// trailer events. The current and later units continue through other stages. +// For WHOLE_BODY_BYTES, this equals its nested action. +message HttpResponseBodySkipRemaining { + // Exactly one action for the current unit. + oneof current { + // Forward the current unit unchanged. + HttpResponseBodyPassThrough pass_through = 1; + // Replace the current unit. + HttpResponseBodyTransform transform = 2; + } +} + +// Replaces the complete input unit. +message HttpResponseBodyTransform { + // Required replacement, limited to max_payload_bytes. Present empty data + // deletes the input unit. The replacement fully accounts for this input unit; + // middleware must not retain input bytes for a later unit in V1. + oneof replacement { + // Normalized replacement bytes. + bytes data = 1; + } +} + +// The current normalized response trailers in wire order. Repeated names stay +// as separate fields. A stage that completes WHOLE_BODY_BYTES or STREAM_BYTES +// receives exactly one trailers event after its final body result, including +// when this set is empty. SKIP, HEADERS_ONLY, semantically bodyless responses, +// and stages ended by block, failure, or skip_remaining receive no trailers. +message HttpResponseTrailers { + repeated HttpHeader headers = 1; +} + +// Applies ordered trailer mutations atomically. An empty mutation list +// preserves the current trailers. A write may target only a case-insensitive +// name present in the trailers event; V1 cannot create a trailer name. Removal +// of an absent name is a no-op. Credential, routing, framing, coding, range, +// hop-by-hop, and connection-nominated fields are protected. Diagnostic fields +// apply whether mutations are empty or nonempty. Invalid diagnostics or +// mutations make the entire result a middleware failure handled according to +// on_error. +message HttpResponseTrailersResult { + // At most 64 operations, 32 KiB of validated name/value data, and 64 KiB + // encoded are accepted. + repeated HeaderMutation trailer_mutations = 1; + // Service diagnostic, never sent to the sandbox or security logs. Maximum + // 4 KiB. + string reason = 2; + // Optional audit code using the HttpRequestResult.reason_code format and + // 64-byte maximum. Never sent to the sandbox. + string reason_code = 3; + // Up to 32 audit-safe findings, each limited to 4 KiB encoded. + repeated Finding findings = 4; + // Non-secret diagnostic metadata, limited to 64 entries and 32 KiB. + map metadata = 5; +} + +// Stable reason OpenShell ended a middleware stage stream. +enum MiddlewareSessionEndReason { + // Invalid reason. + MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED = 0; + // Evaluation completed. + MIDDLEWARE_SESSION_END_REASON_NORMAL = 1; + // The sandbox peer disconnected. + MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT = 2; + // A policy reload replaced the active middleware chain. + MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD = 3; + // A stage denied the operation or blocked the response. + MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL = 4; + // A selected stage failed. + MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE = 5; + // A proxied or middleware protocol was violated. + MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR = 6; + // Evaluation was canceled for another reason. + MIDDLEWARE_SESSION_END_REASON_CANCELLATION = 7; + // Upstream rejected or failed before a valid response or upgrade. + MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE = 8; + // Network policy denied the operation. + MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL = 9; + // The stage successfully declined inspection during preflight. + MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED = 10; + // Upstream disconnected after a valid response or upgrade. + MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT = 11; +} + +// Best-effort terminal notification. A stage receives at most one and sends no +// result. +message MiddlewareSessionEnd { + // Terminal reason. Producers never send UNSPECIFIED. + MiddlewareSessionEndReason reason = 1; + // Set only for PROTOCOL_ERROR. Missing or unknown details mean a generic + // protocol error. + MiddlewareSessionProtocolError protocol_error = 2; +} + +// Details for a protocol-error session end. +message MiddlewareSessionProtocolError { + oneof domain { + // WebSocket protocol violation. + WebSocketProtocolError web_socket = 1; + // Middleware event/result protocol violation. + MiddlewareExchangeProtocolError middleware_exchange = 2; + } +} + +// WebSocket protocol error details, reserved for future categories. +message WebSocketProtocolError {} + +// Middleware exchange error details, reserved for future categories. +message MiddlewareExchangeProtocolError {} + +// Supervisor operation selected for middleware evaluation. +enum SupervisorMiddlewareOperation { + SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED = 0; + SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST = 1; + SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE = 2; + SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE = 3; +} + +// Ordered phase within a supervisor operation. +enum SupervisorMiddlewarePhase { + SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED = 0; + SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS = 1; + SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN = 2; +} + +// WebSocketSessionEvent is one ordered event in a stage-local stream. +// Message sequence numbers identify logical messages session-wide. A stage +// receives a strictly increasing subset of those numbers; gaps are valid when +// session messages are not delivered to that stage. +message WebSocketSessionEvent { + oneof event { + WebSocketPreflight preflight = 1; + WebSocketSessionStart session_start = 2; + WebSocketMessage message = 3; + MiddlewareSessionEnd session_end = 4; + } +} + +// WebSocketPreflight lets a service decline this upgrade before OpenShell +// contacts upstream. It deliberately excludes query data, arbitrary request +// headers, and message payloads. +message WebSocketPreflight { + string session_id = 1; + SupervisorMiddlewarePhase phase = 2; + RequestContext context = 3; + // Admitted HTTP WebSocket-upgrade target. The method is GET, query is always + // empty, and path never includes a query string. + HttpRequestTarget target = 4; + repeated string requested_subprotocols = 5; + // Built-in middleware name or operator-owned registration name. + string middleware_name = 6; + google.protobuf.Struct config = 7; +} + +// WebSocketSessionStart reports bounded metadata known only after the +// upstream 101 response validates. Empty selected_subprotocol means none. +message WebSocketSessionStart { + string selected_subprotocol = 1; +} + +// WebSocketMessage contains one complete reconstructed logical message. +message WebSocketMessage { + // Session-global sequence starting at 1. Values delivered to one stage must + // strictly increase but need not be contiguous. Reject zero, duplicates, and + // regressions; accept gaps. + uint64 sequence = 1; + // One complete logical payload. Protobuf string decoding enforces UTF-8 for + // text messages. Raw frame mechanics are never exposed. Limited to 4 MiB by + // the platform and the binding-specific cap. + oneof payload { + string text = 2; + bytes binary = 3; + } +} + +// WebSocketPreflightAction is the service's one-time scoping decision. +enum WebSocketPreflightAction { + // Invalid response value handled according to the policy failure mode. + WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED = 0; + // Inspect this session after the upstream accepts the upgrade. + WEB_SOCKET_PREFLIGHT_ACTION_INSPECT = 1; + // Voluntarily decline inspection without denying the upgrade. This is a + // successful decision and does not engage on_error. + WEB_SOCKET_PREFLIGHT_ACTION_SKIP = 2; + // Authoritatively deny the upgrade before upstream contact. This is a + // successful decision and is enforced regardless of on_error. + WEB_SOCKET_PREFLIGHT_ACTION_DENY = 3; +} + +message WebSocketPreflightDecision { + WebSocketPreflightAction action = 1; + // Free-form service diagnostic. OpenShell never exposes this to the + // workload or security logs. Limited to 4 KiB before discarding. + string reason = 2; + // Optional stable machine-readable code for a deny decision. Because + // preflight runs before the HTTP upgrade completes, OpenShell may return + // this code to the requester. Codes follow the same format and 64-byte + // maximum as HttpRequestResult.reason_code. + string reason_code = 3; + // Audit-safe findings produced during preflight. At most 32 findings of at + // most 4 KiB encoded each are accepted. + repeated Finding findings = 4; + // Non-secret service-defined metadata included in diagnostics. At most 64 + // entries and 32 KiB of combined key/value data are accepted. + map metadata = 5; +} + +// WebSocketMessageResult contains the decision and optional replacement for +// one message. A replacement must use the same variant as the input payload. +message WebSocketMessageResult { + // Must exactly match the sequence of the corresponding WebSocketMessage. + uint64 sequence = 1; + Decision decision = 2; + // Absence preserves the input unchanged. Oneof presence distinguishes an + // empty replacement from no replacement, and string decoding enforces UTF-8. + oneof replacement { + string text = 3; + bytes binary = 4; + } + // Free-form service diagnostic. OpenShell never exposes this to the + // workload or security logs. Limited to 4 KiB before discarding. + string reason = 5; + // Optional stable machine-readable code for OCSF only. Unlike the HTTP + // reason_code, this value is never put in a WebSocket close frame. + string reason_code = 6; + repeated Finding findings = 7; + map metadata = 8; +} + +// WebSocketSessionEventResult is an evaluation result for a preflight or message +// event. Session start and end events do not produce results. +message WebSocketSessionEventResult { + oneof result { + WebSocketPreflightDecision preflight_decision = 1; + WebSocketMessageResult message_result = 2; + } +} + +// RequestContext identifies the sandbox request being evaluated. +message RequestContext { + // Request id used to correlate middleware and supervisor logs. + string request_id = 1; + // Sandbox id that originated the request. + string sandbox_id = 2; + // Workload process that originated the request, when available. + Process originating_process = 3; + // Sandbox name that originated the request. For display and logging only. + // Names are workspace-scoped and may be reused for different sandbox + // instances, so consumers must use sandbox_id for authorization, persistence, + // durable correlation, and identity. + string sandbox = 4; + // Workspace the sandbox belongs to. For display and logging only; see the + // sandbox guidance above. + string workspace = 5; +} + +// HttpRequestTarget describes the admitted HTTP destination and request target. +message HttpRequestTarget { + // Request scheme, such as "http", "https", "ws", or "wss". + string scheme = 1; + // Destination hostname selected by network policy. + string host = 2; + // Destination TCP port. + uint32 port = 3; + // HTTP request method. + string method = 4; + // Request path without the query string. + string path = 5; + // Raw request query string without the leading question mark. + string query = 6; +} + +// Process identifies a workload process and its executable ancestry. +message Process { + // Executable path for the originating process. + string binary = 1; + // Process id within the sandbox. + uint32 pid = 2; + // Executable paths for ancestor processes, nearest parent first. + repeated string ancestors = 3; +} + +// Decision controls whether OpenShell continues processing the current +// evaluation unit. +enum Decision { + // Invalid response value handled according to the policy failure mode. + DECISION_UNSPECIFIED = 0; + // Continue processing the current request or message and apply any returned + // mutations. + DECISION_ALLOW = 1; + // Reject the current request or message. The operation-specific result + // defines the enclosing protocol behavior. + DECISION_DENY = 2; +} + +// Finding is an audit-safe observation produced during evaluation. +message Finding { + // Stable, service-defined finding type. + string type = 1; + // Human-readable finding label that does not contain request content. + string label = 2; + // Number of matching observations represented by this finding. + uint32 count = 3; + // Service-defined confidence level. + string confidence = 4; + // Service-defined severity level. + string severity = 5; +} + +// ExistingHeaderAction controls how a header write behaves when the +// case-insensitive header name is already present. Every action writes the +// value when the header is absent. +enum ExistingHeaderAction { + EXISTING_HEADER_ACTION_UNSPECIFIED = 0; + // Add another field value without changing existing values. + EXISTING_HEADER_ACTION_APPEND = 1; + // Remove every existing value, then add the new value. + EXISTING_HEADER_ACTION_OVERWRITE = 2; + // Leave the existing values unchanged. + EXISTING_HEADER_ACTION_SKIP = 3; +} + +// WriteHeader proposes one header value and defines collision behavior. +message WriteHeader { + string name = 1; + string value = 2; + ExistingHeaderAction on_existing = 3; +} + +// RemoveHeader removes every value for a case-insensitive header name. +message RemoveHeader { + string name = 1; +} + +// HeaderMutation is one ordered HTTP header operation. +message HeaderMutation { + oneof operation { + WriteHeader write = 1; + RemoveHeader remove = 2; + } +} + +// HttpRequestResult contains the decision and optional request mutations. +message HttpRequestResult { + // Allow or deny decision for this request. + Decision decision = 1; + // Free-form service diagnostic. OpenShell does not relay this text into + // denied responses or security logs. Limited to 4 KiB before discarding. + string reason = 2; + // Replacement request body when has_body is true. Limited to 4 MiB. + bytes body = 3; + // True when body should replace the request body, including with an empty body. + bool has_body = 4; + // Ordered request-header mutations applied before the next middleware and + // before forwarding. Writes and removals may target visible end-to-end + // request headers, but credential, routing, framing, and hop-by-hop headers + // are always protected. Written values cannot contain OpenShell credential + // placeholder syntax. A violating result is a middleware failure handled + // according to the policy failure mode. At most 64 operations, 32 KiB of + // validated name/value data, and 64 KiB encoded are accepted. + repeated HeaderMutation header_mutations = 5; + // Audit-safe findings produced during evaluation. For operator-run services, + // OpenShell logs platform-owned fields derived from the operator-owned + // registration name rather than service-provided type, label, confidence, + // or metadata text. + // At most 32 findings of at most 4 KiB encoded each are accepted per stage. + // A policy selects at most 10 stages, so one chain retains at most 320. + repeated Finding findings = 6; + // Non-secret service-defined metadata included in diagnostics. At most 64 + // entries and 32 KiB of combined key/value data are accepted. + map metadata = 7; + // Optional stable machine-readable code for a deny decision. Codes must + // start with a lowercase ASCII letter and contain only lowercase ASCII + // letters, digits, and underscores, with a maximum length of 64 bytes. + // OpenShell may return this code to the requester, unlike free-form reason. + string reason_code = 8; +}