From 2bbbe3fbdaf83b9740b2d1f12fe1ab2230751c7a Mon Sep 17 00:00:00 2001 From: kperry Date: Mon, 19 Oct 2026 08:49:14 -0500 Subject: [PATCH 1/5] feat: ANS identity extension for NVIDIA OpenShell Initial import of the ans-openshell module: a supervisor middleware that signs allowed outbound requests as the sandbox's registered ANS agent using ANS-6 Method B (DPoP proof, transparency-log receipt and status token), a gateway interceptor that vets destination hosts against ANS registration before a policy is committed, a hydrator that turns OpenShell's OCSF audit export into signed Trust Index observations, and a reference callee that verifies ANS-6 callers with the SDK's pop middleware. Includes the vendored OpenShell protocol definitions (Apache-2.0, see third_party/openshell) with generated Go bindings, an in-process end-to-end demo (make demo), a Helm chart and Dockerfile, CI with a coverage gate, lint and proto-drift checks, and release automation (release-please and GoReleaser) matching the other agentnameservice repositories. Co-Authored-By: Claude Fable 5.1 Signed-off-by: kperry --- .dockerignore | 8 + .github/dependabot.yml | 13 + .github/workflows/ci.yml | 92 + .github/workflows/dco-merge-group.yml | 19 + .github/workflows/release.yml | 67 + .gitignore | 43 + .golangci.yml | 452 + .goreleaser.yaml | 102 + .release-please-manifest.json | 3 + CHANGELOG.md | 1 + CODE_OF_CONDUCT.md | 133 + CONTRIBUTING.md | 181 + LICENSE | 21 + Makefile | 37 + README.md | 161 + SECURITY.md | 19 + buf.gen.yaml | 32 + buf.yaml | 9 + cmd/ans-ocsf-hydrator/main.go | 259 + cmd/ans-openshell-interceptor/main.go | 162 + cmd/ans-openshell-middleware/main.go | 202 + cmd/ans-openshell-middleware/main_test.go | 117 + cmd/ans-reference-callee/main.go | 86 + deploy/Dockerfile | 18 + deploy/deployment_test.go | 180 + deploy/examples/gateway.toml | 40 + deploy/examples/hydrator.yaml | 19 + deploy/examples/interceptor.yaml | 16 + deploy/examples/middleware.yaml | 27 + deploy/examples/policy.yaml | 31 + deploy/helm/ans-openshell/Chart.yaml | 6 + .../helm/ans-openshell/templates/_helpers.tpl | 122 + .../ans-openshell/templates/hydrator.yaml | 57 + .../ans-openshell/templates/interceptor.yaml | 3 + .../ans-openshell/templates/middleware.yaml | 3 + deploy/helm/ans-openshell/values.yaml | 103 + examples/demo/main.go | 32 + examples/demo/scenarios.go | 147 + examples/demo/setup.go | 263 + examples/demo/supervisor.go | 126 + gen/openshell/datamodelv1/datamodel.pb.go | 747 + gen/openshell/extensionv1/extension.pb.go | 230 + .../interceptorv1/gateway_interceptor.pb.go | 1062 + .../gateway_interceptor_grpc.pb.go | 218 + .../middlewarev1/supervisor_middleware.pb.go | 4241 ++++ .../supervisor_middleware_grpc.pb.go | 371 + gen/openshell/openshellv1/openshell.pb.go | 20839 ++++++++++++++++ .../openshellv1/openshell_grpc.pb.go | 3499 +++ gen/openshell/optionsv1/options.pb.go | 206 + gen/openshell/sandboxv1/sandbox.pb.go | 2627 ++ go.mod | 21 + go.sum | 38 + internal/callee/callee.go | 149 + internal/callee/callee_test.go | 199 + internal/config/config.go | 139 + internal/config/config_test.go | 165 + internal/extauth/caller.go | 43 + internal/extauth/caller_test.go | 19 + internal/extauth/grpc.go | 87 + internal/extauth/grpc_test.go | 286 + internal/extauth/helpers_test.go | 132 + internal/extauth/jwks.go | 249 + internal/extauth/jwks_test.go | 336 + internal/extauth/redirect_test.go | 123 + internal/extauth/verifier.go | 311 + internal/extauth/verifier_test.go | 273 + internal/grpcserver/server.go | 216 + internal/grpcserver/server_test.go | 341 + internal/hydrator/agentmap.go | 88 + internal/hydrator/agentmap_test.go | 102 + internal/hydrator/aggregate.go | 155 + internal/hydrator/aggregate_test.go | 144 + internal/hydrator/doc.go | 36 + internal/hydrator/event.go | 237 + internal/hydrator/event_test.go | 341 + internal/hydrator/fixtures_test.go | 139 + internal/hydrator/observation.go | 185 + internal/hydrator/observation_test.go | 219 + internal/hydrator/run.go | 178 + internal/hydrator/run_test.go | 366 + internal/hydrator/sink.go | 174 + internal/hydrator/sink_test.go | 292 + internal/identity/artifacts.go | 131 + internal/identity/artifacts_test.go | 164 + internal/identity/client.go | 73 + internal/identity/client_test.go | 163 + internal/identity/registry.go | 390 + internal/identity/registry_test.go | 265 + internal/identity/registry_verify_test.go | 156 + internal/identity/signer.go | 68 + internal/identity/signer_test.go | 132 + internal/identity/validation.go | 76 + internal/identity/validation_test.go | 103 + internal/interceptor/grpc_test.go | 77 + internal/interceptor/helpers_test.go | 155 + internal/interceptor/hosts.go | 142 + internal/interceptor/hosts_test.go | 109 + internal/interceptor/review_fixes_test.go | 180 + internal/interceptor/service.go | 321 + internal/interceptor/service_test.go | 544 + internal/logging/logging.go | 35 + internal/logging/logging_test.go | 53 + internal/middleware/authorization.go | 40 + internal/middleware/authorization_test.go | 129 + internal/middleware/credentials.go | 53 + internal/middleware/credentials_test.go | 98 + internal/middleware/e2e_test.go | 293 + internal/middleware/grpc_test.go | 88 + internal/middleware/policy.go | 78 + internal/middleware/policy_test.go | 72 + internal/middleware/reasons.go | 66 + internal/middleware/service.go | 455 + internal/middleware/service_test.go | 561 + internal/middleware/target.go | 81 + internal/middleware/target_test.go | 89 + internal/middleware/target_validation_test.go | 41 + internal/pemkeys/pemkeys.go | 65 + internal/pemkeys/pemkeys_test.go | 108 + internal/testfixtures/identity.go | 97 + internal/testfixtures/keys.go | 70 + internal/testfixtures/server.go | 60 + internal/testfixtures/testfixtures_test.go | 385 + internal/testfixtures/tl.go | 187 + internal/vetting/cache.go | 63 + internal/vetting/cache_test.go | 42 + internal/vetting/helpers_test.go | 199 + internal/vetting/host.go | 166 + internal/vetting/host_test.go | 158 + internal/vetting/lookup.go | 129 + internal/vetting/lookup_security_test.go | 173 + internal/vetting/review_fixes_test.go | 47 + internal/vetting/service.go | 344 + internal/vetting/service_test.go | 773 + internal/vetting/trustindex.go | 102 + internal/vetting/trustindex_test.go | 116 + internal/vetting/verdict.go | 168 + internal/vetting/verdict_test.go | 103 + pkg/provenance/external_key_test.go | 149 + pkg/provenance/signer.go | 244 + pkg/provenance/signer_test.go | 308 + release-please-config.json | 28 + third_party/openshell/LICENSE | 203 + third_party/openshell/README.md | 10 + third_party/openshell/VERSION | 3 + third_party/openshell/proto/datamodel.proto | 128 + third_party/openshell/proto/extension.proto | 34 + .../openshell/proto/gateway_interceptor.proto | 153 + third_party/openshell/proto/openshell.proto | 3913 +++ third_party/openshell/proto/options.proto | 36 + third_party/openshell/proto/sandbox.proto | 481 + .../proto/supervisor_middleware.proto | 722 + 151 files changed, 58683 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dco-merge-group.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 .release-please-manifest.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 cmd/ans-ocsf-hydrator/main.go create mode 100644 cmd/ans-openshell-interceptor/main.go create mode 100644 cmd/ans-openshell-middleware/main.go create mode 100644 cmd/ans-openshell-middleware/main_test.go create mode 100644 cmd/ans-reference-callee/main.go create mode 100644 deploy/Dockerfile create mode 100644 deploy/deployment_test.go create mode 100644 deploy/examples/gateway.toml create mode 100644 deploy/examples/hydrator.yaml create mode 100644 deploy/examples/interceptor.yaml create mode 100644 deploy/examples/middleware.yaml create mode 100644 deploy/examples/policy.yaml create mode 100644 deploy/helm/ans-openshell/Chart.yaml create mode 100644 deploy/helm/ans-openshell/templates/_helpers.tpl create mode 100644 deploy/helm/ans-openshell/templates/hydrator.yaml create mode 100644 deploy/helm/ans-openshell/templates/interceptor.yaml create mode 100644 deploy/helm/ans-openshell/templates/middleware.yaml create mode 100644 deploy/helm/ans-openshell/values.yaml create mode 100644 examples/demo/main.go create mode 100644 examples/demo/scenarios.go create mode 100644 examples/demo/setup.go create mode 100644 examples/demo/supervisor.go create mode 100644 gen/openshell/datamodelv1/datamodel.pb.go create mode 100644 gen/openshell/extensionv1/extension.pb.go create mode 100644 gen/openshell/interceptorv1/gateway_interceptor.pb.go create mode 100644 gen/openshell/interceptorv1/gateway_interceptor_grpc.pb.go create mode 100644 gen/openshell/middlewarev1/supervisor_middleware.pb.go create mode 100644 gen/openshell/middlewarev1/supervisor_middleware_grpc.pb.go create mode 100644 gen/openshell/openshellv1/openshell.pb.go create mode 100644 gen/openshell/openshellv1/openshell_grpc.pb.go create mode 100644 gen/openshell/optionsv1/options.pb.go create mode 100644 gen/openshell/sandboxv1/sandbox.pb.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/callee/callee.go create mode 100644 internal/callee/callee_test.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/extauth/caller.go create mode 100644 internal/extauth/caller_test.go create mode 100644 internal/extauth/grpc.go create mode 100644 internal/extauth/grpc_test.go create mode 100644 internal/extauth/helpers_test.go create mode 100644 internal/extauth/jwks.go create mode 100644 internal/extauth/jwks_test.go create mode 100644 internal/extauth/redirect_test.go create mode 100644 internal/extauth/verifier.go create mode 100644 internal/extauth/verifier_test.go create mode 100644 internal/grpcserver/server.go create mode 100644 internal/grpcserver/server_test.go create mode 100644 internal/hydrator/agentmap.go create mode 100644 internal/hydrator/agentmap_test.go create mode 100644 internal/hydrator/aggregate.go create mode 100644 internal/hydrator/aggregate_test.go create mode 100644 internal/hydrator/doc.go create mode 100644 internal/hydrator/event.go create mode 100644 internal/hydrator/event_test.go create mode 100644 internal/hydrator/fixtures_test.go create mode 100644 internal/hydrator/observation.go create mode 100644 internal/hydrator/observation_test.go create mode 100644 internal/hydrator/run.go create mode 100644 internal/hydrator/run_test.go create mode 100644 internal/hydrator/sink.go create mode 100644 internal/hydrator/sink_test.go create mode 100644 internal/identity/artifacts.go create mode 100644 internal/identity/artifacts_test.go create mode 100644 internal/identity/client.go create mode 100644 internal/identity/client_test.go create mode 100644 internal/identity/registry.go create mode 100644 internal/identity/registry_test.go create mode 100644 internal/identity/registry_verify_test.go create mode 100644 internal/identity/signer.go create mode 100644 internal/identity/signer_test.go create mode 100644 internal/identity/validation.go create mode 100644 internal/identity/validation_test.go create mode 100644 internal/interceptor/grpc_test.go create mode 100644 internal/interceptor/helpers_test.go create mode 100644 internal/interceptor/hosts.go create mode 100644 internal/interceptor/hosts_test.go create mode 100644 internal/interceptor/review_fixes_test.go create mode 100644 internal/interceptor/service.go create mode 100644 internal/interceptor/service_test.go create mode 100644 internal/logging/logging.go create mode 100644 internal/logging/logging_test.go create mode 100644 internal/middleware/authorization.go create mode 100644 internal/middleware/authorization_test.go create mode 100644 internal/middleware/credentials.go create mode 100644 internal/middleware/credentials_test.go create mode 100644 internal/middleware/e2e_test.go create mode 100644 internal/middleware/grpc_test.go create mode 100644 internal/middleware/policy.go create mode 100644 internal/middleware/policy_test.go create mode 100644 internal/middleware/reasons.go create mode 100644 internal/middleware/service.go create mode 100644 internal/middleware/service_test.go create mode 100644 internal/middleware/target.go create mode 100644 internal/middleware/target_test.go create mode 100644 internal/middleware/target_validation_test.go create mode 100644 internal/pemkeys/pemkeys.go create mode 100644 internal/pemkeys/pemkeys_test.go create mode 100644 internal/testfixtures/identity.go create mode 100644 internal/testfixtures/keys.go create mode 100644 internal/testfixtures/server.go create mode 100644 internal/testfixtures/testfixtures_test.go create mode 100644 internal/testfixtures/tl.go create mode 100644 internal/vetting/cache.go create mode 100644 internal/vetting/cache_test.go create mode 100644 internal/vetting/helpers_test.go create mode 100644 internal/vetting/host.go create mode 100644 internal/vetting/host_test.go create mode 100644 internal/vetting/lookup.go create mode 100644 internal/vetting/lookup_security_test.go create mode 100644 internal/vetting/review_fixes_test.go create mode 100644 internal/vetting/service.go create mode 100644 internal/vetting/service_test.go create mode 100644 internal/vetting/trustindex.go create mode 100644 internal/vetting/trustindex_test.go create mode 100644 internal/vetting/verdict.go create mode 100644 internal/vetting/verdict_test.go create mode 100644 pkg/provenance/external_key_test.go create mode 100644 pkg/provenance/signer.go create mode 100644 pkg/provenance/signer_test.go create mode 100644 release-please-config.json create mode 100644 third_party/openshell/LICENSE create mode 100644 third_party/openshell/README.md create mode 100644 third_party/openshell/VERSION create mode 100644 third_party/openshell/proto/datamodel.proto create mode 100644 third_party/openshell/proto/extension.proto create mode 100644 third_party/openshell/proto/gateway_interceptor.proto create mode 100644 third_party/openshell/proto/openshell.proto create mode 100644 third_party/openshell/proto/options.proto create mode 100644 third_party/openshell/proto/sandbox.proto create mode 100644 third_party/openshell/proto/supervisor_middleware.proto 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..975ee98 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +# 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=90 + 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 + - 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..c3cf928 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,67 @@ +# Automatically creates release PRs and, when a release-please release lands, +# builds and uploads the four ans-openshell binaries to that release 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 }} 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..cf1bd05 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +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 + +proto: + 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..e17d8e7 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# 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. It reads OCSF JSONL (or the supervisor's stderr with its +`OCSF-JSON` lines) from `--input` or stdin, aggregates per agent and posts one batch: + +```bash +./bin/ans-ocsf-hydrator --config deploy/examples/hydrator.yaml --input gateway-ocsf.jsonl +``` + +`--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, and Advisor proposals are submitted or edited. +6. Ship the gateway's OCSF JSONL to the hydrator ([deploy/examples/hydrator.yaml](deploy/examples/hydrator.yaml)) + on a schedule to feed the Trust Index. 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..9266cd8 --- /dev/null +++ b/cmd/ans-ocsf-hydrator/main.go @@ -0,0 +1,259 @@ +// 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 + dryRun, allowUnmapped bool +} + +// errNothingAttributed reports an input whose events none of the configured +// agents claimed: almost always mapping drift, so the job fails rather than +// quietly starving the Trust Index. +var errNothingAttributed = errors.New("events were parsed but none could be attributed 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 events were parsed but none mapped 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)") + 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, report, err) + if err != nil { + return err + } + if report.Parse.Events > 0 && report.Observations == 0 && !f.allowUnmapped { + 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 f.windowStart != "" || f.windowEnd != "" { + w, err := parseWindow(f.windowStart, f.windowEnd) + if err != nil { + return hydrator.Options{}, err + } + opts.Window = &w + } + return opts, nil +} + +// 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, 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, + "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)) + for _, a := range r.Agents { + logger.InfoContext(ctx, "agent summary", "agent_id", a.AgentID, "ans_name", a.AnsName, + "events", a.Events, "denied", a.Denied, "observations", a.Observations) + } +} diff --git a/cmd/ans-openshell-interceptor/main.go b/cmd/ans-openshell-interceptor/main.go new file mode 100644 index 0000000..886263e --- /dev/null +++ b/cmd/ans-openshell-interceptor/main.go @@ -0,0 +1,162 @@ +// 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 = 15 * 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 + } + if cfg.DestinationPolicy == "" { + cfg.DestinationPolicy = defaultPolicy + } + mode, err := vetting.ParseMode(cfg.DestinationPolicy) + if err != nil { + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + 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, interceptor.Config{ + Audience: cfg.Auth.Audience, + Mode: mode, + ImplementationVersion: version, + LookupWait: cfg.LookupWait, + }, logger) + interceptorv1.RegisterGatewayInterceptorServer(srv.Registrar(), svc) + go watchHealth(ctx, srv, auth, logger) + logger.InfoContext(ctx, "ans-openshell-interceptor starting", "version", version, "mode", mode.String()) + return srv.Serve(ctx) +} + +// watchHealth keeps readiness tied to the verifier holding a gateway key. +func watchHealth(ctx context.Context, srv *grpcserver.Server, auth *extauth.Verifier, logger *slog.Logger) { + ticker := time.NewTicker(healthCheckInterval) + defer ticker.Stop() + ready := auth.Ready() + srv.SetHealthy(ready) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if now := auth.Ready(); now != ready { + ready = now + srv.SetHealthy(ready) + logger.WarnContext(ctx, "readiness changed", "ready", ready, "reason", "gateway verification keys") + } + } + } +} diff --git a/cmd/ans-openshell-middleware/main.go b/cmd/ans-openshell-middleware/main.go new file mode 100644 index 0000000..5b5d262 --- /dev/null +++ b/cmd/ans-openshell-middleware/main.go @@ -0,0 +1,202 @@ +// 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 = 30 * 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 + var vettingBudget time.Duration + if cfg.Vetting != nil { + vetSvc, vetErr := vetting.New(*cfg.Vetting, vetting.Deps{}, logger) + if vetErr != nil { + return vetErr + } + defer vetSvc.Close() + vet = vetSvc + vettingBudget = cfg.Vetting.RequestBudget + } + + 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, middleware.Config{ + Audience: cfg.Auth.Audience, + MaxPayloadBytes: cfg.MaxPayloadBytes, + RequestTimeout: cfg.RequestTimeout, + ImplementationVersion: version, + VettingBudget: vettingBudget, + RequireAuthenticatedCaller: !cfg.Auth.AllowInsecureNoAuth, + }, logger) + middlewarev1.RegisterSupervisorMiddlewareServer(srv.Registrar(), svc) + go watchHealth(ctx, srv, registry, auth, logger) + logger.InfoContext(ctx, "ans-openshell-middleware starting", + "version", version, "identities", registry.Names(), "vetting", vet != nil) + return srv.Serve(ctx) +} + +// watchHealth keeps readiness tied 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. +func watchHealth(ctx context.Context, srv *grpcserver.Server, registry *identity.Registry, auth *extauth.Verifier, + logger *slog.Logger) { + ticker := time.NewTicker(healthCheckInterval) + defer ticker.Stop() + state := healthState{} + for { + next := healthState{authReady: auth.Ready(), unhealthy: registry.Unhealthy(), total: len(registry.Names())} + if next.ready() != state.ready() || !slices.Equal(next.unhealthy, state.unhealthy) { + srv.SetHealthy(next.ready()) + logger.LogAttrs(ctx, next.level(), "readiness evaluated", + slog.Bool("ready", next.ready()), slog.Bool("gateway_keys", next.authReady), + slog.Any("unhealthy_identities", next.unhealthy)) + } + state = next + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } +} + +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..54bfc83 --- /dev/null +++ b/cmd/ans-openshell-middleware/main_test.go @@ -0,0 +1,117 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "maps" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/agentnameservice/ans-openshell/internal/config" + "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) + 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()) + } + }) + } +} + +func loadStartupConfig(t *testing.T, approval 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" +auth: + allow_insecure_no_auth: true +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 +`, 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 +} 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..e15589f --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,18 @@ +# 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 golang:${GO_VERSION}-alpine AS build +ARG CMD=ans-openshell-middleware +ARG VERSION=dev +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 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..83456cc --- /dev/null +++ b/deploy/examples/gateway.toml @@ -0,0 +1,40 @@ +# 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 and +# policy edits even when its destination_policy is "observe". 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"] diff --git a/deploy/examples/hydrator.yaml b/deploy/examples/hydrator.yaml new file mode 100644 index 0000000..6a63905 --- /dev/null +++ b/deploy/examples/hydrator.yaml @@ -0,0 +1,19 @@ +# ans-ocsf-hydrator --config hydrator.yaml --input /var/log/openshell/gateway-ocsf.jsonl +# 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..c5571cc --- /dev/null +++ b/deploy/helm/ans-openshell/templates/hydrator.yaml @@ -0,0 +1,57 @@ +{{- 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 + 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 }}] +{{- 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..0bd11d9 --- /dev/null +++ b/deploy/helm/ans-openshell/values.yaml @@ -0,0 +1,103 @@ +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 gateway's OCSF JSONL. Mount the file with extraVolumes +# and extraVolumeMounts (a hostPath or the gateway's log PVC) and point input at it. +hydrator: + enabled: false + image: + repository: ghcr.io/agentnameservice/ans-ocsf-hydrator + tag: "" + schedule: "*/15 * * * *" + input: /var/log/openshell/gateway-ocsf.jsonl + 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..bd7374b --- /dev/null +++ b/examples/demo/setup.go @@ -0,0 +1,263 @@ +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 + } + 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..f6b8da6 --- /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..388cf92 --- /dev/null +++ b/gen/openshell/sandboxv1/sandbox.pb.go @@ -0,0 +1,2627 @@ +// 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..6783d22 --- /dev/null +++ b/internal/grpcserver/server.go @@ -0,0 +1,216 @@ +// 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/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 + 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) + } + } + s.ready.Store(true) + grpc_health_v1.RegisterHealthServer(s.grpc, s.health) + 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. +func (s *Server) SetHealthy(healthy bool) { + s.ready.Store(healthy) + status := grpc_health_v1.HealthCheckResponse_NOT_SERVING + if healthy { + status = grpc_health_v1.HealthCheckResponse_SERVING + } + s.health.SetServingStatus("", status) +} + +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.SetHealthy(false) + 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..c50104b --- /dev/null +++ b/internal/grpcserver/server_test.go @@ -0,0 +1,341 @@ +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" + "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) + } + 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) + } + 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) + } + 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) + } + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- s.Serve(ctx) }() + base := "http://" + s.HealthAddr().String() + for range 50 { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+"/readyz", nil) + if resp, err := http.DefaultClient.Do(req); err == nil { + _ = resp.Body.Close() + break + } + time.Sleep(20 * time.Millisecond) + } + 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) + } + 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) + } +} 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..692d93f --- /dev/null +++ b/internal/hydrator/aggregate.go @@ -0,0 +1,155 @@ +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 +} + +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 + } + 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..f6b1ff3 --- /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 { + t.Fatalf("unmapped/outOfWindow = %d/%d", agg.unmapped, agg.outOfWindow) + } +} + +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 { + t.Fatalf("outOfWindow = %d, want 3", agg.outOfWindow) + } + 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..9a47bf7 --- /dev/null +++ b/internal/hydrator/run.go @@ -0,0 +1,178 @@ +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 + // 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(), + 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) + } + 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..a75ed24 --- /dev/null +++ b/internal/hydrator/run_test.go @@ -0,0 +1,366 @@ +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}, + 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: "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}, + 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 || 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.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) + } + }) + } +} 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..d113eb6 --- /dev/null +++ b/internal/interceptor/helpers_test.go @@ -0,0 +1,155 @@ +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" +) + +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, &sandboxv1.NetworkEndpoint{Host: h, Port: 443, Protocol: "rest"}) + } + 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 &interceptorv1.InterceptorEvaluation{ + InterceptorName: "ans", + BindingId: "test-binding", + Service: openShellService, + Method: method, + Phase: &interceptorv1.InterceptorEvaluation_Validate{Validate: &interceptorv1.ValidateEvaluation{ProposedOperation: op}}, + } +} + +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) } diff --git a/internal/interceptor/hosts.go b/internal/interceptor/hosts.go new file mode 100644 index 0000000..973b539 --- /dev/null +++ b/internal/interceptor/hosts.go @@ -0,0 +1,142 @@ +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 and returns every destination +// host it would admit. +type hostExtractor func(*structpb.Struct) ([]string, error) + +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), + } +} + +// 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 +} + +// ruleHosts lists every destination a rule 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 ruleHosts(rule *sandboxv1.NetworkPolicyRule) []string { + endpoints := rule.GetEndpoints() + 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 Struct into msg, +// ignoring fields this build of the contract does not know. +func decodeOperation(op *structpb.Struct, msg proto.Message) error { + if op == nil { + return errors.New("proposed_operation is required") + } + raw, err := protojson.Marshal(op) + if err != nil { + return fmt.Errorf("encode proposed_operation: %w", err) + } + if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, msg); err != nil { + return fmt.Errorf("decode proposed_operation: %w", 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..c36d72d --- /dev/null +++ b/internal/interceptor/hosts_test.go @@ -0,0 +1,109 @@ +package interceptor + +import ( + "slices" + "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"}, + } + 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 + } + tests := []struct { + name string + method string + op *structpb.Struct + want []string + wantErr bool + }{ + { + 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: "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) + if (err != nil) != tt.wantErr { + t.Fatalf("extract() error = %v, wantErr %v", err, tt.wantErr) + } + slices.Sort(got) + if !tt.wantErr && !slices.Equal(got, tt.want) { + t.Fatalf("extract() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/interceptor/review_fixes_test.go b/internal/interceptor/review_fixes_test.go new file mode 100644 index 0000000..2d57e0a --- /dev/null +++ b/internal/interceptor/review_fixes_test.go @@ -0,0 +1,180 @@ +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)) + 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); 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 +} diff --git a/internal/interceptor/service.go b/internal/interceptor/service.go new file mode 100644 index 0000000..8076124 --- /dev/null +++ b/internal/interceptor/service.go @@ -0,0 +1,321 @@ +// 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" + "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" +) + +// 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. +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()) + 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}, + } +} + +// 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..cb7fe9a --- /dev/null +++ b/internal/interceptor/service_test.go @@ -0,0 +1,544 @@ +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", + } + 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..0c1b299 --- /dev/null +++ b/internal/vetting/review_fixes_test.go @@ -0,0 +1,47 @@ +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 + 30*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 }) + 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) + } + 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..12ddf38 --- /dev/null +++ b/internal/vetting/service.go @@ -0,0 +1,344 @@ +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: a dependency blip during refresh-ahead must +// not turn a registered host into a denial. +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) { + 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; +} From 68db221fd59bbd1a9fc3cc9b0afdfaf463177cf9 Mon Sep 17 00:00:00 2001 From: kperry Date: Mon, 19 Oct 2026 09:22:09 -0500 Subject: [PATCH 2/5] ci: pin buf to the version that generated the bindings buf-action installs the latest buf by default; 1.74.0 formats generated doc comments differently from the 1.73.0 used for gen/, so the drift check failed on the PR. Co-Authored-By: Claude Fable 5.1 Signed-off-by: kperry --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 975ee98..3fffef4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,9 @@ jobs: - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 #v1 with: setup_only: true + # Pin the buf that generated gen/: buf releases change the formatting of + # generated doc comments, which this drift check would report. + version: 1.73.0 - name: Install protoc plugins run: | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.12 From 5fa87ce2b9633feb6f08ca429a895890b762b55e Mon Sep 17 00:00:00 2001 From: kperry Date: Mon, 19 Oct 2026 09:33:21 -0500 Subject: [PATCH 3/5] fix: regenerate bindings with the CI toolchain and pin the proto plugins The committed bindings were produced by a protoc-gen-go built with Go 1.27, whose go/format lays out doc-comment lists differently from the Go 1.26.9 that CI builds the plugin with, so the drift check failed. make proto now builds both plugins with the CI toolchain at the pinned versions. Co-Authored-By: Claude Fable 5.1 Signed-off-by: kperry --- .github/workflows/ci.yml | 3 +-- Makefile | 11 ++++++++++- gen/openshell/openshellv1/openshell.pb.go | 4 ++-- gen/openshell/sandboxv1/sandbox.pb.go | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fffef4..e845a09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,8 +82,7 @@ jobs: - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 #v1 with: setup_only: true - # Pin the buf that generated gen/: buf releases change the formatting of - # generated doc comments, which this drift check would report. + # Pinned so the drift check is reproducible; make proto documents the plugin pins. version: 1.73.0 - name: Install protoc plugins run: | diff --git a/Makefile b/Makefile index cf1bd05..b04fb6b 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,17 @@ 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: - buf generate + @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 ./... diff --git a/gen/openshell/openshellv1/openshell.pb.go b/gen/openshell/openshellv1/openshell.pb.go index f6b8da6..c11f63e 100644 --- a/gen/openshell/openshellv1/openshell.pb.go +++ b/gen/openshell/openshellv1/openshell.pb.go @@ -11702,8 +11702,8 @@ type UpdateConfigRequest struct { // The new policy to apply. // // Sandbox scope (`global=false`): - // - only network_policies may differ from create-time - // policy; static fields must match version 1. + // - 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). diff --git a/gen/openshell/sandboxv1/sandbox.pb.go b/gen/openshell/sandboxv1/sandbox.pb.go index 388cf92..8dcf0f5 100644 --- a/gen/openshell/sandboxv1/sandbox.pb.go +++ b/gen/openshell/sandboxv1/sandbox.pb.go @@ -841,6 +841,7 @@ type NetworkEndpoint struct { // 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. From c49a70734ae5e16673c6d8a79eb18aaa6fc6e4b0 Mon Sep 17 00:00:00 2001 From: kperry Date: Mon, 19 Oct 2026 14:43:59 -0500 Subject: [PATCH 4/5] fix: assert readiness before serving, vet provider-profile endpoints, back off failed refreshes Readiness: grpcserver.New now starts not-ready and stays not-ready once Serve begins draining; Server.WatchReadiness applies an owner-supplied probe immediately and on every tick. Both binaries wire their probes through it, so a middleware replica that holds no gateway verification key at startup answers /readyz with 503 instead of advertising Ready while rejecting every RPC. Destination vetting: the interceptor binds ImportProviderProfiles and UpdateProviderProfiles. Custom provider profiles carry endpoints that the gateway composes into every attached sandbox's effective policy, so they are vetted like policy rules; the gateway example registers both RPCs and the README notes that built-in profiles cannot be vetted. Vetting cache: a refresh that fails while a valid verdict is cached defers the next refresh by the error TTL instead of retrying on every hit. Binaries: the config-to-service mapping is a tested function in each main; the interceptor rejects vetting.request_budget, which it never honoured. Release and CI: release.yml publishes the three chart images to ghcr.io at the released version, which the chart's defaults already reference, for linux/amd64 and linux/arm64; the Dockerfile cross-compiles for the target platform. The coverage gate is raised to 95%. Co-Authored-By: Claude Fable 5.1 Signed-off-by: kperry --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 46 +++++- README.md | 9 +- cmd/ans-openshell-interceptor/main.go | 84 ++++++---- cmd/ans-openshell-interceptor/main_test.go | 179 +++++++++++++++++++++ cmd/ans-openshell-middleware/main.go | 66 ++++---- cmd/ans-openshell-middleware/main_test.go | 174 +++++++++++++++++++- deploy/Dockerfile | 6 +- deploy/examples/gateway.toml | 12 +- examples/demo/setup.go | 1 + internal/grpcserver/server.go | 51 +++++- internal/grpcserver/server_test.go | 93 +++++++++++ internal/interceptor/helpers_test.go | 28 +++- internal/interceptor/hosts.go | 36 ++++- internal/interceptor/hosts_test.go | 15 ++ internal/interceptor/review_fixes_test.go | 32 ++++ internal/interceptor/service_test.go | 10 +- internal/vetting/review_fixes_test.go | 20 ++- internal/vetting/service.go | 6 +- 19 files changed, 781 insertions(+), 89 deletions(-) create mode 100644 cmd/ans-openshell-interceptor/main_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e845a09..7c989e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: 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=90 + THRESHOLD=95 if [ "$(echo "${COVERAGE} < ${THRESHOLD}" | bc -l)" -eq 1 ]; then echo "::error::Coverage ${COVERAGE}% is below the required ${THRESHOLD}% threshold" exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3cf928..68ba3d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ # Automatically creates release PRs and, when a release-please release lands, -# builds and uploads the four ans-openshell binaries to that release in the -# same workflow run. +# 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. @@ -65,3 +65,45 @@ jobs: 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/README.md b/README.md index e17d8e7..df56c07 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,14 @@ The reference callee takes flags only: 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, and Advisor proposals are submitted or edited. + to vet hosts when sandboxes are created, policies are updated, custom provider profiles are + imported or updated, and Advisor proposals are submitted or edited. An operation naming more + than 64 destinations is denied under `require_registered`, so import large profile sets in + batches. 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 gateway's OCSF JSONL to the hydrator ([deploy/examples/hydrator.yaml](deploy/examples/hydrator.yaml)) on a schedule to feed the Trust Index. The signal ids it emits are configurable; the Trust Index must register them before it scores them. diff --git a/cmd/ans-openshell-interceptor/main.go b/cmd/ans-openshell-interceptor/main.go index 886263e..8d6bb71 100644 --- a/cmd/ans-openshell-interceptor/main.go +++ b/cmd/ans-openshell-interceptor/main.go @@ -32,7 +32,7 @@ const ( defaultHealthListen = ":8081" defaultPolicy = "observe" drainDelay = 3 * time.Second - healthCheckInterval = 15 * time.Second + healthCheckInterval = 5 * time.Second ) type fileConfig struct { @@ -92,17 +92,16 @@ func run() error { if cfg.HealthListen == "" { cfg.HealthListen = defaultHealthListen } - if cfg.DestinationPolicy == "" { - cfg.DestinationPolicy = defaultPolicy - } - mode, err := vetting.ParseMode(cfg.DestinationPolicy) - if err != nil { - return err - } - 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 @@ -129,34 +128,59 @@ func run() error { if err != nil { return err } - svc := interceptor.NewService(vet, interceptor.Config{ + 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, - }, logger) - interceptorv1.RegisterGatewayInterceptorServer(srv.Registrar(), svc) - go watchHealth(ctx, srv, auth, logger) - logger.InfoContext(ctx, "ans-openshell-interceptor starting", "version", version, "mode", mode.String()) - return srv.Serve(ctx) + }, nil } -// watchHealth keeps readiness tied to the verifier holding a gateway key. -func watchHealth(ctx context.Context, srv *grpcserver.Server, auth *extauth.Verifier, logger *slog.Logger) { - ticker := time.NewTicker(healthCheckInterval) - defer ticker.Stop() - ready := auth.Ready() - srv.SetHealthy(ready) - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if now := auth.Ready(); now != ready { - ready = now - srv.SetHealthy(ready) - logger.WarnContext(ctx, "readiness changed", "ready", ready, "reason", "gateway verification keys") +// 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 index 5b5d262..dc6b945 100644 --- a/cmd/ans-openshell-middleware/main.go +++ b/cmd/ans-openshell-middleware/main.go @@ -32,7 +32,7 @@ var version = "dev" const ( defaultListen = ":50051" defaultHealthListen = ":8081" - healthCheckInterval = 30 * time.Second + healthCheckInterval = 5 * time.Second drainDelay = 3 * time.Second ) @@ -123,7 +123,6 @@ func serve(ctx context.Context, cfg fileConfig, logger *slog.Logger) error { } var vet vetting.Verifier - var vettingBudget time.Duration if cfg.Vetting != nil { vetSvc, vetErr := vetting.New(*cfg.Vetting, vetting.Deps{}, logger) if vetErr != nil { @@ -131,7 +130,6 @@ func serve(ctx context.Context, cfg fileConfig, logger *slog.Logger) error { } defer vetSvc.Close() vet = vetSvc - vettingBudget = cfg.Vetting.RequestBudget } srv, err := grpcserver.New(ctx, grpcserver.Options{ @@ -145,44 +143,56 @@ func serve(ctx context.Context, cfg fileConfig, logger *slog.Logger) error { if err != nil { return err } - svc := middleware.NewService(registry, authz, vet, middleware.Config{ + 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, - VettingBudget: vettingBudget, RequireAuthenticatedCaller: !cfg.Auth.AllowInsecureNoAuth, - }, logger) - middlewarev1.RegisterSupervisorMiddlewareServer(srv.Registrar(), svc) - go watchHealth(ctx, srv, registry, auth, logger) - logger.InfoContext(ctx, "ans-openshell-middleware starting", - "version", version, "identities", registry.Names(), "vetting", vet != nil) - return srv.Serve(ctx) + } + if cfg.Vetting != nil { + c.VettingBudget = cfg.Vetting.RequestBudget + } + return c } -// watchHealth keeps readiness tied to what the replica can actually do: it -// must hold a gateway verification key and at least one identity with usable +// 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. -func watchHealth(ctx context.Context, srv *grpcserver.Server, registry *identity.Registry, auth *extauth.Verifier, - logger *slog.Logger) { - ticker := time.NewTicker(healthCheckInterval) - defer ticker.Stop() - state := healthState{} - for { +// 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 next.ready() != state.ready() || !slices.Equal(next.unhealthy, state.unhealthy) { - srv.SetHealthy(next.ready()) + 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)) } - state = next - select { - case <-ctx.Done(): - return - case <-ticker.C: - } + last = &next + return next.ready() } } diff --git a/cmd/ans-openshell-middleware/main_test.go b/cmd/ans-openshell-middleware/main_test.go index 54bfc83..fd20ec9 100644 --- a/cmd/ans-openshell-middleware/main_test.go +++ b/cmd/ans-openshell-middleware/main_test.go @@ -6,14 +6,17 @@ import ( "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" ) @@ -32,7 +35,7 @@ func TestServeSandboxApprovals(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { t.Parallel() - cfg := loadStartupConfig(t, tt.approval) + 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) } @@ -59,7 +62,9 @@ func TestServeSandboxApprovals(t *testing.T) { } } -func loadStartupConfig(t *testing.T, approval string) fileConfig { +// 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") @@ -76,8 +81,9 @@ func loadStartupConfig(t *testing.T, approval string) fileConfig { } } yaml := fmt.Sprintf(`listen: "127.0.0.1:0" +health_listen: %q auth: - allow_insecure_no_auth: true +%s transparency_log: root_keys_file: %q identities: @@ -89,7 +95,7 @@ identities: receipt_file: %q status_token_file: %q sandbox_identities: %s -`, filepath.Join(dir, "roots.txt"), id.AnsName, id.AgentID, +`, 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") @@ -115,3 +121,163 @@ func (w *startupLogWriter) Write(p []byte) (int, error) { } 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/deploy/Dockerfile b/deploy/Dockerfile index e15589f..f3f6cd1 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,14 +1,16 @@ # 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 golang:${GO_VERSION}-alpine AS build +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 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/${CMD} ./cmd/${CMD} +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 diff --git a/deploy/examples/gateway.toml b/deploy/examples/gateway.toml index 83456cc..c2ce3a9 100644 --- a/deploy/examples/gateway.toml +++ b/deploy/examples/gateway.toml @@ -9,8 +9,8 @@ max_payload_bytes = 4194304 # 4 MiB: the body is hashed into 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 and -# policy edits even when its destination_policy is "observe". Use "fail_open" where availability +# failure_policy = "fail_closed" makes the interceptor a hard dependency of sandbox creation, +# policy edits and custom provider-profile imports even when its destination_policy is "observe". Use "fail_open" where availability # matters more than vetting coverage; in require_registered mode keep it closed. [[openshell.gateway.interceptors]] name = "ans-destinations" @@ -38,3 +38,11 @@ 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"] diff --git a/examples/demo/setup.go b/examples/demo/setup.go index bd7374b..57117a6 100644 --- a/examples/demo/setup.go +++ b/examples/demo/setup.go @@ -191,6 +191,7 @@ func (d *demo) startMiddleware(ctx context.Context, dir, jwksFile string) error 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 diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index 6783d22..ae657b9 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -10,6 +10,7 @@ import ( "log/slog" "net" "net/http" + "sync" "sync/atomic" "time" @@ -61,6 +62,8 @@ type Server struct { lis net.Listener healthLis net.Listener ready atomic.Bool + mu sync.Mutex + draining bool opts Options logger *slog.Logger } @@ -109,8 +112,8 @@ func New(ctx context.Context, opts Options, logger *slog.Logger) (*Server, error return nil, fmt.Errorf("listen on health address %q: %w", opts.HealthListen, err) } } - s.ready.Store(true) grpc_health_v1.RegisterHealthServer(s.grpc, s.health) + s.health.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING) return s, nil } @@ -130,9 +133,19 @@ func (s *Server) HealthAddr() net.Addr { } // SetHealthy flips the gRPC health service between SERVING and NOT_SERVING and -// the HTTP /readyz endpoint between 200 and 503. +// 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.ready.Store(healthy) + 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 @@ -140,6 +153,36 @@ func (s *Server) SetHealthy(healthy bool) { 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) { @@ -185,7 +228,7 @@ func (s *Server) Serve(ctx context.Context) error { } case <-ctx.Done(): } - s.SetHealthy(false) + s.drain() s.health.Shutdown() if s.opts.DrainDelay > 0 { time.Sleep(s.opts.DrainDelay) diff --git a/internal/grpcserver/server_test.go b/internal/grpcserver/server_test.go index c50104b..ae574be 100644 --- a/internal/grpcserver/server_test.go +++ b/internal/grpcserver/server_test.go @@ -14,6 +14,7 @@ import ( "net/http" "os" "path/filepath" + "sync/atomic" "testing" "time" @@ -60,6 +61,7 @@ func TestServe_PlaintextHealthAndShutdown(t *testing.T) { 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) }() @@ -104,6 +106,7 @@ func TestServe_TLS(t *testing.T) { 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) }() @@ -246,6 +249,7 @@ func TestServe_HealthEndpoints(t *testing.T) { 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) }() @@ -308,18 +312,24 @@ func TestServe_DrainDelayReportsNotReadyFirst(t *testing.T) { 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) @@ -332,6 +342,10 @@ func TestServe_DrainDelayReportsNotReadyFirst(t *testing.T) { 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) } @@ -339,3 +353,82 @@ func TestServe_DrainDelayReportsNotReadyFirst(t *testing.T) { 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/interceptor/helpers_test.go b/internal/interceptor/helpers_test.go index d113eb6..d33a931 100644 --- a/internal/interceptor/helpers_test.go +++ b/internal/interceptor/helpers_test.go @@ -24,6 +24,8 @@ const ( methodCreate = "CreateSandbox" methodSubmit = "SubmitPolicyAnalysis" methodEdit = "EditDraftChunk" + methodImport = "ImportProviderProfiles" + methodUpdate = "UpdateProviderProfiles" ) type lookupCall struct { @@ -103,7 +105,7 @@ func mustStruct(t *testing.T, v map[string]any) *structpb.Struct { func rule(hosts ...string) *sandboxv1.NetworkPolicyRule { endpoints := make([]*sandboxv1.NetworkEndpoint, 0, len(hosts)) for _, h := range hosts { - endpoints = append(endpoints, &sandboxv1.NetworkEndpoint{Host: h, Port: 443, Protocol: "rest"}) + endpoints = append(endpoints, endpoint(h)) } return &sandboxv1.NetworkPolicyRule{Name: "rule", Endpoints: endpoints} } @@ -153,3 +155,27 @@ func gatewayPeer(major, minor uint32, required ...string) *extensionv1.PeerMetad } 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 +} diff --git a/internal/interceptor/hosts.go b/internal/interceptor/hosts.go index 973b539..d86d655 100644 --- a/internal/interceptor/hosts.go +++ b/internal/interceptor/hosts.go @@ -34,6 +34,8 @@ func newBindings() []binding { 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), } } @@ -92,11 +94,35 @@ func editDraftChunkHosts(op *structpb.Struct) ([]string, error) { return ruleHosts(req.GetProposedRule()), nil } -// ruleHosts lists every destination a rule 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 ruleHosts(rule *sandboxv1.NetworkPolicyRule) []string { - endpoints := rule.GetEndpoints() +// 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 +} + +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() == "" { diff --git a/internal/interceptor/hosts_test.go b/internal/interceptor/hosts_test.go index c36d72d..13a53b7 100644 --- a/internal/interceptor/hosts_test.go +++ b/internal/interceptor/hosts_test.go @@ -41,6 +41,8 @@ func TestNewBindings(t *testing.T) { {"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"}, } if len(got) != len(want) { t.Fatalf("newBindings() returned %d bindings, want %d", len(got), len(want)) @@ -86,6 +88,19 @@ func TestExtractors(t *testing.T) { }, {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: "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}, diff --git a/internal/interceptor/review_fixes_test.go b/internal/interceptor/review_fixes_test.go index 2d57e0a..68673e7 100644 --- a/internal/interceptor/review_fixes_test.go +++ b/internal/interceptor/review_fixes_test.go @@ -178,3 +178,35 @@ func callsWithin(got, want []lookupCall, tol time.Duration) bool { } 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) + } + }) + } +} diff --git a/internal/interceptor/service_test.go b/internal/interceptor/service_test.go index cb7fe9a..c4c5522 100644 --- a/internal/interceptor/service_test.go +++ b/internal/interceptor/service_test.go @@ -73,10 +73,12 @@ func checkManifest(t *testing.T, m *interceptorv1.InterceptorManifest) { 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", + "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", } if len(m.GetBindings()) != len(want) { t.Fatalf("bindings = %v, want %d", m.GetBindings(), len(want)) diff --git a/internal/vetting/review_fixes_test.go b/internal/vetting/review_fixes_test.go index 0c1b299..2fcce58 100644 --- a/internal/vetting/review_fixes_test.go +++ b/internal/vetting/review_fixes_test.go @@ -32,13 +32,27 @@ func TestRefreshFailureKeepsValidVerdict(t *testing.T) { t.Fatalf("initial Lookup = %+v, want registered", v) } h.dns.WithError("agent.example.com", errors.New("servfail")) - h.clock.Advance(9*time.Minute + 30*time.Second) + 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 }) - 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) + 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 { diff --git a/internal/vetting/service.go b/internal/vetting/service.go index 12ddf38..4511c80 100644 --- a/internal/vetting/service.go +++ b/internal/vetting/service.go @@ -302,8 +302,9 @@ func (s *Service) run(ctx context.Context, fqdn models.Fqdn, f *flight) { // 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: a dependency blip during refresh-ahead must -// not turn a registered host into a denial. +// 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() @@ -317,6 +318,7 @@ func (s *Service) store(host string, v Verdict) (Verdict, Status, bool) { 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) From cb7c025ff656f12bdfd03fece74ff52ae42c948d Mon Sep 17 00:00:00 2001 From: kperry Date: Mon, 19 Oct 2026 17:26:26 -0500 Subject: [PATCH 5/5] fix: vet draft approvals, feed the hydrator from supervisors, judge attribution not observations Interceptor: ApproveDraftChunk and ApproveAllDraftChunks are bound. They install stored rules the request does not carry, so the extractors resolve the chunks from the evaluation's current_state (GetDraftPolicyResponse shape, which must carry the chunks field) following the gateway's own rules: pending chunks only, security-flagged ones only when requested. The pinned OpenShell gateway sends no state for these RPCs, so under require_registered an approval whose rules cannot be resolved is refused and under observe it is allowed and annotated draft_unresolved. The gateway example registers both RPCs. Hydrator: the exit rule failed any batch without observations, including correctly attributed batches of process, lifecycle or detection events. The report now counts attributed events; the job fails only when events mapped to no configured agent at all, or when a non-empty input yielded no OCSF events, which is what an export left disabled looks like. A new --window-last flag lets a scheduled run post only the trailing window, and the chart's CronJob uses it. Hydrator feed: the gateway's ocsf_log JSONL carries gateway-origin events only, so the README, example and chart now describe enabling ocsf_json_enabled and collecting the supervisors' OCSF-JSON stderr lines or JSONL files into the volume the CronJob reads. Co-Authored-By: Claude Fable 5.1 Signed-off-by: kperry --- README.md | 39 +++-- cmd/ans-ocsf-hydrator/main.go | 75 ++++++++-- cmd/ans-ocsf-hydrator/main_test.go | 75 ++++++++++ deploy/examples/gateway.toml | 12 +- deploy/examples/hydrator.yaml | 5 +- .../ans-openshell/templates/hydrator.yaml | 13 +- deploy/helm/ans-openshell/values.yaml | 14 +- internal/hydrator/aggregate.go | 2 + internal/hydrator/aggregate_test.go | 8 +- internal/hydrator/run.go | 8 + internal/hydrator/run_test.go | 42 +++++- internal/interceptor/helpers_test.go | 32 +++- internal/interceptor/hosts.go | 138 ++++++++++++++++-- internal/interceptor/hosts_test.go | 56 ++++++- internal/interceptor/review_fixes_test.go | 59 +++++++- internal/interceptor/service.go | 39 ++++- internal/interceptor/service_test.go | 2 + 17 files changed, 553 insertions(+), 66 deletions(-) create mode 100644 cmd/ans-ocsf-hydrator/main_test.go diff --git a/README.md b/README.md index df56c07..f322241 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,18 @@ configured address, `--log-level` (`debug`, `info`, `warn`, `error`), `--log-for ./bin/ans-openshell-interceptor --config deploy/examples/interceptor.yaml ``` -The hydrator is a batch job. It reads OCSF JSONL (or the supervisor's stderr with its -`OCSF-JSON` lines) from `--input` or stdin, aggregates per agent and posts one batch: +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 gateway-ocsf.jsonl +./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 @@ -104,15 +111,23 @@ The reference callee takes flags only: 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 or edited. An operation naming more - than 64 destinations is denied under `require_registered`, so import large profile sets in - batches. 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 gateway's OCSF JSONL to the hydrator ([deploy/examples/hydrator.yaml](deploy/examples/hydrator.yaml)) - on a schedule to feed the Trust Index. The signal ids it emits are configurable; the Trust + 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) diff --git a/cmd/ans-ocsf-hydrator/main.go b/cmd/ans-ocsf-hydrator/main.go index 9266cd8..3f5a422 100644 --- a/cmd/ans-ocsf-hydrator/main.go +++ b/cmd/ans-ocsf-hydrator/main.go @@ -50,13 +50,18 @@ type fileConfig struct { type flags struct { configPath, input, windowStart, windowEnd string + windowLast time.Duration dryRun, allowUnmapped bool } -// errNothingAttributed reports an input whose events none of the configured -// agents claimed: almost always mapping drift, so the job fails rather than -// quietly starving the Trust Index. -var errNothingAttributed = errors.New("events were parsed but none could be attributed to a configured agent (set --allow-unmapped to accept this)") +// 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 { @@ -70,9 +75,10 @@ func run() error { 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 events were parsed but none mapped to an agent") + 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") @@ -109,11 +115,28 @@ func run() error { defer closeInput() report, err := hydrator.Run(ctx, in, sink, opts, logger) - logReport(ctx, logger, report, err) + logReport(ctx, logger, cfg.Agents, report, err) if err != nil { return err } - if report.Parse.Events > 0 && report.Observations == 0 && !f.allowUnmapped { + 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 @@ -138,14 +161,33 @@ func buildOptions(cfg fileConfig, f flags) (hydrator.Options, error) { if opts.Signer, err = batchSigner(cfg); err != nil { return hydrator.Options{}, err } - if f.windowStart != "" || f.windowEnd != "" { + 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 hydrator.Options{}, err + return nil, err } - opts.Window = &w + return &w, nil } - return opts, 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 @@ -243,17 +285,24 @@ func openInput(path string) (io.Reader, func(), error) { return f, func() { _ = f.Close() }, nil } -func logReport(ctx context.Context, logger *slog.Logger, r hydrator.Report, runErr error) { +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, + "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/deploy/examples/gateway.toml b/deploy/examples/gateway.toml index c2ce3a9..f082fda 100644 --- a/deploy/examples/gateway.toml +++ b/deploy/examples/gateway.toml @@ -10,7 +10,9 @@ 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 and custom provider-profile imports even when its destination_policy is "observe". Use "fail_open" where availability +# 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" @@ -46,3 +48,11 @@ 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 index 6a63905..41a1b4e 100644 --- a/deploy/examples/hydrator.yaml +++ b/deploy/examples/hydrator.yaml @@ -1,4 +1,7 @@ -# ans-ocsf-hydrator --config hydrator.yaml --input /var/log/openshell/gateway-ocsf.jsonl +# 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 diff --git a/deploy/helm/ans-openshell/templates/hydrator.yaml b/deploy/helm/ans-openshell/templates/hydrator.yaml index c5571cc..25eb4dd 100644 --- a/deploy/helm/ans-openshell/templates/hydrator.yaml +++ b/deploy/helm/ans-openshell/templates/hydrator.yaml @@ -26,6 +26,9 @@ spec: 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} @@ -33,7 +36,15 @@ spec: - 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 }}] + 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 }}} diff --git a/deploy/helm/ans-openshell/values.yaml b/deploy/helm/ans-openshell/values.yaml index 0bd11d9..a8b057a 100644 --- a/deploy/helm/ans-openshell/values.yaml +++ b/deploy/helm/ans-openshell/values.yaml @@ -70,15 +70,23 @@ interceptor: cache_ttl: 10m exempt_hosts: [] -# The hydrator runs as a CronJob over the gateway's OCSF JSONL. Mount the file with extraVolumes -# and extraVolumeMounts (a hostPath or the gateway's log PVC) and point input at it. +# 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 * * * *" - input: /var/log/openshell/gateway-ocsf.jsonl + 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: [] diff --git a/internal/hydrator/aggregate.go b/internal/hydrator/aggregate.go index 692d93f..f9d0bf7 100644 --- a/internal/hydrator/aggregate.go +++ b/internal/hydrator/aggregate.go @@ -100,6 +100,7 @@ type aggregator struct { unmapped int noContainer int outOfWindow int + attributed int } func newAggregator(agents AgentMap, window *Window) *aggregator { @@ -124,6 +125,7 @@ func (a *aggregator) add(ev Event) { } return } + a.attributed++ st, seen := a.stats[mapping.AgentID] if !seen { st = newAgentStats(mapping) diff --git a/internal/hydrator/aggregate_test.go b/internal/hydrator/aggregate_test.go index f6b1ff3..a775cd0 100644 --- a/internal/hydrator/aggregate_test.go +++ b/internal/hydrator/aggregate_test.go @@ -46,8 +46,8 @@ func TestAggregatorNetworkMath(t *testing.T) { if got := st.rate(); got != 0.6 { t.Fatalf("rate() = %v, want 0.6", got) } - if agg.unmapped != 0 || agg.outOfWindow != 0 { - t.Fatalf("unmapped/outOfWindow = %d/%d", agg.unmapped, agg.outOfWindow) + 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) } } @@ -111,8 +111,8 @@ func TestAggregatorExplicitWindow(t *testing.T) { for _, ms := range []int64{1000, 2000, 4000, 5000, 0} { agg.add(netEvent(ClassNetworkActivity, "sb", false, "", "h", "", ms)) } - if agg.outOfWindow != 3 { - t.Fatalf("outOfWindow = %d, want 3", agg.outOfWindow) + 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) diff --git a/internal/hydrator/run.go b/internal/hydrator/run.go index 9a47bf7..9e5f739 100644 --- a/internal/hydrator/run.go +++ b/internal/hydrator/run.go @@ -48,6 +48,9 @@ type AgentSummary struct { // 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 @@ -80,6 +83,7 @@ func Run(ctx context.Context, in io.Reader, sink Sink, opts Options, logger *slo err := consume(ctx, reader, agg) report := Report{ Parse: reader.Stats(), + Attributed: agg.attributed, Unmapped: agg.unmapped, NoContainer: agg.noContainer, OutOfWindow: agg.outOfWindow, @@ -95,6 +99,10 @@ func Run(ctx context.Context, in io.Reader, sink Sink, opts Options, logger *slo 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 diff --git a/internal/hydrator/run_test.go b/internal/hydrator/run_test.go index a75ed24..313af44 100644 --- a/internal/hydrator/run_test.go +++ b/internal/hydrator/run_test.go @@ -100,6 +100,7 @@ func TestRunEndToEnd(t *testing.T) { 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, @@ -216,15 +217,28 @@ func TestRunWithoutObservations(t *testing.T) { 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}, - Window: Window{Start: fixedClock(), End: fixedClock()}, - Agents: []AgentSummary{{AgentID: "a", Events: 1}}, + Parse: ParseStats{Lines: 1, Events: 1}, + Attributed: 1, + Window: Window{Start: fixedClock(), End: fixedClock()}, + Agents: []AgentSummary{{AgentID: "a", Events: 1}}, }, }, } @@ -254,7 +268,7 @@ func TestRunMappedAgentWithoutObservationsStillSummarized(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - if report.Observations != 0 || report.Posted || len(report.Agents) != 1 || report.Agents[0].Events != 1 || report.Agents[0].Observations != 0 { + 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) } } @@ -269,7 +283,7 @@ func TestRunExplicitWindow(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - if report.OutOfWindow != 4 || report.Unmapped != 0 || report.Window != window || report.Observations != 4 { + 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 { @@ -364,3 +378,21 @@ func TestRunFailures(t *testing.T) { }) } } + +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/interceptor/helpers_test.go b/internal/interceptor/helpers_test.go index d33a931..46b4e71 100644 --- a/internal/interceptor/helpers_test.go +++ b/internal/interceptor/helpers_test.go @@ -26,6 +26,8 @@ const ( methodEdit = "EditDraftChunk" methodImport = "ImportProviderProfiles" methodUpdate = "UpdateProviderProfiles" + methodApprove = "ApproveDraftChunk" + methodApproveAll = "ApproveAllDraftChunks" ) type lookupCall struct { @@ -130,12 +132,17 @@ func editDraftChunk(r *sandboxv1.NetworkPolicyRule) *openshellv1.EditDraftChunkR } 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}}, + Phase: &interceptorv1.InterceptorEvaluation_Validate{Validate: &interceptorv1.ValidateEvaluation{ + ProposedOperation: op, CurrentState: state}}, } } @@ -179,3 +186,26 @@ func updateProviderProfiles(p *openshellv1.ProviderProfile) *openshellv1.UpdateP } 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 index d86d655..248c8cd 100644 --- a/internal/interceptor/hosts.go +++ b/internal/interceptor/hosts.go @@ -14,9 +14,21 @@ import ( "github.com/agentnameservice/ans-openshell/gen/openshell/sandboxv1" ) -// hostExtractor decodes a proposed operation and returns every destination -// host it would admit. -type hostExtractor func(*structpb.Struct) ([]string, error) +// 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 @@ -36,13 +48,15 @@ func newBindings() []binding { 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) { +func updateConfigHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.UpdateConfigRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -62,7 +76,7 @@ func updateConfigHosts(op *structpb.Struct) ([]string, error) { return hosts, nil } -func createSandboxHosts(op *structpb.Struct) ([]string, error) { +func createSandboxHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.CreateSandboxRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -74,7 +88,7 @@ func createSandboxHosts(op *structpb.Struct) ([]string, error) { return hosts, nil } -func submitPolicyAnalysisHosts(op *structpb.Struct) ([]string, error) { +func submitPolicyAnalysisHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.SubmitPolicyAnalysisRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -86,7 +100,7 @@ func submitPolicyAnalysisHosts(op *structpb.Struct) ([]string, error) { return hosts, nil } -func editDraftChunkHosts(op *structpb.Struct) ([]string, error) { +func editDraftChunkHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.EditDraftChunkRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -97,7 +111,7 @@ func editDraftChunkHosts(op *structpb.Struct) ([]string, error) { // 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) { +func importProviderProfilesHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.ImportProviderProfilesRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -109,7 +123,7 @@ func importProviderProfilesHosts(op *structpb.Struct) ([]string, error) { return hosts, nil } -func updateProviderProfilesHosts(op *structpb.Struct) ([]string, error) { +func updateProviderProfilesHosts(op, _ *structpb.Struct) ([]string, error) { var req openshellv1.UpdateProviderProfilesRequest if err := decodeOperation(op, &req); err != nil { return nil, err @@ -117,6 +131,91 @@ func updateProviderProfilesHosts(op *structpb.Struct) ([]string, error) { 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 @@ -134,18 +233,27 @@ func endpointHosts(endpoints []*sandboxv1.NetworkEndpoint) []string { return hosts } -// decodeOperation converts the gateway's protobuf-JSON Struct into msg, +// 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 { - if op == nil { - return errors.New("proposed_operation is required") + 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(op) + raw, err := protojson.Marshal(st) if err != nil { - return fmt.Errorf("encode proposed_operation: %w", err) + return fmt.Errorf("encode %s: %w", what, err) } if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, msg); err != nil { - return fmt.Errorf("decode proposed_operation: %w", err) + return fmt.Errorf("decode %s: %w", what, err) } return nil } diff --git a/internal/interceptor/hosts_test.go b/internal/interceptor/hosts_test.go index 13a53b7..7b4a54c 100644 --- a/internal/interceptor/hosts_test.go +++ b/internal/interceptor/hosts_test.go @@ -1,7 +1,10 @@ package interceptor import ( + "errors" "slices" + "strconv" + "strings" "testing" "google.golang.org/protobuf/types/known/structpb" @@ -43,6 +46,8 @@ func TestNewBindings(t *testing.T) { {"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)) @@ -66,12 +71,20 @@ func TestExtractors(t *testing.T) { 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 - want []string - wantErr bool + 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, @@ -101,6 +114,23 @@ func TestExtractors(t *testing.T) { {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}, @@ -111,10 +141,16 @@ func TestExtractors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := extractor(tt.method)(tt.op) + 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) @@ -122,3 +158,11 @@ func TestExtractors(t *testing.T) { }) } } + +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 index 68673e7..761bc46 100644 --- a/internal/interceptor/review_fixes_test.go +++ b/internal/interceptor/review_fixes_test.go @@ -34,7 +34,7 @@ func TestUpdateConfigHosts(t *testing.T) { {Operation: &openshellv1.PolicyMergeOperation_RemoveRule{RemoveRule: &openshellv1.RemoveNetworkRule{RuleName: "old"}}}, }, } - hosts, err := updateConfigHosts(toStruct(t, req)) + hosts, err := updateConfigHosts(toStruct(t, req), nil) if err != nil { t.Fatalf("updateConfigHosts() error = %v", err) } @@ -42,7 +42,7 @@ func TestUpdateConfigHosts(t *testing.T) { if strings.Join(hosts, ",") != strings.Join(want, ",") { t.Fatalf("updateConfigHosts() = %v, want %v", hosts, want) } - if _, err := updateConfigHosts(nil); err == nil { + if _, err := updateConfigHosts(nil, nil); err == nil { t.Fatal("updateConfigHosts(nil) = nil error") } } @@ -210,3 +210,58 @@ func TestEvaluateProviderProfilesAreVetted(t *testing.T) { }) } } + +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 index 8076124..f93f63a 100644 --- a/internal/interceptor/service.go +++ b/internal/interceptor/service.go @@ -5,6 +5,7 @@ package interceptor import ( "context" + "errors" "fmt" "log/slog" "strconv" @@ -54,6 +55,9 @@ const ( 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. @@ -123,7 +127,8 @@ func (s *Service) Describe(ctx context.Context, req *interceptorv1.DescribeReque // 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. +// 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 { @@ -145,7 +150,10 @@ func (s *Service) Evaluate(ctx context.Context, req *interceptorv1.InterceptorEv if s.cfg.Mode == vetting.ModeOff { return s.skipped(ctx, req, outcomeVettingOff), nil } - hosts, err := b.extract(validate.GetProposedOperation()) + 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) @@ -196,6 +204,33 @@ func (s *Service) skipped(ctx context.Context, req *interceptorv1.InterceptorEva } } +// 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) { diff --git a/internal/interceptor/service_test.go b/internal/interceptor/service_test.go index c4c5522..37e804e 100644 --- a/internal/interceptor/service_test.go +++ b/internal/interceptor/service_test.go @@ -79,6 +79,8 @@ func checkManifest(t *testing.T, m *interceptorv1.InterceptorManifest) { "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))