diff --git a/.github/dependency-license-policy.toml b/.github/dependency-license-policy.toml index 1e65918a..98637bf2 100644 --- a/.github/dependency-license-policy.toml +++ b/.github/dependency-license-policy.toml @@ -17,6 +17,14 @@ source = '{"registry":"https://pypi.org/simple"}' license = "Apache-2.0" evidence = "https://github.com/aio-libs/multidict/blob/v6.9.1/LICENSE" +[[clarification]] +ecosystem = "pypi" +name = "protobuf" +version = "6.33.6" +source = '{"registry":"https://pypi.org/simple"}' +license = "BSD-3-Clause" +evidence = "https://github.com/protocolbuffers/protobuf/blob/v33.6/LICENSE" + [[clarification]] ecosystem = "pypi" name = "reachy-mini" diff --git a/projects/use-case-examples/agent-containment/LICENSE b/projects/use-case-examples/agent-containment/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/projects/use-case-examples/agent-containment/LICENSE @@ -0,0 +1,201 @@ + 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/projects/use-case-examples/agent-containment/README.md b/projects/use-case-examples/agent-containment/README.md new file mode 100644 index 00000000..3fde17dd --- /dev/null +++ b/projects/use-case-examples/agent-containment/README.md @@ -0,0 +1,319 @@ +# Agent containment + +Machine-isolated agents with egress usage monitoring. + +Capable agents probe their own runtime first, then abuse whatever access their +policy allows. This example puts everything that governs an agent on a machine +the agent cannot reach, and watches the traffic its policy already allows. + +- **The control node** runs the OpenShell gateway, every sandbox's + supervisor, and an egress watchdog that enforces byte budgets on allowed + hosts. +- **A sandbox node** runs sandbox workloads and nothing else. Add more + sandbox nodes the same way. + +This deployment pattern is often called *two-box*. The egress usage +monitoring builds on Hugging Face's proof of concept and +[RFC](https://github.com/Hugoch/OpenShell/blob/poc/egress-usage-monitoring/rfc/NNNN-egress-usage-monitoring/poc.md). +It uses released OpenShell (0.1.4) with no changes to OpenShell. + +![Agent containment architecture: the control node runs the gateway, supervisors, and egress watchdog; the sandbox node runs network-isolated sandbox workloads whose only path is a mediated channel to their supervisor on the control node.](docs/architecture.svg) + +> **Status: proof of life on two machines.** Placement, per-request +> accounting, and a byte budget with quarantine work on a DGX Station (control node) +> and a DGX Spark (sandbox node) on the same office network. The boundary has been +> probed from a root shell on the sandbox node. See +> [What has been verified](#what-has-been-verified). + +## How it works + +- **One Kubernetes (k3s) cluster.** The control node is the k3s server; each sandbox node + joins as a node. +- **Sandbox nodes accept only sandbox workloads.** `sandbox-node.sh` marks the + node so Kubernetes schedules nothing there unless it explicitly opts in (a + Kubernetes [taint](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)). + The `sandboxes` sandbox template opts sandbox workloads in and sends them + there. +- **Supervisors stay on the control node.** OpenShell runs each sandbox's supervisor as a + separate pod, and those pods never opt in, so they can only run on the control node. So + does the gateway, which the Helm chart pins there. +- **The workload's only path is its supervisor.** OpenShell's own network + policy gives the workload pod no egress and accepts only its supervisor's + connection. +- **Sandbox nodes cannot reach the controls.** Every Kubernetes node can + normally reach every pod and service address. Network policies on the + gateway and the watchdog admit only supervisors and the control node itself, + so even a sandbox node's host cannot open a connection to either. Run the + operator CLI on the control node. +- **Sandbox nodes hold little.** They join with a join-only token generated by + `control-node.sh`. By default k3s would hand them the server token, which can + fetch the cluster's CA keys. + +## Layout + +| Path | Purpose | +|---|---| +| `deploy/versions.env` | The only place versions live. Every script sources it. | +| `deploy/control-node.sh` | The control node: k3s server, Agent Sandbox, OpenShell chart, CLI, `sandboxes` template. Prints the command for each sandbox node. | +| `deploy/sandbox-node.sh TOKEN CONTROL_IP` | Each sandbox node: joins the cluster, accepting only sandbox workloads. Nothing else. | +| `deploy/watchdog.sh` | Mints watchdog TLS, runs the watchdog as a pod on the control node, registers it with the gateway. `--remove` undoes it. | +| `deploy/gateway-policy.yaml` | Network policy that admits only supervisors and the control node itself to the gateway. `control-node.sh` applies it. | +| `deploy/watchdog.yaml` | The watchdog pod, its in-cluster service, and a network policy that admits only the gateway and supervisors. | +| `deploy/verify.sh` | The definition of "it works". Exits non-zero on failure. | +| `deploy/rehearse-k3d.sh` | Development only: both nodes as containers on one Docker host. | +| `deploy/demo-policy.yaml`, `deploy/watched-policy.yaml` | Sandbox policies: curl may reach httpbin.org; the second attaches the watchdog. | +| `docs/architecture.svg` | The architecture diagram above. | +| `src/egress_watchdog/` | The watchdog: `server.py` (gRPC, TLS, JWT), `ledger.py` (accounting, budget, quarantine). | +| `proto/` | `supervisor_middleware.proto` and `extension.proto` from OpenShell v0.1.4. | + +## Run on two machines + +Two Ubuntu 24.04 machines (VMs, cloud instances, workstations, Sparks, or Pis) +with `curl` and `sudo`. No GPU or +Docker is needed. The boxes must reach each other on TCP 6443 and 10250 and +UDP 8472 at the addresses you choose. A Tailscale or other WireGuard overlay +works and avoids opening a host firewall: pick each node's overlay address. + +On the control node, from this directory: + +```bash +CONTROL_IP= deploy/control-node.sh +``` + +It prints a `deploy/sandbox-node.sh TOKEN CONTROL_IP` command. Run it on each +sandbox node, with the address the control node should use for that box: + +```bash +NODE_IP= deploy/sandbox-node.sh +``` + +Then on the control node: + +```bash +deploy/watchdog.sh +deploy/verify.sh +``` + +`deploy/control-node.sh --reset` and `deploy/sandbox-node.sh --reset` uninstall +k3s. + +Both scripts pin the cluster network (flannel) to the interface that holds the +chosen address. Without that, flannel tunnels over the default-route interface +even when `--node-ip` names another one, and cross-node pod traffic silently +goes to the wrong network. + +## Stand in for a sandbox node until you have one + +With `control-node.sh` installed for real, a k3s node in a privileged container +on the control node can join as a sandbox node. It exercises the real install and +cross-node traffic, but shares the control node's kernel. + +```bash +source deploy/versions.env +docker run -d --name twobox-standin-sandbox-node --privileged --restart unless-stopped \ + --tmpfs /run --tmpfs /var/run \ + -e K3S_URL="https://${CONTROL_IP}:6443" \ + -e K3S_TOKEN="$(sudo cat /var/lib/rancher/k3s/server/agent-token)" \ + "rancher/k3s:${K3S_VERSION/+/-}" agent \ + --node-label "${SANDBOX_LABEL}" --node-taint "${SANDBOX_TAINT}" +``` + +Remove it with `docker rm -f twobox-standin-sandbox-node` and +`kubectl delete node twobox-standin-sandbox-node`. + +## Rehearse on one Docker host + +Without sudo or a second machine, `deploy/rehearse-k3d.sh` creates both nodes +as containers, then runs `control-node.sh --existing-cluster` on them. It needs +Docker and [k3d](https://k3d.io) v5, on Linux or on an Apple silicon Mac with +Docker Desktop. It shares one kernel, so it proves scheduling and the +watchdog, not the physical boundary. It leaves your default kubeconfig alone; +`deploy/rehearse-k3d.sh --reset` deletes the cluster and its state. + +The operator CLI here runs beside the cluster rather than on the control node, +so the script sets `OPERATOR_CIDR` to let it through the gateway's network +policy. Set the same variable for `control-node.sh` if you operate a real +cluster from another machine. + +```bash +deploy/rehearse-k3d.sh +deploy/watchdog.sh +deploy/verify.sh +``` + +Expected output: + +```text +PASS Control node is Ready +PASS Sandbox node is Ready +PASS Sandbox node accepts only sandbox workloads +PASS Gateway runs on the control node +PASS Sandbox workload runs on the sandbox node +PASS Its supervisor runs on the control node +PASS Allowed request (curl to httpbin.org) succeeds +PASS Unlisted host (example.com) is refused +PASS Watchdog accounted for the request +All checks passed +``` + +## The watchdog + +A Python gRPC service that implements OpenShell's supervisor middleware +contract for HTTP requests in the pre-credentials phase. It runs as a pod on +the control node: a pinned, stock `uv` Python image runs this checkout, so +there is no image to build. Its first start installs dependencies from PyPI. +Read its log with `kubectl -n openshell logs deployment/egress-watchdog`. + +- **Reachable only by the gateway and supervisors.** A network policy drops + everything else, including traffic from sandbox nodes and their hosts. + +- **Authenticated from the first run.** TLS with a private CA that the gateway + trusts through `tls_ca_cert_path`, and a gateway-signed Ed25519 JWT on every + call. The watchdog checks `typ`, the pinned algorithm, issuer, audience, and + expiry. For request evaluations, it also checks that the token's `sandbox_id` + matches the request. There is no insecure mode. +- **Accounting.** One JSON line per request: sandbox, host, method, path, + request bytes, rolling-minute bytes, and decision. Bodies and queries are + never logged. +- **Budget.** `bytes_per_minute` per sandbox per host, configured in the + sandbox policy's middleware `config`: + + ```yaml + network_middlewares: + egress-watchdog: + middleware: egress-watchdog + on_error: fail_closed + config: + budget: + bytes_per_minute: 1048576 # on_exceed: deny (default) or finding + endpoints: + include: ["httpbin.org"] + ``` + +- **Stage-one quarantine.** When a deny budget trips, the watchdog refuses + every later request from that sandbox on the hosts it watches, in + milliseconds and with no gateway call. + +Develop and test: + +```bash +uv sync --locked +uv run ruff format --check . +uv run ruff check . +uv run pytest +``` + +After refreshing `proto/` from a new OpenShell release, run +`scripts/generate-bindings.sh`. + +## What has been verified + +### Two machines, 2026-10-15 + +The control node: DGX Station (arm64, Ubuntu 24.04.3, kernel 6.14 64k pages, UFW on, +an unrelated package-installed OpenShell gateway and Docker containers already +running). The sandbox node: DGX Spark (arm64, Ubuntu 24.04.4, kernel 6.17). Both on the +same office network, about 3 ms apart; k3s traffic runs over a Tailscale +overlay that takes the direct LAN path. OpenShell 0.1.4. + +- **From zero:** `control-node.sh` 42 s, `sandbox-node.sh` 13 s. +- **`verify.sh`:** all nine checks pass. The first sandbox took about two and + a half minutes, mostly pulling the 1 GB workload image onto the sandbox node. +- **Exfil by volume:** 64 KiB POSTs get sixteen 200s (exactly 1 MiB), then 403s + (`egress_budget_exceeded`, then `sandbox_quarantined`), and a follow-up GET + is refused. +- **Added latency** against httpbin.org, curl `time_starttransfer`, 60 requests + each: p50 287 ms unwatched vs 288 ms watched; p90 298 ms vs 301 ms. The + watchdog call stays on the control node (supervisor to watchdog), so the link between + the boxes is in both numbers equally. Directional, not a benchmark. +- **Boundary, from a root shell on the sandbox node host:** + + | Probe | Result | + |---|---| + | The sandbox node's node credential reads the gateway JWT keys, gateway or client TLS, the credential-encryption key, or the watchdog CA | Forbidden | + | Node credential lists secrets, or execs into a supervisor pod | Forbidden | + | Node credential reads the bootstrap secret of a workload on the sandbox node | Allowed (expected: that is the workload's own identity) | + | The sandbox node's k3s join token fetches the server bootstrap data (cluster CA keys) | 403: The sandbox node holds a join-only token | + | Gateway from the sandbox node host: its own NodePort, the control node's NodePort, pod IP, service IP | No connection (network policy) | + | Gateway API without a client certificate (before the network policy) | Refused: `missing authorization header` | + | k3s API on the control node, no credentials | 401 | + | Kubelet on the control node: no credentials / the sandbox node's node certificate | 401 / 403 | + | Watchdog from the sandbox node host: pod IP, service IP | No connection (network policy) | + | Watchdog with no token / a forged token (earlier host-process watchdog) | TLS connects; `UNAUTHENTICATED` | + | Supervisor pod ports 5500 and 5501 on the control node | Closed | + +- **From inside the workload pod** (uid 10001; same probes on the stand-in): + direct connections to the internet, allowed hosts, the watchdog, the gateway, + and the Kubernetes API are all refused, and no service-account token is + mounted. + +### Rehearsals + +- `control-node.sh` on a Spark with a containerized stand-in the sandbox node, and the k3d + rehearsal: all nine checks pass on both. +- 22 unit tests: budget parsing, the rolling window, quarantine, and token + rejection (missing, wrong audience, issuer, `typ`, expiry, caller, foreign + key, mismatched sandbox). + +Not verified yet: + +- k3s over a plain LAN between two hosts (the Station's UFW drops k3s ports on + its LAN interface; we used Tailscale rather than open them). +- That impersonating the workload's boundary identity buys only that sandbox's + policy. The sandbox node's host can read every local workload's bootstrap secret, so + root on the sandbox node owns every sandbox scheduled there. +- x86_64 and Ubuntu 22.04. + +## Findings so far + +- **`openshell gateway add --local` can import the wrong certificates.** It + reads its client bundle from `OPENSHELL_LOCAL_TLS_DIR`, or else from a + package-installed gateway's `~/.local/state/openshell/tls`. On a machine that + already runs OpenShell, it silently replaced this cluster's bundle with the + other gateway's, and the CLI failed with `BadSignature`. `control-node.sh` now sets + `OPENSHELL_LOCAL_TLS_DIR` to this cluster's bundle. +- **OpenShell ships no network policy for the gateway.** Its NodePort opens on + every node, and any node can reach in-cluster service and pod addresses, so a + ClusterIP service would not help. Without `deploy/gateway-policy.yaml`, the + gateway's client certificate requirement is the only thing refusing a + sandbox node's host. An optional gateway ingress policy in the chart, with + configurable sources for remote operators and ingress controllers, would + give every multi-node deployment this layer. +- **Ed25519 TLS certificates break some gRPC clients.** Python gRPC could not + complete a handshake with an Ed25519 watchdog certificate; the watchdog now + uses ECDSA P-256. Gateway JWTs stay Ed25519. + +- **`omm create` fails on OpenShell v0.1.4.** `supervisor_middleware.proto` + now imports `extension.proto`, which `omm` does not download. The template + also lacks the `extension` metadata and `expected_audience` that 0.1.x + `Describe` needs. The watchdog vendors both protos by hand instead. +- **Supervisor placement is implicit.** Supervisor pods have no node selector, + toleration, or affinity setting. Keeping them off sandbox nodes relies + entirely on those boxes refusing other workloads, so any other node can host + supervisors. A first-class supervisor placement + setting would make this explicit. +- **Middleware sees no originating binary in Kubernetes mode.** `binary` is + empty in every evaluation, although the supervisor enforces the policy's + binary rule. Novelty-by-binary needs this field. +- **Bodies over `max_payload_bytes` skip middleware.** The supervisor applies + the entry's `on_error` instead of calling the service. With `fail_open`, + large uploads go unaccounted; with `fail_closed`, 4 MiB becomes a + per-request ceiling on watched hosts. A body-free accounting binding + (headers plus a byte count) would remove the trade-off. +- **Middleware sees only what it is attached to.** Stage-one quarantine covers + the watched hosts. Freezing the sandbox's whole network is stage two, through + the gateway (policy replacement), which is not built yet. +- **The supervisor policy poll interval is not settable through Helm.** + `OPENSHELL_POLICY_POLL_INTERVAL_SECS` (default 10) is not exposed by the + driver, so stage two's latency is bounded by that default. + +## Limitations + +- The watchdog keeps state in memory; restarting it forgets counts and + quarantines. +- The watchdog pod mounts the checkout on the control node and runs as its + owner. Run `deploy/watchdog.sh` from the checkout you want it to serve. +- The template's workload image is pulled by tag and should be pinned by digest + before publishing. +- Request bytes come from `Content-Length` or the received body, so chunked + uploads without a length are undercounted. Response bytes are not counted. diff --git a/projects/use-case-examples/agent-containment/deploy/control-node.sh b/projects/use-case-examples/agent-containment/deploy/control-node.sh new file mode 100755 index 00000000..90168c7f --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/control-node.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# The control node: k3s server, Agent Sandbox, the OpenShell gateway, the +# operator CLI, and the `sandboxes` sandbox template. Prints the command to run +# on each sandbox node. +# +# Usage: +# deploy/control-node.sh install k3s and everything above +# deploy/control-node.sh --existing-cluster skip k3s and use $KUBECONFIG as is +# deploy/control-node.sh --reset uninstall k3s and remove operator state +# +# Environment: +# CONTROL_IP address sandbox nodes use to reach this node (default: first host IP) +# TWOBOX_HOME operator state: CLI, kubeconfig, gateway config (default: ~/.twobox) +# OPERATOR_CIDR extra source allowed to reach the gateway, when the operator +# CLI does not run on the control node itself (default: none) + +set -euo pipefail + +deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=versions.env +source "${deploy_dir}/versions.env" + +TWOBOX_HOME="${TWOBOX_HOME:-${HOME}/.twobox}" +CONTROL_IP="${CONTROL_IP:-$(hostname -I | awk '{print $1}')}" +GATEWAY_NAME=twobox +JOIN_TOKEN_FILE=/etc/rancher/k3s/twobox-join-token + +main() { + case "${1:-}" in + --reset) reset; return ;; + --existing-cluster) use_existing_cluster ;; + "") install_k3s_server ;; + *) echo "usage: $0 [--existing-cluster|--reset]" >&2; exit 2 ;; + esac + + install_helm + install_openshell_cli + install_agent_sandbox + install_gateway + register_gateway + create_sandbox_template + write_operator_env + print_next_steps "${1:-}" +} + +install_k3s_server() { + echo "==> k3s ${K3S_VERSION} server on ${CONTROL_IP}" + # Sandbox nodes join with a join-only token. Without one, k3s hands them the + # server token, which can fetch the cluster's bootstrap data and CA keys. + sudo mkdir -p /etc/rancher/k3s + if ! sudo test -s "${JOIN_TOKEN_FILE}"; then + openssl rand -hex 32 | sudo tee "${JOIN_TOKEN_FILE}" >/dev/null + sudo chmod 600 "${JOIN_TOKEN_FILE}" + fi + curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="${K3S_VERSION}" sh -s - server \ + --disable=traefik \ + --agent-token-file "${JOIN_TOKEN_FILE}" \ + --node-ip "${CONTROL_IP}" \ + --flannel-iface "$(interface_of "${CONTROL_IP}")" \ + --tls-san "${CONTROL_IP}" \ + --node-label "${CONTROL_LABEL}" + mkdir -p "${TWOBOX_HOME}" + sudo cat /etc/rancher/k3s/k3s.yaml >"${TWOBOX_HOME}/kubeconfig" + chmod 600 "${TWOBOX_HOME}/kubeconfig" + export KUBECONFIG="${TWOBOX_HOME}/kubeconfig" + # The node object appears a few seconds after the service starts. + for _ in $(seq 1 60); do + kubectl get nodes -o name 2>/dev/null | grep -q . && break + sleep 2 + done + kubectl wait --for=condition=Ready node --all --timeout=120s +} + +use_existing_cluster() { + : "${KUBECONFIG:?--existing-cluster needs KUBECONFIG set}" + command -v kubectl >/dev/null || { echo "kubectl is required" >&2; exit 1; } + if ! kubectl get nodes -l "${CONTROL_LABEL}" -o name | grep -q .; then + echo "no node labelled ${CONTROL_LABEL} in this cluster" >&2 + exit 1 + fi +} + +install_helm() { + mkdir -p "${TWOBOX_HOME}/bin" + export PATH="${TWOBOX_HOME}/bin:${PATH}" + if helm version --short 2>/dev/null | grep -q "^${HELM_VERSION}"; then return; fi + echo "==> helm ${HELM_VERSION}" + local platform tmp + platform="$(uname -s | tr '[:upper:]' '[:lower:]')-$(go_arch)" + tmp="$(mktemp -d)" + curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-${platform}.tar.gz" | tar -xz -C "${tmp}" + install -m 0755 "${tmp}/${platform}/helm" "${TWOBOX_HOME}/bin/helm" + rm -rf "${tmp}" +} + +install_openshell_cli() { + if openshell --version 2>/dev/null | grep -qx "openshell ${OPENSHELL_VERSION}"; then return; fi + echo "==> openshell CLI ${OPENSHELL_VERSION}" + local asset tmp base + case "$(uname -s)-$(go_arch)" in + Linux-amd64) asset=openshell-x86_64-unknown-linux-musl.tar.gz ;; + Linux-arm64) asset=openshell-aarch64-unknown-linux-musl.tar.gz ;; + # macOS runs only the single-host rehearsal (rehearse-k3d.sh). + Darwin-arm64) asset=openshell-aarch64-apple-darwin.tar.gz ;; + *) echo "no OpenShell CLI build for $(uname -s) $(uname -m)" >&2; exit 1 ;; + esac + base="https://github.com/NVIDIA/OpenShell/releases/download/v${OPENSHELL_VERSION}" + tmp="$(mktemp -d)" + curl -fsSL -o "${tmp}/${asset}" "${base}/${asset}" + curl -fsSL -o "${tmp}/checksums.txt" "${base}/openshell-checksums-sha256.txt" + (cd "${tmp}" && grep " ${asset}\$" checksums.txt | shasum -a 256 -c --quiet) + tar -xzf "${tmp}/${asset}" -C "${TWOBOX_HOME}/bin" openshell + rm -rf "${tmp}" +} + +install_agent_sandbox() { + echo "==> Agent Sandbox ${AGENT_SANDBOX_VERSION}" + kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${AGENT_SANDBOX_VERSION}/sandbox.yaml" >/dev/null + kubectl -n agent-sandbox-system rollout status deploy/agent-sandbox-controller --timeout=180s +} + +install_gateway() { + echo "==> OpenShell gateway ${OPENSHELL_VERSION} (chart)" + helm upgrade --install openshell oci://ghcr.io/nvidia/openshell/helm-chart \ + --version "${OPENSHELL_VERSION}" \ + --namespace openshell --create-namespace \ + --values "${deploy_dir}/values.yaml" \ + --set "service.nodePort=${GATEWAY_NODE_PORT}" \ + --set "pkiInitJob.serverIpAddresses={${CONTROL_IP}}" \ + --wait --timeout 10m + # Only supervisors and the control node itself may reach the gateway. + kubectl apply -f "${deploy_dir}/gateway-policy.yaml" >/dev/null + if [[ -n "${OPERATOR_CIDR:-}" ]]; then + kubectl -n openshell patch networkpolicy openshell-gateway --type=json -p \ + "[{\"op\":\"add\",\"path\":\"/spec/ingress/0/from/-\",\"value\":{\"ipBlock\":{\"cidr\":\"${OPERATOR_CIDR}\"}}}]" >/dev/null + fi +} + +register_gateway() { + export XDG_CONFIG_HOME="${TWOBOX_HOME}/config" + # `gateway add --local` imports its client bundle from OPENSHELL_LOCAL_TLS_DIR, + # or else from a package-installed gateway under ~/.local/state/openshell/tls. + # Point it at this cluster's bundle so an existing local gateway's is never used. + local tls="${TWOBOX_HOME}/tls" + mkdir -p "${tls}/client" + client_tls_file ca.crt >"${tls}/ca.crt" + client_tls_file tls.crt >"${tls}/client/tls.crt" + client_tls_file tls.key >"${tls}/client/tls.key" + chmod 600 "${tls}/client/tls.key" + rm -rf "${XDG_CONFIG_HOME}/openshell/gateways/${GATEWAY_NAME}" + OPENSHELL_LOCAL_TLS_DIR="${tls}" \ + openshell gateway add "https://127.0.0.1:${GATEWAY_NODE_PORT}" --local --name "${GATEWAY_NAME}" + openshell gateway select "${GATEWAY_NAME}" >/dev/null 2>&1 || true + openshell status +} + +create_sandbox_template() { + local key="${SANDBOX_TAINT%%=*}" rest="${SANDBOX_TAINT#*=}" + local value="${rest%%:*}" effect="${rest#*:}" + local driver_config + driver_config="$(printf '{"kubernetes":{"pod":{"node_selector":{"%s":"%s"},"tolerations":[{"key":"%s","operator":"Equal","value":"%s","effect":"%s"}]}}}' \ + "${SANDBOX_LABEL%%=*}" "${SANDBOX_LABEL#*=}" "${key}" "${value}" "${effect}")" + openshell sandbox template delete sandboxes >/dev/null 2>&1 || true + openshell sandbox template create sandboxes --image "${SANDBOX_IMAGE}" --driver-config-json "${driver_config}" +} + +write_operator_env() { + cat >"${TWOBOX_HOME}/env" <&2 + exit 1 + fi + echo "${interface}" +} + +client_tls_file() { + kubectl -n openshell get secret openshell-client-tls -o jsonpath="{.data.${1//./\\.}}" | base64 -d +} + +go_arch() { + case "$(uname -m)" in + x86_64) echo amd64 ;; + aarch64 | arm64) echo arm64 ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; + esac +} + +main "$@" diff --git a/projects/use-case-examples/agent-containment/deploy/demo-policy.yaml b/projects/use-case-examples/agent-containment/deploy/demo-policy.yaml new file mode 100644 index 00000000..e43fd02f --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/demo-policy.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Sandbox policy for verify.sh and the scenarios: curl may read and write +# httpbin.org, and nothing else leaves the sandbox. +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + httpbin: + name: httpbin + endpoints: + - host: httpbin.org + port: 443 + protocol: rest + access: read-write + binaries: + - path: /usr/bin/curl diff --git a/projects/use-case-examples/agent-containment/deploy/gateway-policy.yaml b/projects/use-case-examples/agent-containment/deploy/gateway-policy.yaml new file mode 100644 index 00000000..7c54457f --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/gateway-policy.yaml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Only sandbox supervisors and other gateway replicas may call the gateway pod, +# plus the control node itself (the operator CLI and kubelet probes). Every +# node can otherwise reach in-cluster addresses and NodePorts, so without this +# policy a sandbox node's host could open connections to the gateway. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: openshell-gateway + namespace: openshell +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + policyTypes: [Ingress] + ingress: + - from: + - podSelector: + matchLabels: + openshell.ai/boundary-role: supervisor + - podSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell diff --git a/projects/use-case-examples/agent-containment/deploy/rehearse-k3d.sh b/projects/use-case-examples/agent-containment/deploy/rehearse-k3d.sh new file mode 100755 index 00000000..9a1c6ce5 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/rehearse-k3d.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Development rehearsal only: two k3s nodes as containers on one Docker host, +# with the same labels and taint control-node.sh and sandbox-node.sh apply, then +# control-node.sh --existing-cluster on top. It exercises scheduling, the chart, +# the template, and the watchdog without sudo or a second machine. It shares one +# kernel, so it proves nothing about the physical boundary; use control-node.sh +# and sandbox-node.sh for that. +# +# Usage: +# deploy/rehearse-k3d.sh create the cluster and run control-node.sh on it +# deploy/rehearse-k3d.sh --reset delete the cluster and operator state +# +# Requires Docker and k3d (https://k3d.io) on PATH. + +set -euo pipefail + +deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# The watchdog pod mounts this checkout, so the control node container needs it. +project_dir="$(dirname "${deploy_dir}")" +# shellcheck source=versions.env +source "${deploy_dir}/versions.env" + +CLUSTER=twobox +export TWOBOX_HOME="${TWOBOX_HOME:-${HOME}/.twobox}" + +if [[ "${1:-}" == "--reset" ]]; then + k3d cluster delete "${CLUSTER}" + rm -rf "${TWOBOX_HOME}" + exit 0 +fi + +# k3d reads '@' as a node filter, so a literal '@' in the path is escaped. +if ! k3d cluster list "${CLUSTER}" >/dev/null 2>&1; then + k3d cluster create "${CLUSTER}" \ + --image "rancher/k3s:${K3S_VERSION/+/-}" \ + --servers 1 --agents 1 --no-lb \ + --api-port 127.0.0.1:6550 \ + --port "127.0.0.1:${GATEWAY_NODE_PORT}:${GATEWAY_NODE_PORT}@server:0:direct" \ + --k3s-arg "--disable=traefik@server:0" \ + --volume "${project_dir//@/\\@}:${project_dir//@/\\@}@server:0" \ + --k3s-node-label "${CONTROL_LABEL}@server:0" \ + --k3s-node-label "${SANDBOX_LABEL}@agent:0" \ + --k3s-arg "--node-taint=${SANDBOX_TAINT}@agent:0" \ + --kubeconfig-update-default=false --kubeconfig-switch-context=false \ + --wait --timeout 180s +fi + +mkdir -p "${TWOBOX_HOME}" +k3d kubeconfig get "${CLUSTER}" >"${TWOBOX_HOME}/kubeconfig" +chmod 600 "${TWOBOX_HOME}/kubeconfig" +export KUBECONFIG="${TWOBOX_HOME}/kubeconfig" +# The operator CLI on this host reaches the gateway through Docker's network +# gateway, not as the control node itself, so the gateway policy admits it. +operator_ip="$(docker network inspect "k3d-${CLUSTER}" -f '{{(index .IPAM.Config 0).Gateway}}')" +CONTROL_IP=127.0.0.1 OPERATOR_CIDR="${operator_ip}/32" "${deploy_dir}/control-node.sh" --existing-cluster diff --git a/projects/use-case-examples/agent-containment/deploy/sandbox-node.sh b/projects/use-case-examples/agent-containment/deploy/sandbox-node.sh new file mode 100755 index 00000000..b92ef992 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/sandbox-node.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# A sandbox node (the sandbox node, and any further boxes): a k3s agent node marked so that +# only sandbox workloads run on it. Nothing else is installed here: no gateway, +# no supervisor, no credentials, no CLI. Run it on each sandbox node. +# +# Usage: +# deploy/sandbox-node.sh TOKEN CONTROL_IP join the cluster the control node created +# deploy/sandbox-node.sh --reset uninstall the k3s agent +# +# Environment: +# NODE_IP address the control node uses to reach this node (default: first host IP) + +set -euo pipefail + +deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=versions.env +source "${deploy_dir}/versions.env" + +if [[ "${1:-}" == "--reset" ]]; then + if [[ -x /usr/local/bin/k3s-agent-uninstall.sh ]]; then sudo /usr/local/bin/k3s-agent-uninstall.sh; fi + exit 0 +fi +if [[ $# -ne 2 ]]; then + echo "usage: $0 TOKEN CONTROL_IP | --reset" >&2 + exit 2 +fi + +NODE_IP="${NODE_IP:-$(hostname -I | awk '{print $1}')}" +# Flannel tunnels over the default-route interface unless told otherwise, even +# when --node-ip names another one, so pin it to the interface holding NODE_IP. +NODE_INTERFACE="$(ip -o -4 addr show | awk -v ip="${NODE_IP}" '$4 ~ "^" ip "/" {print $2; exit}')" +if [[ -z "${NODE_INTERFACE}" ]]; then + echo "no local interface has address ${NODE_IP}" >&2 + exit 1 +fi + +echo "==> k3s ${K3S_VERSION} agent on ${NODE_IP}, joining ${2}" +curl -sfL https://get.k3s.io | \ + INSTALL_K3S_VERSION="${K3S_VERSION}" K3S_URL="https://${2}:6443" K3S_TOKEN="${1}" \ + sh -s - agent \ + --node-ip "${NODE_IP}" \ + --flannel-iface "${NODE_INTERFACE}" \ + --node-label "${SANDBOX_LABEL}" \ + --node-taint "${SANDBOX_TAINT}" + +echo "Sandbox node joined. Run deploy/verify.sh on the control node." diff --git a/projects/use-case-examples/agent-containment/deploy/values.yaml b/projects/use-case-examples/agent-containment/deploy/values.yaml new file mode 100644 index 00000000..0cf45a27 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/values.yaml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenShell Helm values for the two-box deployment. +# +# The gateway runs on the control node. Supervisor pods have no node selector +# in OpenShell 0.1.4; they stay on the control node because sandbox nodes are +# tainted and supervisors carry no toleration. Sandbox workload pods +# reach sandbox nodes through the `sandboxes` template, which sets a node +# selector and toleration with driver config. +# control-node.sh sets the NodePort and the gateway certificate IP from versions.env. + +nodeSelector: + openshell.io/role: control + +service: + type: NodePort + +server: + drivers: + kubernetes: + # Lets the `sandboxes` template carry pod.node_selector and pod.tolerations. + allowDriverConfig: true diff --git a/projects/use-case-examples/agent-containment/deploy/verify.sh b/projects/use-case-examples/agent-containment/deploy/verify.sh new file mode 100755 index 00000000..239d16c1 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/verify.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# The only definition of "it works". Run on the control node after +# sandbox-node.sh has joined the sandbox node. +# Creates one sandbox from the sandboxes template, checks where each pod landed, +# and sends one allowed and one disallowed request. Exits non-zero on failure. +# +# Usage: deploy/verify.sh [--keep] --keep leaves the verify sandbox running + +set -euo pipefail + +deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=versions.env +source "${deploy_dir}/versions.env" +# shellcheck disable=SC1091 +source "${TWOBOX_HOME:-${HOME}/.twobox}/env" + +SANDBOX=twobox-verify +failures=0 +policy="${deploy_dir}/demo-policy.yaml" +if kubectl -n openshell get deployment egress-watchdog >/dev/null 2>&1; then + policy="${deploy_dir}/watched-policy.yaml" +fi + +check() { + local description="$1" + shift + if "$@"; then + echo "PASS ${description}" + else + echo "FAIL ${description}" + failures=$((failures + 1)) + fi +} + +role_of_node() { + kubectl get node "$1" -o jsonpath='{.metadata.labels.openshell\.io/role}' +} + +role_of_pod() { + role_of_node "$(kubectl -n openshell get pod "$1" -o jsonpath='{.spec.nodeName}')" +} + +node_ready_with_label() { + kubectl get nodes -l "$1" \ + -o jsonpath='{range .items[*]}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' | + grep -qx True +} + +sandbox_node_reserved() { + kubectl get nodes -l "${SANDBOX_LABEL}" -o jsonpath='{.items[*].spec.taints[*].key}' | + grep -qw "${SANDBOX_TAINT%%=*}" +} + +watchdog_logged() { + kubectl -n openshell logs deployment/egress-watchdog --tail=200 | + grep -q "\"sandbox\": \"$1\", .*\"host\": \"httpbin.org\"" +} + +sandbox_curl() { + openshell sandbox exec -n "${SANDBOX}" --no-tty -- \ + curl -sS -m 10 -o /dev/null -w '%{http_code}' "$1" 2>/dev/null || true +} + +check "Control node is Ready" node_ready_with_label "${CONTROL_LABEL}" +check "Sandbox node is Ready" node_ready_with_label "${SANDBOX_LABEL}" +check "Sandbox node accepts only sandbox workloads" sandbox_node_reserved + +if ! openshell sandbox get "${SANDBOX}" >/dev/null 2>&1; then + openshell sandbox create --template sandboxes --name "${SANDBOX}" \ + --policy "${policy}" --detach --no-tty -- sleep infinity >/dev/null +fi + +workload_pod="default--${SANDBOX}" +kubectl -n openshell wait --for=condition=Ready "pod/${workload_pod}" --timeout=300s >/dev/null +pair="$(kubectl -n openshell get pod "${workload_pod}" -o jsonpath='{.metadata.labels.openshell\.ai/boundary-pair}')" +supervisor_pod="$(kubectl -n openshell get pod \ + -l "openshell.ai/boundary-pair=${pair},openshell.ai/boundary-role=supervisor" -o name)" +supervisor_pod="${supervisor_pod#pod/}" + +check "Gateway runs on the control node" test "$(role_of_pod openshell-0)" = control +check "Sandbox workload runs on the sandbox node" test "$(role_of_pod "${workload_pod}")" = sandboxes +check "Its supervisor runs on the control node" test "$(role_of_pod "${supervisor_pod}")" = control +check "Allowed request (curl to httpbin.org) succeeds" test "$(sandbox_curl https://httpbin.org/get)" = 200 +check "Unlisted host (example.com) is refused" test "$(sandbox_curl https://example.com/)" = 000 +if [[ "${policy}" == *watched-policy.yaml ]]; then + check "Watchdog accounted for the request" watchdog_logged "${SANDBOX}" +fi + +if [[ "${1:-}" != "--keep" ]]; then + openshell sandbox delete "${SANDBOX}" >/dev/null +fi + +if ((failures > 0)); then + echo "${failures} check(s) failed" + exit 1 +fi +echo "All checks passed" diff --git a/projects/use-case-examples/agent-containment/deploy/versions.env b/projects/use-case-examples/agent-containment/deploy/versions.env new file mode 100644 index 00000000..6eed60b4 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/versions.env @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# The only place versions live. Every deploy script sources this file. +K3S_VERSION=v1.31.5+k3s1 +HELM_VERSION=v3.17.3 +AGENT_SANDBOX_VERSION=v1.0.3 +OPENSHELL_VERSION=0.1.4 +# Workload image for the sandboxes template: curl, Python, and coding agents. +# TODO: pin by digest before publishing. +SANDBOX_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest + +# Stock uv Python image the watchdog pod runs this checkout with (multi-arch). +WATCHDOG_IMAGE=ghcr.io/astral-sh/uv:0.9.30-python3.12-bookworm-slim@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 + +# Node labels for the two roles, and the taint that keeps everything but +# sandbox workloads off sandbox nodes. +CONTROL_LABEL=openshell.io/role=control +SANDBOX_LABEL=openshell.io/role=sandboxes +SANDBOX_TAINT=openshell.io/sandboxes-only=true:NoSchedule + +# Gateway NodePort on the control node. The operator CLI there reaches it on loopback. +GATEWAY_NODE_PORT=30080 diff --git a/projects/use-case-examples/agent-containment/deploy/watchdog.sh b/projects/use-case-examples/agent-containment/deploy/watchdog.sh new file mode 100755 index 00000000..9fc44f59 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/watchdog.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Run the egress watchdog as a pod on the control node and register it with the +# gateway as supervisor middleware. Run on the control node after +# control-node.sh, from this checkout: the pod runs the code in it. +# +# Usage: +# deploy/watchdog.sh mint TLS material if needed, deploy, register +# deploy/watchdog.sh --remove delete the watchdog and its registration +# +# The gateway calls Describe at startup and refuses to start if the watchdog is +# unreachable, so the pod is ready before the gateway is reconfigured. + +set -euo pipefail + +deploy_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +project_dir="$(dirname "${deploy_dir}")" +# shellcheck source=versions.env +source "${deploy_dir}/versions.env" +# shellcheck disable=SC1091 +source "${TWOBOX_HOME:-${HOME}/.twobox}/env" + +SERVICE_DNS=egress-watchdog.openshell.svc.cluster.local +state="${TWOBOX_HOME}/watchdog" +mkdir -p "${state}" + +render_manifest() { + sed -e "s|__WATCHDOG_IMAGE__|${WATCHDOG_IMAGE}|" -e "s|__PROJECT_DIR__|${project_dir}|" \ + -e "s|__RUN_AS_UID__|$(owner_id -u)|" -e "s|__RUN_AS_GID__|$(owner_id -g)|" \ + "${deploy_dir}/watchdog.yaml" +} + +# The checkout's owner uid (-u) or gid (-g). GNU stat on Linux, BSD stat on macOS. +owner_id() { + local gnu=%u bsd=%u + if [[ "$1" == -g ]]; then gnu=%g bsd=%g; fi + stat -c "${gnu}" "${project_dir}" 2>/dev/null || stat -f "${bsd}" "${project_dir}" +} + +main() { + case "${1:-}" in + --remove) remove; return ;; + "") ;; + *) echo "usage: $0 [--remove]" >&2; exit 2 ;; + esac + mint_tls + deploy_pod + register +} + +mint_tls() { + if [[ -f "${state}/server.crt" ]] && + openssl x509 -in "${state}/server.crt" -noout -checkhost "${SERVICE_DNS}" | grep -q "does match"; then + return + fi + echo "==> Minting watchdog CA and server certificate for ${SERVICE_DNS}" + openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 365 \ + -subj "/CN=egress-watchdog-ca" -keyout "${state}/ca.key" -out "${state}/ca.crt" 2>/dev/null + openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -subj "/CN=egress-watchdog" \ + -keyout "${state}/server.key" -out "${state}/server.csr" 2>/dev/null + openssl x509 -req -in "${state}/server.csr" -CA "${state}/ca.crt" -CAkey "${state}/ca.key" \ + -CAcreateserial -days 365 -out "${state}/server.crt" \ + -extfile <(printf 'subjectAltName=DNS:%s,DNS:egress-watchdog.openshell.svc\nextendedKeyUsage=serverAuth\n' \ + "${SERVICE_DNS}") 2>/dev/null + chmod 600 "${state}"/*.key +} + +deploy_pod() { + echo "==> Deploying the watchdog pod" + kubectl -n openshell create secret tls egress-watchdog-tls \ + --cert="${state}/server.crt" --key="${state}/server.key" --dry-run=client -o yaml | + kubectl apply -f - >/dev/null + kubectl -n openshell create secret generic egress-watchdog-ca \ + --from-file=ca.crt="${state}/ca.crt" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + render_manifest | kubectl apply -f - >/dev/null + # Restart so a changed certificate or checkout is picked up. + kubectl -n openshell rollout restart deployment/egress-watchdog >/dev/null + # The first start installs dependencies from PyPI. + kubectl -n openshell rollout status deployment/egress-watchdog --timeout=300s +} + +register() { + echo "==> Registering the watchdog with the gateway" + cat >"${state}/values.yaml" </dev/null + restart_gateway + openshell gateway info | grep -A1 "egress-watchdog" + echo "Watchdog log: kubectl -n openshell logs deployment/egress-watchdog" +} + +remove() { + # Unregister first: the gateway will not start while a registered watchdog is down. + helm upgrade openshell oci://ghcr.io/nvidia/openshell/helm-chart \ + --version "${OPENSHELL_VERSION}" --namespace openshell --reuse-values \ + --set-json 'gatewayConfig.openshell\.supervisor\.middleware=[]' \ + --set-json 'server.extraVolumes=[]' --set-json 'server.extraVolumeMounts=[]' \ + --wait --timeout 5m >/dev/null + restart_gateway + render_manifest | kubectl delete --ignore-not-found -f - >/dev/null + kubectl -n openshell delete secret egress-watchdog-tls egress-watchdog-ca --ignore-not-found >/dev/null +} + +restart_gateway() { + # Registration is read at gateway startup; a config-only change may not roll the pod. + kubectl -n openshell rollout restart statefulset/openshell >/dev/null + kubectl -n openshell rollout status statefulset/openshell --timeout=180s + for _ in $(seq 1 30); do + openshell status 2>/dev/null | grep -q "Status: Connected" && return + sleep 2 + done +} + +main "$@" diff --git a/projects/use-case-examples/agent-containment/deploy/watchdog.yaml b/projects/use-case-examples/agent-containment/deploy/watchdog.yaml new file mode 100644 index 00000000..fadbd37c --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/watchdog.yaml @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# The egress watchdog as a pod on the control node. It runs a stock uv Python +# image against this project's checkout on the control node, so there is no +# image to build. watchdog.sh fills in the placeholders. The pod runs as the +# checkout's owner, because home directories are not readable by other users. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: egress-watchdog + namespace: openshell + labels: + app.kubernetes.io/name: egress-watchdog +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: egress-watchdog + template: + metadata: + labels: + app.kubernetes.io/name: egress-watchdog + spec: + nodeSelector: + openshell.io/role: control + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: __RUN_AS_UID__ + runAsGroup: __RUN_AS_GID__ + seccompProfile: + type: RuntimeDefault + containers: + - name: watchdog + image: __WATCHDOG_IMAGE__ + command: + - uv + - run + - --project=/app + - --locked + - --no-dev + - egress-watchdog + - --listen=0.0.0.0:50051 + - --tls-cert=/etc/egress-watchdog/tls/tls.crt + - --tls-key=/etc/egress-watchdog/tls/tls.key + - --jwt-public-key=/etc/egress-watchdog/jwt/public.pem + env: + # The checkout is read-only; uv keeps its environment and cache in /tmp. + - name: UV_PROJECT_ENVIRONMENT + value: /tmp/venv + - name: UV_CACHE_DIR + value: /tmp/uv-cache + - name: HOME + value: /tmp + - name: PYTHONUNBUFFERED + value: "1" + ports: + - name: grpc + containerPort: 50051 + readinessProbe: + tcpSocket: + port: grpc + periodSeconds: 5 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + volumeMounts: + - name: project + mountPath: /app + readOnly: true + - name: tls + mountPath: /etc/egress-watchdog/tls + readOnly: true + - name: jwt + mountPath: /etc/egress-watchdog/jwt + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: project + hostPath: + path: __PROJECT_DIR__ + type: Directory + - name: tls + secret: + secretName: egress-watchdog-tls + # Only the gateway's public key; the signing key is never mounted. + - name: jwt + secret: + secretName: openshell-jwt-keys + items: + - key: public.pem + path: public.pem + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: egress-watchdog + namespace: openshell +spec: + selector: + app.kubernetes.io/name: egress-watchdog + ports: + - name: grpc + port: 50051 + targetPort: grpc +--- +# Only the gateway and sandbox supervisors may call the watchdog. Traffic from a +# sandbox node, including its host, does not match and is dropped. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-watchdog + namespace: openshell +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: egress-watchdog + policyTypes: [Ingress] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: openshell + - podSelector: + matchLabels: + openshell.ai/boundary-role: supervisor + ports: + - port: grpc diff --git a/projects/use-case-examples/agent-containment/deploy/watched-policy.yaml b/projects/use-case-examples/agent-containment/deploy/watched-policy.yaml new file mode 100644 index 00000000..850e60e7 --- /dev/null +++ b/projects/use-case-examples/agent-containment/deploy/watched-policy.yaml @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# demo-policy.yaml plus the egress watchdog on httpbin.org: every request is +# accounted, and more than 1 MiB of request bytes in a rolling minute denies +# the request and quarantines the sandbox at the watchdog. +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_middlewares: + egress-watchdog: + middleware: egress-watchdog + # Bodies over the watchdog's 4 MiB payload limit skip middleware; fail + # closed so they are refused instead of passing unaccounted. + on_error: fail_closed + config: + budget: + bytes_per_minute: 1048576 + endpoints: + include: ["httpbin.org"] + +network_policies: + httpbin: + name: httpbin + endpoints: + - host: httpbin.org + port: 443 + protocol: rest + access: read-write + binaries: + - path: /usr/bin/curl diff --git a/projects/use-case-examples/agent-containment/docs/architecture.svg b/projects/use-case-examples/agent-containment/docs/architecture.svg new file mode 100644 index 00000000..739a8f3a --- /dev/null +++ b/projects/use-case-examples/agent-containment/docs/architecture.svg @@ -0,0 +1,108 @@ + + + + + + Agent containment architecture + One k3s cluster across two machines. The control node runs the OpenShell gateway, every sandbox's supervisor, and the egress watchdog. A sandbox node accepts only network-isolated sandbox workloads; more sandbox nodes join the same way. Each workload's only network path is a mediated channel to its supervisor on the control node. The supervisor sends every request to the watchdog, then makes policy-approved connections to external services. The sandbox node holds no policy, credentials, signing keys, or watchdog. + + + + + + + + + + + + + + EXTERNAL SERVICES + + + Allowed APIs + registries, tools, data + + Models + inference APIs + + + CONTROL NODE + + + + Gateway + policy, credentials, + signing keys + + + Egress watchdog + accounting, byte budgets, + quarantine + + + Supervisors + ONE PER SANDBOX · GOVERNS THE AGENT + Policy evaluation · Credential injection + TLS termination · Upstream connections + + SUPERVISORS NEVER RUN ON SANDBOX NODES + + + + EVERY REQUEST + + POLICY-APPROVED EGRESS + + + SANDBOX NODE · SANDBOXES ONLY + + + + SANDBOX WORKLOADS · NETWORK ISOLATED + + Sandbox + process owner and syscall boundary + + Agent + untrusted workload + + + + WORKLOAD NETWORK RULE + Deny all egress + except the channel to its supervisor + + + + MEDIATED + CHANNEL + + + + + + NO DIRECT EGRESS + + + + CONTROL NODE HOLDS + Policy · provider credentials · signing keys · the watchdog + + SANDBOX NODE HOLDS + Sandbox workloads · a join-only token + diff --git a/projects/use-case-examples/agent-containment/proto/extension.proto b/projects/use-case-examples/agent-containment/proto/extension.proto new file mode 100644 index 00000000..7c505f1a --- /dev/null +++ b/projects/use-case-examples/agent-containment/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/projects/use-case-examples/agent-containment/proto/supervisor_middleware.proto b/projects/use-case-examples/agent-containment/proto/supervisor_middleware.proto new file mode 100644 index 00000000..6c0cc947 --- /dev/null +++ b/projects/use-case-examples/agent-containment/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; +} diff --git a/projects/use-case-examples/agent-containment/pyproject.toml b/projects/use-case-examples/agent-containment/pyproject.toml new file mode 100644 index 00000000..b620ccf3 --- /dev/null +++ b/projects/use-case-examples/agent-containment/pyproject.toml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[project] +name = "egress-watchdog" +version = "0.1.0" +description = "Egress watchdog middleware for the agent containment example" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "grpcio>=1.81.1,<2", + "protobuf>=6.33.5,<7", + "pyjwt[crypto]>=2.10,<3", +] + +[project.scripts] +egress-watchdog = "egress_watchdog.server:main" + +[dependency-groups] +dev = [ + "grpcio-tools>=1.81.1,<2", + "pytest>=8,<10", + "ruff>=0.12,<1", +] + +[build-system] +requires = ["uv_build>=0.11.8,<0.12.0"] +build-backend = "uv_build" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py311" +extend-exclude = ["src/egress_watchdog/bindings"] + +[tool.ruff.lint] +select = ["B", "E", "F", "I", "RUF", "SIM", "UP"] diff --git a/projects/use-case-examples/agent-containment/scripts/generate-bindings.sh b/projects/use-case-examples/agent-containment/scripts/generate-bindings.sh new file mode 100755 index 00000000..ac36bb17 --- /dev/null +++ b/projects/use-case-examples/agent-containment/scripts/generate-bindings.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Regenerate the Python gRPC bindings from the vendored OpenShell protos. +# Refresh proto/ from the release named in deploy/versions.env first. + +set -euo pipefail + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +out="${project_dir}/src/egress_watchdog/bindings" + +cd "${project_dir}" +uv run --group dev python -m grpc_tools.protoc -Iproto \ + --python_out="${out}" --pyi_out="${out}" --grpc_python_out="${out}" \ + proto/extension.proto proto/supervisor_middleware.proto + +# protoc emits top-level imports; make them absolute package imports. +sed -i.bak -E 's/^import (extension_pb2|supervisor_middleware_pb2) as/from egress_watchdog.bindings import \1 as/' \ + "${out}"/*_pb2.py "${out}"/*_pb2_grpc.py +rm -f "${out}"/*.bak diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/__init__.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/__init__.py new file mode 100644 index 00000000..52a7a9da --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/__init__.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/__init__.py new file mode 100644 index 00000000..52a7a9da --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.py new file mode 100644 index 00000000..179985b1 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: extension.proto +# Protobuf Python Version: 6.33.5 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 5, + '', + 'extension.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x65xtension.proto\x12\x16openshell.extension.v1\"/\n\x0fProtocolVersion\x12\r\n\x05major\x18\x01 \x01(\r\x12\r\n\x05minor\x18\x02 \x01(\r\"\xcd\x01\n\x0cPeerMetadata\x12\x41\n\x10protocol_version\x18\x01 \x01(\x0b\x32\'.openshell.extension.v1.ProtocolVersion\x12\x1b\n\x13implementation_name\x18\x02 \x01(\t\x12\x1e\n\x16implementation_version\x18\x03 \x01(\t\x12\x1e\n\x16supported_capabilities\x18\x04 \x03(\t\x12\x1d\n\x15required_capabilities\x18\x05 \x03(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'extension_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_PROTOCOLVERSION']._serialized_start=43 + _globals['_PROTOCOLVERSION']._serialized_end=90 + _globals['_PEERMETADATA']._serialized_start=93 + _globals['_PEERMETADATA']._serialized_end=298 +# @@protoc_insertion_point(module_scope) diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.pyi b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.pyi new file mode 100644 index 00000000..0cab88d5 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2.pyi @@ -0,0 +1,29 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ProtocolVersion(_message.Message): + __slots__ = ("major", "minor") + MAJOR_FIELD_NUMBER: _ClassVar[int] + MINOR_FIELD_NUMBER: _ClassVar[int] + major: int + minor: int + def __init__(self, major: _Optional[int] = ..., minor: _Optional[int] = ...) -> None: ... + +class PeerMetadata(_message.Message): + __slots__ = ("protocol_version", "implementation_name", "implementation_version", "supported_capabilities", "required_capabilities") + PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int] + IMPLEMENTATION_NAME_FIELD_NUMBER: _ClassVar[int] + IMPLEMENTATION_VERSION_FIELD_NUMBER: _ClassVar[int] + SUPPORTED_CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + REQUIRED_CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + protocol_version: ProtocolVersion + implementation_name: str + implementation_version: str + supported_capabilities: _containers.RepeatedScalarFieldContainer[str] + required_capabilities: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, protocol_version: _Optional[_Union[ProtocolVersion, _Mapping]] = ..., implementation_name: _Optional[str] = ..., implementation_version: _Optional[str] = ..., supported_capabilities: _Optional[_Iterable[str]] = ..., required_capabilities: _Optional[_Iterable[str]] = ...) -> None: ... diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2_grpc.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2_grpc.py new file mode 100644 index 00000000..e08c3716 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/extension_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.81.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in extension_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.py new file mode 100644 index 00000000..8394e6e4 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: supervisor_middleware.proto +# Protobuf Python Version: 6.33.5 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 5, + '', + 'supervisor_middleware.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from egress_watchdog.bindings import extension_pb2 as extension__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bsupervisor_middleware.proto\x12\x17openshell.middleware.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x0f\x65xtension.proto\"R\n\x19MiddlewareDescribeRequest\x12\x35\n\x07gateway\x18\x01 \x01(\x0b\x32$.openshell.extension.v1.PeerMetadata\"\xcd\x01\n\x12MiddlewareManifest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0fservice_version\x18\x02 \x01(\t\x12<\n\x08\x62indings\x18\x03 \x03(\x0b\x32*.openshell.middleware.v1.MiddlewareBinding\x12\x19\n\x11\x65xpected_audience\x18\x04 \x01(\t\x12\x37\n\textension\x18\x05 \x01(\x0b\x32$.openshell.extension.v1.PeerMetadata\"\xff\x01\n\x11MiddlewareBinding\x12I\n\toperation\x18\x01 \x01(\x0e\x32\x36.openshell.middleware.v1.SupervisorMiddlewareOperation\x12\x41\n\x05phase\x18\x02 \x01(\x0e\x32\x32.openshell.middleware.v1.SupervisorMiddlewarePhase\x12\x19\n\x11max_payload_bytes\x18\x03 \x01(\x04\x12\x32\n\x0frequest_timeout\x18h \x01(\x0b\x32\x19.google.protobuf.DurationJ\x04\x08\x04\x10\x05R\x07timeout\"Y\n\x15ValidateConfigRequest\x12\'\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x17\n\x0fmiddleware_name\x18\x02 \x01(\t\"7\n\x16ValidateConfigResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\"\xd6\x02\n\x15HttpRequestEvaluation\x12\x41\n\x05phase\x18\x01 \x01(\x0e\x32\x32.openshell.middleware.v1.SupervisorMiddlewarePhase\x12\x38\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\'.openshell.middleware.v1.RequestContext\x12\'\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12:\n\x06target\x18\x04 \x01(\x0b\x32*.openshell.middleware.v1.HttpRequestTarget\x12\x34\n\x07headers\x18\x05 \x03(\x0b\x32#.openshell.middleware.v1.HttpHeader\x12\x0c\n\x04\x62ody\x18\x06 \x01(\x0c\x12\x17\n\x0fmiddleware_name\x18\x07 \x01(\t\")\n\nHttpHeader\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\xa9\x02\n\x11HttpResponseEvent\x12\x43\n\tpreflight\x18\x01 \x01(\x0b\x32..openshell.middleware.v1.HttpResponsePreflightH\x00\x12=\n\x04\x62ody\x18\x02 \x01(\x0b\x32-.openshell.middleware.v1.HttpResponseBodyUnitH\x00\x12\x41\n\x08trailers\x18\x04 \x01(\x0b\x32-.openshell.middleware.v1.HttpResponseTrailersH\x00\x12\x44\n\x0bsession_end\x18\x03 \x01(\x0b\x32-.openshell.middleware.v1.MiddlewareSessionEndH\x00\x42\x07\n\x05\x65vent\"\x8d\x02\n\x17HttpResponseEventResult\x12P\n\x10preflight_result\x18\x01 \x01(\x0b\x32\x34.openshell.middleware.v1.HttpResponsePreflightResultH\x00\x12\x46\n\x0b\x62ody_result\x18\x02 \x01(\x0b\x32/.openshell.middleware.v1.HttpResponseBodyResultH\x00\x12N\n\x0ftrailers_result\x18\x03 \x01(\x0b\x32\x33.openshell.middleware.v1.HttpResponseTrailersResultH\x00\x42\x08\n\x06result\"\x82\x03\n\x15HttpResponsePreflight\x12\x38\n\x07\x63ontext\x18\x01 \x01(\x0b\x32\'.openshell.middleware.v1.RequestContext\x12:\n\x06target\x18\x02 \x01(\x0b\x32*.openshell.middleware.v1.HttpRequestTarget\x12\x13\n\x0bstatus_code\x18\x03 \x01(\r\x12\x34\n\x07headers\x18\x04 \x03(\x0b\x32#.openshell.middleware.v1.HttpHeader\x12\x17\n\x0fmiddleware_name\x18\x05 \x01(\t\x12\'\n\x06\x63onfig\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x19\n\x11max_payload_bytes\x18\x07 \x01(\x04\x12K\n\x14permitted_body_modes\x18\x08 \x03(\x0e\x32-.openshell.middleware.v1.HttpResponseBodyMode\"\xe3\x03\n\x1bHttpResponsePreflightResult\x12\x42\n\x04skip\x18\x01 \x01(\x0b\x32\x32.openshell.middleware.v1.HttpResponsePreflightSkipH\x00\x12H\n\x07inspect\x18\x02 \x01(\x0b\x32\x35.openshell.middleware.v1.HttpResponsePreflightInspectH\x00\x12L\n\x0e\x62lock_delivery\x18\x07 \x01(\x0b\x32\x32.openshell.middleware.v1.HttpResponseBlockDeliveryH\x00\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x13\n\x0breason_code\x18\x04 \x01(\t\x12\x32\n\x08\x66indings\x18\x05 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12T\n\x08metadata\x18\x06 \x03(\x0b\x32\x42.openshell.middleware.v1.HttpResponsePreflightResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x08\n\x06\x61\x63tion\"\x1b\n\x19HttpResponsePreflightSkip\"\x1b\n\x19HttpResponseBlockDelivery\"\xa3\x01\n\x1cHttpResponsePreflightInspect\x12@\n\tbody_mode\x18\x01 \x01(\x0e\x32-.openshell.middleware.v1.HttpResponseBodyMode\x12\x41\n\x10header_mutations\x18\x02 \x03(\x0b\x32\'.openshell.middleware.v1.HeaderMutation\"Z\n\x14HttpResponseBodyUnit\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12\x0e\n\x04\x64\x61ta\x18\x02 \x01(\x0cH\x00\x12\x15\n\rend_of_stream\x18\x03 \x01(\x08\x42\t\n\x07payload\"\xc6\x04\n\x16HttpResponseBodyResult\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12L\n\x0cpass_through\x18\x02 \x01(\x0b\x32\x34.openshell.middleware.v1.HttpResponseBodyPassThroughH\x00\x12G\n\ttransform\x18\x03 \x01(\x0b\x32\x32.openshell.middleware.v1.HttpResponseBodyTransformH\x00\x12L\n\x0e\x62lock_delivery\x18\x08 \x01(\x0b\x32\x32.openshell.middleware.v1.HttpResponseBlockDeliveryH\x00\x12P\n\x0eskip_remaining\x18\t \x01(\x0b\x32\x36.openshell.middleware.v1.HttpResponseBodySkipRemainingH\x00\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x13\n\x0breason_code\x18\x05 \x01(\t\x12\x32\n\x08\x66indings\x18\x06 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12O\n\x08metadata\x18\x07 \x03(\x0b\x32=.openshell.middleware.v1.HttpResponseBodyResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x08\n\x06\x61\x63tion\"\x1d\n\x1bHttpResponseBodyPassThrough\"\xc1\x01\n\x1dHttpResponseBodySkipRemaining\x12L\n\x0cpass_through\x18\x01 \x01(\x0b\x32\x34.openshell.middleware.v1.HttpResponseBodyPassThroughH\x00\x12G\n\ttransform\x18\x02 \x01(\x0b\x32\x32.openshell.middleware.v1.HttpResponseBodyTransformH\x00\x42\t\n\x07\x63urrent\":\n\x19HttpResponseBodyTransform\x12\x0e\n\x04\x64\x61ta\x18\x01 \x01(\x0cH\x00\x42\r\n\x0breplacement\"L\n\x14HttpResponseTrailers\x12\x34\n\x07headers\x18\x01 \x03(\x0b\x32#.openshell.middleware.v1.HttpHeader\"\xbf\x02\n\x1aHttpResponseTrailersResult\x12\x42\n\x11trailer_mutations\x18\x01 \x03(\x0b\x32\'.openshell.middleware.v1.HeaderMutation\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x13\n\x0breason_code\x18\x03 \x01(\t\x12\x32\n\x08\x66indings\x18\x04 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12S\n\x08metadata\x18\x05 \x03(\x0b\x32\x41.openshell.middleware.v1.HttpResponseTrailersResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xac\x01\n\x14MiddlewareSessionEnd\x12\x43\n\x06reason\x18\x01 \x01(\x0e\x32\x33.openshell.middleware.v1.MiddlewareSessionEndReason\x12O\n\x0eprotocol_error\x18\x02 \x01(\x0b\x32\x37.openshell.middleware.v1.MiddlewareSessionProtocolError\"\xca\x01\n\x1eMiddlewareSessionProtocolError\x12\x45\n\nweb_socket\x18\x01 \x01(\x0b\x32/.openshell.middleware.v1.WebSocketProtocolErrorH\x00\x12W\n\x13middleware_exchange\x18\x02 \x01(\x0b\x32\x38.openshell.middleware.v1.MiddlewareExchangeProtocolErrorH\x00\x42\x08\n\x06\x64omain\"\x18\n\x16WebSocketProtocolError\"!\n\x1fMiddlewareExchangeProtocolError\"\xaf\x02\n\x15WebSocketSessionEvent\x12@\n\tpreflight\x18\x01 \x01(\x0b\x32+.openshell.middleware.v1.WebSocketPreflightH\x00\x12G\n\rsession_start\x18\x02 \x01(\x0b\x32..openshell.middleware.v1.WebSocketSessionStartH\x00\x12<\n\x07message\x18\x03 \x01(\x0b\x32).openshell.middleware.v1.WebSocketMessageH\x00\x12\x44\n\x0bsession_end\x18\x04 \x01(\x0b\x32-.openshell.middleware.v1.MiddlewareSessionEndH\x00\x42\x07\n\x05\x65vent\"\xc3\x02\n\x12WebSocketPreflight\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x41\n\x05phase\x18\x02 \x01(\x0e\x32\x32.openshell.middleware.v1.SupervisorMiddlewarePhase\x12\x38\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\'.openshell.middleware.v1.RequestContext\x12:\n\x06target\x18\x04 \x01(\x0b\x32*.openshell.middleware.v1.HttpRequestTarget\x12\x1e\n\x16requested_subprotocols\x18\x05 \x03(\t\x12\x17\n\x0fmiddleware_name\x18\x06 \x01(\t\x12\'\n\x06\x63onfig\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\"5\n\x15WebSocketSessionStart\x12\x1c\n\x14selected_subprotocol\x18\x01 \x01(\t\"Q\n\x10WebSocketMessage\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12\x0e\n\x04text\x18\x02 \x01(\tH\x00\x12\x10\n\x06\x62inary\x18\x03 \x01(\x0cH\x00\x42\t\n\x07payload\"\xbe\x02\n\x1aWebSocketPreflightDecision\x12\x41\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x31.openshell.middleware.v1.WebSocketPreflightAction\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x13\n\x0breason_code\x18\x03 \x01(\t\x12\x32\n\x08\x66indings\x18\x04 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12S\n\x08metadata\x18\x05 \x03(\x0b\x32\x41.openshell.middleware.v1.WebSocketPreflightDecision.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xeb\x02\n\x16WebSocketMessageResult\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12\x33\n\x08\x64\x65\x63ision\x18\x02 \x01(\x0e\x32!.openshell.middleware.v1.Decision\x12\x0e\n\x04text\x18\x03 \x01(\tH\x00\x12\x10\n\x06\x62inary\x18\x04 \x01(\x0cH\x00\x12\x0e\n\x06reason\x18\x05 \x01(\t\x12\x13\n\x0breason_code\x18\x06 \x01(\t\x12\x32\n\x08\x66indings\x18\x07 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12O\n\x08metadata\x18\x08 \x03(\x0b\x32=.openshell.middleware.v1.WebSocketMessageResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0breplacement\"\xc5\x01\n\x1bWebSocketSessionEventResult\x12Q\n\x12preflight_decision\x18\x01 \x01(\x0b\x32\x33.openshell.middleware.v1.WebSocketPreflightDecisionH\x00\x12I\n\x0emessage_result\x18\x02 \x01(\x0b\x32/.openshell.middleware.v1.WebSocketMessageResultH\x00\x42\x08\n\x06result\"\x9b\x01\n\x0eRequestContext\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x12\n\nsandbox_id\x18\x02 \x01(\t\x12=\n\x13originating_process\x18\x03 \x01(\x0b\x32 .openshell.middleware.v1.Process\x12\x0f\n\x07sandbox\x18\x04 \x01(\t\x12\x11\n\tworkspace\x18\x05 \x01(\t\"l\n\x11HttpRequestTarget\x12\x0e\n\x06scheme\x18\x01 \x01(\t\x12\x0c\n\x04host\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\x0c\n\x04path\x18\x05 \x01(\t\x12\r\n\x05query\x18\x06 \x01(\t\"9\n\x07Process\x12\x0e\n\x06\x62inary\x18\x01 \x01(\t\x12\x0b\n\x03pid\x18\x02 \x01(\r\x12\x11\n\tancestors\x18\x03 \x03(\t\"[\n\x07\x46inding\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\r\n\x05\x63ount\x18\x03 \x01(\r\x12\x12\n\nconfidence\x18\x04 \x01(\t\x12\x10\n\x08severity\x18\x05 \x01(\t\"n\n\x0bWriteHeader\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x42\n\x0bon_existing\x18\x03 \x01(\x0e\x32-.openshell.middleware.v1.ExistingHeaderAction\"\x1c\n\x0cRemoveHeader\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x8d\x01\n\x0eHeaderMutation\x12\x35\n\x05write\x18\x01 \x01(\x0b\x32$.openshell.middleware.v1.WriteHeaderH\x00\x12\x37\n\x06remove\x18\x02 \x01(\x0b\x32%.openshell.middleware.v1.RemoveHeaderH\x00\x42\x0b\n\toperation\"\x81\x03\n\x11HttpRequestResult\x12\x33\n\x08\x64\x65\x63ision\x18\x01 \x01(\x0e\x32!.openshell.middleware.v1.Decision\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0c\n\x04\x62ody\x18\x03 \x01(\x0c\x12\x10\n\x08has_body\x18\x04 \x01(\x08\x12\x41\n\x10header_mutations\x18\x05 \x03(\x0b\x32\'.openshell.middleware.v1.HeaderMutation\x12\x32\n\x08\x66indings\x18\x06 \x03(\x0b\x32 .openshell.middleware.v1.Finding\x12J\n\x08metadata\x18\x07 \x03(\x0b\x32\x38.openshell.middleware.v1.HttpRequestResult.MetadataEntry\x12\x13\n\x0breason_code\x18\x08 \x01(\t\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\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\x12\x37\n3MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT\x10\x02\x12/\n+MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD\x10\x03\x12\x33\n/MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL\x10\x04\x12\x34\n0MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE\x10\x05\x12\x30\n,MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR\x10\x06\x12.\n*MIDDLEWARE_SESSION_END_REASON_CANCELLATION\x10\x07\x12\x32\n.MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE\x10\x08\x12/\n+MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL\x10\t\x12/\n+MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED\x10\n\x12\x35\n1MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT\x10\x0b*\xec\x01\n\x1dSupervisorMiddlewareOperation\x12/\n+SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED\x10\x00\x12\x30\n,SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST\x10\x01\x12\x35\n1SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE\x10\x02\x12\x31\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\x08\x44\x65\x63ision\x12\x18\n\x14\x44\x45\x43ISION_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x44\x45\x43ISION_ALLOW\x10\x01\x12\x11\n\rDECISION_DENY\x10\x02*\xa8\x01\n\x14\x45xistingHeaderAction\x12&\n\"EXISTING_HEADER_ACTION_UNSPECIFIED\x10\x00\x12!\n\x1d\x45XISTING_HEADER_ACTION_APPEND\x10\x01\x12$\n EXISTING_HEADER_ACTION_OVERWRITE\x10\x02\x12\x1f\n\x1b\x45XISTING_HEADER_ACTION_SKIP\x10\x03\x32\xf0\x03\n\x14SupervisorMiddleware\x12k\n\x08\x44\x65scribe\x12\x32.openshell.middleware.v1.MiddlewareDescribeRequest\x1a+.openshell.middleware.v1.MiddlewareManifest\x12q\n\x0eValidateConfig\x12..openshell.middleware.v1.ValidateConfigRequest\x1a/.openshell.middleware.v1.ValidateConfigResponse\x12q\n\x13\x45valuateHttpRequest\x12..openshell.middleware.v1.HttpRequestEvaluation\x1a*.openshell.middleware.v1.HttpRequestResult\x12\x84\x01\n\x18\x45valuateWebSocketSession\x12..openshell.middleware.v1.WebSocketSessionEvent\x1a\x34.openshell.middleware.v1.WebSocketSessionEventResult(\x01\x30\x01\x32\x85\x01\n\x15HttpResponsePreReturn\x12l\n\x08\x45valuate\x12*.openshell.middleware.v1.HttpResponseEvent\x1a\x30.openshell.middleware.v1.HttpResponseEventResult(\x01\x30\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'supervisor_middleware_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_HTTPRESPONSEPREFLIGHTRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPRESPONSEPREFLIGHTRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPRESPONSEBODYRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPRESPONSEBODYRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPRESPONSETRAILERSRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPRESPONSETRAILERSRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_WEBSOCKETPREFLIGHTDECISION_METADATAENTRY']._loaded_options = None + _globals['_WEBSOCKETPREFLIGHTDECISION_METADATAENTRY']._serialized_options = b'8\001' + _globals['_WEBSOCKETMESSAGERESULT_METADATAENTRY']._loaded_options = None + _globals['_WEBSOCKETMESSAGERESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPREQUESTRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPREQUESTRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPRESPONSEBODYMODE']._serialized_start=7449 + _globals['_HTTPRESPONSEBODYMODE']._serialized_end=7642 + _globals['_MIDDLEWARESESSIONENDREASON']._serialized_start=7645 + _globals['_MIDDLEWARESESSIONENDREASON']._serialized_end=8278 + _globals['_SUPERVISORMIDDLEWAREOPERATION']._serialized_start=8281 + _globals['_SUPERVISORMIDDLEWAREOPERATION']._serialized_end=8517 + _globals['_SUPERVISORMIDDLEWAREPHASE']._serialized_start=8520 + _globals['_SUPERVISORMIDDLEWAREPHASE']._serialized_end=8685 + _globals['_WEBSOCKETPREFLIGHTACTION']._serialized_start=8688 + _globals['_WEBSOCKETPREFLIGHTACTION']._serialized_end=8876 + _globals['_DECISION']._serialized_start=8878 + _globals['_DECISION']._serialized_end=8953 + _globals['_EXISTINGHEADERACTION']._serialized_start=8956 + _globals['_EXISTINGHEADERACTION']._serialized_end=9124 + _globals['_MIDDLEWAREDESCRIBEREQUEST']._serialized_start=135 + _globals['_MIDDLEWAREDESCRIBEREQUEST']._serialized_end=217 + _globals['_MIDDLEWAREMANIFEST']._serialized_start=220 + _globals['_MIDDLEWAREMANIFEST']._serialized_end=425 + _globals['_MIDDLEWAREBINDING']._serialized_start=428 + _globals['_MIDDLEWAREBINDING']._serialized_end=683 + _globals['_VALIDATECONFIGREQUEST']._serialized_start=685 + _globals['_VALIDATECONFIGREQUEST']._serialized_end=774 + _globals['_VALIDATECONFIGRESPONSE']._serialized_start=776 + _globals['_VALIDATECONFIGRESPONSE']._serialized_end=831 + _globals['_HTTPREQUESTEVALUATION']._serialized_start=834 + _globals['_HTTPREQUESTEVALUATION']._serialized_end=1176 + _globals['_HTTPHEADER']._serialized_start=1178 + _globals['_HTTPHEADER']._serialized_end=1219 + _globals['_HTTPRESPONSEEVENT']._serialized_start=1222 + _globals['_HTTPRESPONSEEVENT']._serialized_end=1519 + _globals['_HTTPRESPONSEEVENTRESULT']._serialized_start=1522 + _globals['_HTTPRESPONSEEVENTRESULT']._serialized_end=1791 + _globals['_HTTPRESPONSEPREFLIGHT']._serialized_start=1794 + _globals['_HTTPRESPONSEPREFLIGHT']._serialized_end=2180 + _globals['_HTTPRESPONSEPREFLIGHTRESULT']._serialized_start=2183 + _globals['_HTTPRESPONSEPREFLIGHTRESULT']._serialized_end=2666 + _globals['_HTTPRESPONSEPREFLIGHTRESULT_METADATAENTRY']._serialized_start=2609 + _globals['_HTTPRESPONSEPREFLIGHTRESULT_METADATAENTRY']._serialized_end=2656 + _globals['_HTTPRESPONSEPREFLIGHTSKIP']._serialized_start=2668 + _globals['_HTTPRESPONSEPREFLIGHTSKIP']._serialized_end=2695 + _globals['_HTTPRESPONSEBLOCKDELIVERY']._serialized_start=2697 + _globals['_HTTPRESPONSEBLOCKDELIVERY']._serialized_end=2724 + _globals['_HTTPRESPONSEPREFLIGHTINSPECT']._serialized_start=2727 + _globals['_HTTPRESPONSEPREFLIGHTINSPECT']._serialized_end=2890 + _globals['_HTTPRESPONSEBODYUNIT']._serialized_start=2892 + _globals['_HTTPRESPONSEBODYUNIT']._serialized_end=2982 + _globals['_HTTPRESPONSEBODYRESULT']._serialized_start=2985 + _globals['_HTTPRESPONSEBODYRESULT']._serialized_end=3567 + _globals['_HTTPRESPONSEBODYRESULT_METADATAENTRY']._serialized_start=2609 + _globals['_HTTPRESPONSEBODYRESULT_METADATAENTRY']._serialized_end=2656 + _globals['_HTTPRESPONSEBODYPASSTHROUGH']._serialized_start=3569 + _globals['_HTTPRESPONSEBODYPASSTHROUGH']._serialized_end=3598 + _globals['_HTTPRESPONSEBODYSKIPREMAINING']._serialized_start=3601 + _globals['_HTTPRESPONSEBODYSKIPREMAINING']._serialized_end=3794 + _globals['_HTTPRESPONSEBODYTRANSFORM']._serialized_start=3796 + _globals['_HTTPRESPONSEBODYTRANSFORM']._serialized_end=3854 + _globals['_HTTPRESPONSETRAILERS']._serialized_start=3856 + _globals['_HTTPRESPONSETRAILERS']._serialized_end=3932 + _globals['_HTTPRESPONSETRAILERSRESULT']._serialized_start=3935 + _globals['_HTTPRESPONSETRAILERSRESULT']._serialized_end=4254 + _globals['_HTTPRESPONSETRAILERSRESULT_METADATAENTRY']._serialized_start=2609 + _globals['_HTTPRESPONSETRAILERSRESULT_METADATAENTRY']._serialized_end=2656 + _globals['_MIDDLEWARESESSIONEND']._serialized_start=4257 + _globals['_MIDDLEWARESESSIONEND']._serialized_end=4429 + _globals['_MIDDLEWARESESSIONPROTOCOLERROR']._serialized_start=4432 + _globals['_MIDDLEWARESESSIONPROTOCOLERROR']._serialized_end=4634 + _globals['_WEBSOCKETPROTOCOLERROR']._serialized_start=4636 + _globals['_WEBSOCKETPROTOCOLERROR']._serialized_end=4660 + _globals['_MIDDLEWAREEXCHANGEPROTOCOLERROR']._serialized_start=4662 + _globals['_MIDDLEWAREEXCHANGEPROTOCOLERROR']._serialized_end=4695 + _globals['_WEBSOCKETSESSIONEVENT']._serialized_start=4698 + _globals['_WEBSOCKETSESSIONEVENT']._serialized_end=5001 + _globals['_WEBSOCKETPREFLIGHT']._serialized_start=5004 + _globals['_WEBSOCKETPREFLIGHT']._serialized_end=5327 + _globals['_WEBSOCKETSESSIONSTART']._serialized_start=5329 + _globals['_WEBSOCKETSESSIONSTART']._serialized_end=5382 + _globals['_WEBSOCKETMESSAGE']._serialized_start=5384 + _globals['_WEBSOCKETMESSAGE']._serialized_end=5465 + _globals['_WEBSOCKETPREFLIGHTDECISION']._serialized_start=5468 + _globals['_WEBSOCKETPREFLIGHTDECISION']._serialized_end=5786 + _globals['_WEBSOCKETPREFLIGHTDECISION_METADATAENTRY']._serialized_start=2609 + _globals['_WEBSOCKETPREFLIGHTDECISION_METADATAENTRY']._serialized_end=2656 + _globals['_WEBSOCKETMESSAGERESULT']._serialized_start=5789 + _globals['_WEBSOCKETMESSAGERESULT']._serialized_end=6152 + _globals['_WEBSOCKETMESSAGERESULT_METADATAENTRY']._serialized_start=2609 + _globals['_WEBSOCKETMESSAGERESULT_METADATAENTRY']._serialized_end=2656 + _globals['_WEBSOCKETSESSIONEVENTRESULT']._serialized_start=6155 + _globals['_WEBSOCKETSESSIONEVENTRESULT']._serialized_end=6352 + _globals['_REQUESTCONTEXT']._serialized_start=6355 + _globals['_REQUESTCONTEXT']._serialized_end=6510 + _globals['_HTTPREQUESTTARGET']._serialized_start=6512 + _globals['_HTTPREQUESTTARGET']._serialized_end=6620 + _globals['_PROCESS']._serialized_start=6622 + _globals['_PROCESS']._serialized_end=6679 + _globals['_FINDING']._serialized_start=6681 + _globals['_FINDING']._serialized_end=6772 + _globals['_WRITEHEADER']._serialized_start=6774 + _globals['_WRITEHEADER']._serialized_end=6884 + _globals['_REMOVEHEADER']._serialized_start=6886 + _globals['_REMOVEHEADER']._serialized_end=6914 + _globals['_HEADERMUTATION']._serialized_start=6917 + _globals['_HEADERMUTATION']._serialized_end=7058 + _globals['_HTTPREQUESTRESULT']._serialized_start=7061 + _globals['_HTTPREQUESTRESULT']._serialized_end=7446 + _globals['_HTTPREQUESTRESULT_METADATAENTRY']._serialized_start=2609 + _globals['_HTTPREQUESTRESULT_METADATAENTRY']._serialized_end=2656 + _globals['_SUPERVISORMIDDLEWARE']._serialized_start=9127 + _globals['_SUPERVISORMIDDLEWARE']._serialized_end=9623 + _globals['_HTTPRESPONSEPRERETURN']._serialized_start=9626 + _globals['_HTTPRESPONSEPRERETURN']._serialized_end=9759 +# @@protoc_insertion_point(module_scope) diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.pyi b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.pyi new file mode 100644 index 00000000..971e972a --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2.pyi @@ -0,0 +1,574 @@ +import datetime + +from google.protobuf import struct_pb2 as _struct_pb2 +from google.protobuf import duration_pb2 as _duration_pb2 +import extension_pb2 as _extension_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class HttpResponseBodyMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + HTTP_RESPONSE_BODY_MODE_UNSPECIFIED: _ClassVar[HttpResponseBodyMode] + HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY: _ClassVar[HttpResponseBodyMode] + HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES: _ClassVar[HttpResponseBodyMode] + HTTP_RESPONSE_BODY_MODE_STREAM_BYTES: _ClassVar[HttpResponseBodyMode] + +class MiddlewareSessionEndReason(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_NORMAL: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_CANCELLATION: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED: _ClassVar[MiddlewareSessionEndReason] + MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT: _ClassVar[MiddlewareSessionEndReason] + +class SupervisorMiddlewareOperation(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED: _ClassVar[SupervisorMiddlewareOperation] + SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST: _ClassVar[SupervisorMiddlewareOperation] + SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE: _ClassVar[SupervisorMiddlewareOperation] + SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE: _ClassVar[SupervisorMiddlewareOperation] + +class SupervisorMiddlewarePhase(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED: _ClassVar[SupervisorMiddlewarePhase] + SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS: _ClassVar[SupervisorMiddlewarePhase] + SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN: _ClassVar[SupervisorMiddlewarePhase] + +class WebSocketPreflightAction(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED: _ClassVar[WebSocketPreflightAction] + WEB_SOCKET_PREFLIGHT_ACTION_INSPECT: _ClassVar[WebSocketPreflightAction] + WEB_SOCKET_PREFLIGHT_ACTION_SKIP: _ClassVar[WebSocketPreflightAction] + WEB_SOCKET_PREFLIGHT_ACTION_DENY: _ClassVar[WebSocketPreflightAction] + +class Decision(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DECISION_UNSPECIFIED: _ClassVar[Decision] + DECISION_ALLOW: _ClassVar[Decision] + DECISION_DENY: _ClassVar[Decision] + +class ExistingHeaderAction(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + EXISTING_HEADER_ACTION_UNSPECIFIED: _ClassVar[ExistingHeaderAction] + EXISTING_HEADER_ACTION_APPEND: _ClassVar[ExistingHeaderAction] + EXISTING_HEADER_ACTION_OVERWRITE: _ClassVar[ExistingHeaderAction] + EXISTING_HEADER_ACTION_SKIP: _ClassVar[ExistingHeaderAction] +HTTP_RESPONSE_BODY_MODE_UNSPECIFIED: HttpResponseBodyMode +HTTP_RESPONSE_BODY_MODE_HEADERS_ONLY: HttpResponseBodyMode +HTTP_RESPONSE_BODY_MODE_WHOLE_BODY_BYTES: HttpResponseBodyMode +HTTP_RESPONSE_BODY_MODE_STREAM_BYTES: HttpResponseBodyMode +MIDDLEWARE_SESSION_END_REASON_UNSPECIFIED: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_NORMAL: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_DOWNSTREAM_DISCONNECT: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_POLICY_RELOAD: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_DENIAL: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_MIDDLEWARE_FAILURE: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_PROTOCOL_ERROR: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_CANCELLATION: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_UPSTREAM_FAILURE: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_POLICY_DENIAL: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_STAGE_SKIPPED: MiddlewareSessionEndReason +MIDDLEWARE_SESSION_END_REASON_UPSTREAM_DISCONNECT: MiddlewareSessionEndReason +SUPERVISOR_MIDDLEWARE_OPERATION_UNSPECIFIED: SupervisorMiddlewareOperation +SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST: SupervisorMiddlewareOperation +SUPERVISOR_MIDDLEWARE_OPERATION_WEBSOCKET_MESSAGE: SupervisorMiddlewareOperation +SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_RESPONSE: SupervisorMiddlewareOperation +SUPERVISOR_MIDDLEWARE_PHASE_UNSPECIFIED: SupervisorMiddlewarePhase +SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS: SupervisorMiddlewarePhase +SUPERVISOR_MIDDLEWARE_PHASE_PRE_RETURN: SupervisorMiddlewarePhase +WEB_SOCKET_PREFLIGHT_ACTION_UNSPECIFIED: WebSocketPreflightAction +WEB_SOCKET_PREFLIGHT_ACTION_INSPECT: WebSocketPreflightAction +WEB_SOCKET_PREFLIGHT_ACTION_SKIP: WebSocketPreflightAction +WEB_SOCKET_PREFLIGHT_ACTION_DENY: WebSocketPreflightAction +DECISION_UNSPECIFIED: Decision +DECISION_ALLOW: Decision +DECISION_DENY: Decision +EXISTING_HEADER_ACTION_UNSPECIFIED: ExistingHeaderAction +EXISTING_HEADER_ACTION_APPEND: ExistingHeaderAction +EXISTING_HEADER_ACTION_OVERWRITE: ExistingHeaderAction +EXISTING_HEADER_ACTION_SKIP: ExistingHeaderAction + +class MiddlewareDescribeRequest(_message.Message): + __slots__ = ("gateway",) + GATEWAY_FIELD_NUMBER: _ClassVar[int] + gateway: _extension_pb2.PeerMetadata + def __init__(self, gateway: _Optional[_Union[_extension_pb2.PeerMetadata, _Mapping]] = ...) -> None: ... + +class MiddlewareManifest(_message.Message): + __slots__ = ("name", "service_version", "bindings", "expected_audience", "extension") + NAME_FIELD_NUMBER: _ClassVar[int] + SERVICE_VERSION_FIELD_NUMBER: _ClassVar[int] + BINDINGS_FIELD_NUMBER: _ClassVar[int] + EXPECTED_AUDIENCE_FIELD_NUMBER: _ClassVar[int] + EXTENSION_FIELD_NUMBER: _ClassVar[int] + name: str + service_version: str + bindings: _containers.RepeatedCompositeFieldContainer[MiddlewareBinding] + expected_audience: str + extension: _extension_pb2.PeerMetadata + def __init__(self, name: _Optional[str] = ..., service_version: _Optional[str] = ..., bindings: _Optional[_Iterable[_Union[MiddlewareBinding, _Mapping]]] = ..., expected_audience: _Optional[str] = ..., extension: _Optional[_Union[_extension_pb2.PeerMetadata, _Mapping]] = ...) -> None: ... + +class MiddlewareBinding(_message.Message): + __slots__ = ("operation", "phase", "max_payload_bytes", "request_timeout") + OPERATION_FIELD_NUMBER: _ClassVar[int] + PHASE_FIELD_NUMBER: _ClassVar[int] + MAX_PAYLOAD_BYTES_FIELD_NUMBER: _ClassVar[int] + REQUEST_TIMEOUT_FIELD_NUMBER: _ClassVar[int] + operation: SupervisorMiddlewareOperation + phase: SupervisorMiddlewarePhase + max_payload_bytes: int + request_timeout: _duration_pb2.Duration + def __init__(self, operation: _Optional[_Union[SupervisorMiddlewareOperation, str]] = ..., phase: _Optional[_Union[SupervisorMiddlewarePhase, str]] = ..., max_payload_bytes: _Optional[int] = ..., request_timeout: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ...) -> None: ... + +class ValidateConfigRequest(_message.Message): + __slots__ = ("config", "middleware_name") + CONFIG_FIELD_NUMBER: _ClassVar[int] + MIDDLEWARE_NAME_FIELD_NUMBER: _ClassVar[int] + config: _struct_pb2.Struct + middleware_name: str + def __init__(self, config: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., middleware_name: _Optional[str] = ...) -> None: ... + +class ValidateConfigResponse(_message.Message): + __slots__ = ("valid", "reason") + VALID_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + valid: bool + reason: str + def __init__(self, valid: _Optional[bool] = ..., reason: _Optional[str] = ...) -> None: ... + +class HttpRequestEvaluation(_message.Message): + __slots__ = ("phase", "context", "config", "target", "headers", "body", "middleware_name") + PHASE_FIELD_NUMBER: _ClassVar[int] + CONTEXT_FIELD_NUMBER: _ClassVar[int] + CONFIG_FIELD_NUMBER: _ClassVar[int] + TARGET_FIELD_NUMBER: _ClassVar[int] + HEADERS_FIELD_NUMBER: _ClassVar[int] + BODY_FIELD_NUMBER: _ClassVar[int] + MIDDLEWARE_NAME_FIELD_NUMBER: _ClassVar[int] + phase: SupervisorMiddlewarePhase + context: RequestContext + config: _struct_pb2.Struct + target: HttpRequestTarget + headers: _containers.RepeatedCompositeFieldContainer[HttpHeader] + body: bytes + middleware_name: str + def __init__(self, phase: _Optional[_Union[SupervisorMiddlewarePhase, str]] = ..., context: _Optional[_Union[RequestContext, _Mapping]] = ..., config: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., target: _Optional[_Union[HttpRequestTarget, _Mapping]] = ..., headers: _Optional[_Iterable[_Union[HttpHeader, _Mapping]]] = ..., body: _Optional[bytes] = ..., middleware_name: _Optional[str] = ...) -> None: ... + +class HttpHeader(_message.Message): + __slots__ = ("name", "value") + NAME_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + name: str + value: str + def __init__(self, name: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + +class HttpResponseEvent(_message.Message): + __slots__ = ("preflight", "body", "trailers", "session_end") + PREFLIGHT_FIELD_NUMBER: _ClassVar[int] + BODY_FIELD_NUMBER: _ClassVar[int] + TRAILERS_FIELD_NUMBER: _ClassVar[int] + SESSION_END_FIELD_NUMBER: _ClassVar[int] + preflight: HttpResponsePreflight + body: HttpResponseBodyUnit + trailers: HttpResponseTrailers + session_end: MiddlewareSessionEnd + def __init__(self, preflight: _Optional[_Union[HttpResponsePreflight, _Mapping]] = ..., body: _Optional[_Union[HttpResponseBodyUnit, _Mapping]] = ..., trailers: _Optional[_Union[HttpResponseTrailers, _Mapping]] = ..., session_end: _Optional[_Union[MiddlewareSessionEnd, _Mapping]] = ...) -> None: ... + +class HttpResponseEventResult(_message.Message): + __slots__ = ("preflight_result", "body_result", "trailers_result") + PREFLIGHT_RESULT_FIELD_NUMBER: _ClassVar[int] + BODY_RESULT_FIELD_NUMBER: _ClassVar[int] + TRAILERS_RESULT_FIELD_NUMBER: _ClassVar[int] + preflight_result: HttpResponsePreflightResult + body_result: HttpResponseBodyResult + trailers_result: HttpResponseTrailersResult + def __init__(self, preflight_result: _Optional[_Union[HttpResponsePreflightResult, _Mapping]] = ..., body_result: _Optional[_Union[HttpResponseBodyResult, _Mapping]] = ..., trailers_result: _Optional[_Union[HttpResponseTrailersResult, _Mapping]] = ...) -> None: ... + +class HttpResponsePreflight(_message.Message): + __slots__ = ("context", "target", "status_code", "headers", "middleware_name", "config", "max_payload_bytes", "permitted_body_modes") + CONTEXT_FIELD_NUMBER: _ClassVar[int] + TARGET_FIELD_NUMBER: _ClassVar[int] + STATUS_CODE_FIELD_NUMBER: _ClassVar[int] + HEADERS_FIELD_NUMBER: _ClassVar[int] + MIDDLEWARE_NAME_FIELD_NUMBER: _ClassVar[int] + CONFIG_FIELD_NUMBER: _ClassVar[int] + MAX_PAYLOAD_BYTES_FIELD_NUMBER: _ClassVar[int] + PERMITTED_BODY_MODES_FIELD_NUMBER: _ClassVar[int] + context: RequestContext + target: HttpRequestTarget + status_code: int + headers: _containers.RepeatedCompositeFieldContainer[HttpHeader] + middleware_name: str + config: _struct_pb2.Struct + max_payload_bytes: int + permitted_body_modes: _containers.RepeatedScalarFieldContainer[HttpResponseBodyMode] + def __init__(self, context: _Optional[_Union[RequestContext, _Mapping]] = ..., target: _Optional[_Union[HttpRequestTarget, _Mapping]] = ..., status_code: _Optional[int] = ..., headers: _Optional[_Iterable[_Union[HttpHeader, _Mapping]]] = ..., middleware_name: _Optional[str] = ..., config: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., max_payload_bytes: _Optional[int] = ..., permitted_body_modes: _Optional[_Iterable[_Union[HttpResponseBodyMode, str]]] = ...) -> None: ... + +class HttpResponsePreflightResult(_message.Message): + __slots__ = ("skip", "inspect", "block_delivery", "reason", "reason_code", "findings", "metadata") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + SKIP_FIELD_NUMBER: _ClassVar[int] + INSPECT_FIELD_NUMBER: _ClassVar[int] + BLOCK_DELIVERY_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + skip: HttpResponsePreflightSkip + inspect: HttpResponsePreflightInspect + block_delivery: HttpResponseBlockDelivery + reason: str + reason_code: str + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + def __init__(self, skip: _Optional[_Union[HttpResponsePreflightSkip, _Mapping]] = ..., inspect: _Optional[_Union[HttpResponsePreflightInspect, _Mapping]] = ..., block_delivery: _Optional[_Union[HttpResponseBlockDelivery, _Mapping]] = ..., reason: _Optional[str] = ..., reason_code: _Optional[str] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class HttpResponsePreflightSkip(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class HttpResponseBlockDelivery(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class HttpResponsePreflightInspect(_message.Message): + __slots__ = ("body_mode", "header_mutations") + BODY_MODE_FIELD_NUMBER: _ClassVar[int] + HEADER_MUTATIONS_FIELD_NUMBER: _ClassVar[int] + body_mode: HttpResponseBodyMode + header_mutations: _containers.RepeatedCompositeFieldContainer[HeaderMutation] + def __init__(self, body_mode: _Optional[_Union[HttpResponseBodyMode, str]] = ..., header_mutations: _Optional[_Iterable[_Union[HeaderMutation, _Mapping]]] = ...) -> None: ... + +class HttpResponseBodyUnit(_message.Message): + __slots__ = ("sequence", "data", "end_of_stream") + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + END_OF_STREAM_FIELD_NUMBER: _ClassVar[int] + sequence: int + data: bytes + end_of_stream: bool + def __init__(self, sequence: _Optional[int] = ..., data: _Optional[bytes] = ..., end_of_stream: _Optional[bool] = ...) -> None: ... + +class HttpResponseBodyResult(_message.Message): + __slots__ = ("sequence", "pass_through", "transform", "block_delivery", "skip_remaining", "reason", "reason_code", "findings", "metadata") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + PASS_THROUGH_FIELD_NUMBER: _ClassVar[int] + TRANSFORM_FIELD_NUMBER: _ClassVar[int] + BLOCK_DELIVERY_FIELD_NUMBER: _ClassVar[int] + SKIP_REMAINING_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + sequence: int + pass_through: HttpResponseBodyPassThrough + transform: HttpResponseBodyTransform + block_delivery: HttpResponseBlockDelivery + skip_remaining: HttpResponseBodySkipRemaining + reason: str + reason_code: str + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + def __init__(self, sequence: _Optional[int] = ..., pass_through: _Optional[_Union[HttpResponseBodyPassThrough, _Mapping]] = ..., transform: _Optional[_Union[HttpResponseBodyTransform, _Mapping]] = ..., block_delivery: _Optional[_Union[HttpResponseBlockDelivery, _Mapping]] = ..., skip_remaining: _Optional[_Union[HttpResponseBodySkipRemaining, _Mapping]] = ..., reason: _Optional[str] = ..., reason_code: _Optional[str] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class HttpResponseBodyPassThrough(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class HttpResponseBodySkipRemaining(_message.Message): + __slots__ = ("pass_through", "transform") + PASS_THROUGH_FIELD_NUMBER: _ClassVar[int] + TRANSFORM_FIELD_NUMBER: _ClassVar[int] + pass_through: HttpResponseBodyPassThrough + transform: HttpResponseBodyTransform + def __init__(self, pass_through: _Optional[_Union[HttpResponseBodyPassThrough, _Mapping]] = ..., transform: _Optional[_Union[HttpResponseBodyTransform, _Mapping]] = ...) -> None: ... + +class HttpResponseBodyTransform(_message.Message): + __slots__ = ("data",) + DATA_FIELD_NUMBER: _ClassVar[int] + data: bytes + def __init__(self, data: _Optional[bytes] = ...) -> None: ... + +class HttpResponseTrailers(_message.Message): + __slots__ = ("headers",) + HEADERS_FIELD_NUMBER: _ClassVar[int] + headers: _containers.RepeatedCompositeFieldContainer[HttpHeader] + def __init__(self, headers: _Optional[_Iterable[_Union[HttpHeader, _Mapping]]] = ...) -> None: ... + +class HttpResponseTrailersResult(_message.Message): + __slots__ = ("trailer_mutations", "reason", "reason_code", "findings", "metadata") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + TRAILER_MUTATIONS_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + trailer_mutations: _containers.RepeatedCompositeFieldContainer[HeaderMutation] + reason: str + reason_code: str + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + def __init__(self, trailer_mutations: _Optional[_Iterable[_Union[HeaderMutation, _Mapping]]] = ..., reason: _Optional[str] = ..., reason_code: _Optional[str] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class MiddlewareSessionEnd(_message.Message): + __slots__ = ("reason", "protocol_error") + REASON_FIELD_NUMBER: _ClassVar[int] + PROTOCOL_ERROR_FIELD_NUMBER: _ClassVar[int] + reason: MiddlewareSessionEndReason + protocol_error: MiddlewareSessionProtocolError + def __init__(self, reason: _Optional[_Union[MiddlewareSessionEndReason, str]] = ..., protocol_error: _Optional[_Union[MiddlewareSessionProtocolError, _Mapping]] = ...) -> None: ... + +class MiddlewareSessionProtocolError(_message.Message): + __slots__ = ("web_socket", "middleware_exchange") + WEB_SOCKET_FIELD_NUMBER: _ClassVar[int] + MIDDLEWARE_EXCHANGE_FIELD_NUMBER: _ClassVar[int] + web_socket: WebSocketProtocolError + middleware_exchange: MiddlewareExchangeProtocolError + def __init__(self, web_socket: _Optional[_Union[WebSocketProtocolError, _Mapping]] = ..., middleware_exchange: _Optional[_Union[MiddlewareExchangeProtocolError, _Mapping]] = ...) -> None: ... + +class WebSocketProtocolError(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class MiddlewareExchangeProtocolError(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class WebSocketSessionEvent(_message.Message): + __slots__ = ("preflight", "session_start", "message", "session_end") + PREFLIGHT_FIELD_NUMBER: _ClassVar[int] + SESSION_START_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + SESSION_END_FIELD_NUMBER: _ClassVar[int] + preflight: WebSocketPreflight + session_start: WebSocketSessionStart + message: WebSocketMessage + session_end: MiddlewareSessionEnd + def __init__(self, preflight: _Optional[_Union[WebSocketPreflight, _Mapping]] = ..., session_start: _Optional[_Union[WebSocketSessionStart, _Mapping]] = ..., message: _Optional[_Union[WebSocketMessage, _Mapping]] = ..., session_end: _Optional[_Union[MiddlewareSessionEnd, _Mapping]] = ...) -> None: ... + +class WebSocketPreflight(_message.Message): + __slots__ = ("session_id", "phase", "context", "target", "requested_subprotocols", "middleware_name", "config") + SESSION_ID_FIELD_NUMBER: _ClassVar[int] + PHASE_FIELD_NUMBER: _ClassVar[int] + CONTEXT_FIELD_NUMBER: _ClassVar[int] + TARGET_FIELD_NUMBER: _ClassVar[int] + REQUESTED_SUBPROTOCOLS_FIELD_NUMBER: _ClassVar[int] + MIDDLEWARE_NAME_FIELD_NUMBER: _ClassVar[int] + CONFIG_FIELD_NUMBER: _ClassVar[int] + session_id: str + phase: SupervisorMiddlewarePhase + context: RequestContext + target: HttpRequestTarget + requested_subprotocols: _containers.RepeatedScalarFieldContainer[str] + middleware_name: str + config: _struct_pb2.Struct + def __init__(self, session_id: _Optional[str] = ..., phase: _Optional[_Union[SupervisorMiddlewarePhase, str]] = ..., context: _Optional[_Union[RequestContext, _Mapping]] = ..., target: _Optional[_Union[HttpRequestTarget, _Mapping]] = ..., requested_subprotocols: _Optional[_Iterable[str]] = ..., middleware_name: _Optional[str] = ..., config: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... + +class WebSocketSessionStart(_message.Message): + __slots__ = ("selected_subprotocol",) + SELECTED_SUBPROTOCOL_FIELD_NUMBER: _ClassVar[int] + selected_subprotocol: str + def __init__(self, selected_subprotocol: _Optional[str] = ...) -> None: ... + +class WebSocketMessage(_message.Message): + __slots__ = ("sequence", "text", "binary") + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + TEXT_FIELD_NUMBER: _ClassVar[int] + BINARY_FIELD_NUMBER: _ClassVar[int] + sequence: int + text: str + binary: bytes + def __init__(self, sequence: _Optional[int] = ..., text: _Optional[str] = ..., binary: _Optional[bytes] = ...) -> None: ... + +class WebSocketPreflightDecision(_message.Message): + __slots__ = ("action", "reason", "reason_code", "findings", "metadata") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + ACTION_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + action: WebSocketPreflightAction + reason: str + reason_code: str + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + def __init__(self, action: _Optional[_Union[WebSocketPreflightAction, str]] = ..., reason: _Optional[str] = ..., reason_code: _Optional[str] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class WebSocketMessageResult(_message.Message): + __slots__ = ("sequence", "decision", "text", "binary", "reason", "reason_code", "findings", "metadata") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + DECISION_FIELD_NUMBER: _ClassVar[int] + TEXT_FIELD_NUMBER: _ClassVar[int] + BINARY_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + sequence: int + decision: Decision + text: str + binary: bytes + reason: str + reason_code: str + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + def __init__(self, sequence: _Optional[int] = ..., decision: _Optional[_Union[Decision, str]] = ..., text: _Optional[str] = ..., binary: _Optional[bytes] = ..., reason: _Optional[str] = ..., reason_code: _Optional[str] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class WebSocketSessionEventResult(_message.Message): + __slots__ = ("preflight_decision", "message_result") + PREFLIGHT_DECISION_FIELD_NUMBER: _ClassVar[int] + MESSAGE_RESULT_FIELD_NUMBER: _ClassVar[int] + preflight_decision: WebSocketPreflightDecision + message_result: WebSocketMessageResult + def __init__(self, preflight_decision: _Optional[_Union[WebSocketPreflightDecision, _Mapping]] = ..., message_result: _Optional[_Union[WebSocketMessageResult, _Mapping]] = ...) -> None: ... + +class RequestContext(_message.Message): + __slots__ = ("request_id", "sandbox_id", "originating_process", "sandbox", "workspace") + REQUEST_ID_FIELD_NUMBER: _ClassVar[int] + SANDBOX_ID_FIELD_NUMBER: _ClassVar[int] + ORIGINATING_PROCESS_FIELD_NUMBER: _ClassVar[int] + SANDBOX_FIELD_NUMBER: _ClassVar[int] + WORKSPACE_FIELD_NUMBER: _ClassVar[int] + request_id: str + sandbox_id: str + originating_process: Process + sandbox: str + workspace: str + def __init__(self, request_id: _Optional[str] = ..., sandbox_id: _Optional[str] = ..., originating_process: _Optional[_Union[Process, _Mapping]] = ..., sandbox: _Optional[str] = ..., workspace: _Optional[str] = ...) -> None: ... + +class HttpRequestTarget(_message.Message): + __slots__ = ("scheme", "host", "port", "method", "path", "query") + SCHEME_FIELD_NUMBER: _ClassVar[int] + HOST_FIELD_NUMBER: _ClassVar[int] + PORT_FIELD_NUMBER: _ClassVar[int] + METHOD_FIELD_NUMBER: _ClassVar[int] + PATH_FIELD_NUMBER: _ClassVar[int] + QUERY_FIELD_NUMBER: _ClassVar[int] + scheme: str + host: str + port: int + method: str + path: str + query: str + def __init__(self, scheme: _Optional[str] = ..., host: _Optional[str] = ..., port: _Optional[int] = ..., method: _Optional[str] = ..., path: _Optional[str] = ..., query: _Optional[str] = ...) -> None: ... + +class Process(_message.Message): + __slots__ = ("binary", "pid", "ancestors") + BINARY_FIELD_NUMBER: _ClassVar[int] + PID_FIELD_NUMBER: _ClassVar[int] + ANCESTORS_FIELD_NUMBER: _ClassVar[int] + binary: str + pid: int + ancestors: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, binary: _Optional[str] = ..., pid: _Optional[int] = ..., ancestors: _Optional[_Iterable[str]] = ...) -> None: ... + +class Finding(_message.Message): + __slots__ = ("type", "label", "count", "confidence", "severity") + TYPE_FIELD_NUMBER: _ClassVar[int] + LABEL_FIELD_NUMBER: _ClassVar[int] + COUNT_FIELD_NUMBER: _ClassVar[int] + CONFIDENCE_FIELD_NUMBER: _ClassVar[int] + SEVERITY_FIELD_NUMBER: _ClassVar[int] + type: str + label: str + count: int + confidence: str + severity: str + def __init__(self, type: _Optional[str] = ..., label: _Optional[str] = ..., count: _Optional[int] = ..., confidence: _Optional[str] = ..., severity: _Optional[str] = ...) -> None: ... + +class WriteHeader(_message.Message): + __slots__ = ("name", "value", "on_existing") + NAME_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + ON_EXISTING_FIELD_NUMBER: _ClassVar[int] + name: str + value: str + on_existing: ExistingHeaderAction + def __init__(self, name: _Optional[str] = ..., value: _Optional[str] = ..., on_existing: _Optional[_Union[ExistingHeaderAction, str]] = ...) -> None: ... + +class RemoveHeader(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: str + def __init__(self, name: _Optional[str] = ...) -> None: ... + +class HeaderMutation(_message.Message): + __slots__ = ("write", "remove") + WRITE_FIELD_NUMBER: _ClassVar[int] + REMOVE_FIELD_NUMBER: _ClassVar[int] + write: WriteHeader + remove: RemoveHeader + def __init__(self, write: _Optional[_Union[WriteHeader, _Mapping]] = ..., remove: _Optional[_Union[RemoveHeader, _Mapping]] = ...) -> None: ... + +class HttpRequestResult(_message.Message): + __slots__ = ("decision", "reason", "body", "has_body", "header_mutations", "findings", "metadata", "reason_code") + class MetadataEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + DECISION_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + BODY_FIELD_NUMBER: _ClassVar[int] + HAS_BODY_FIELD_NUMBER: _ClassVar[int] + HEADER_MUTATIONS_FIELD_NUMBER: _ClassVar[int] + FINDINGS_FIELD_NUMBER: _ClassVar[int] + METADATA_FIELD_NUMBER: _ClassVar[int] + REASON_CODE_FIELD_NUMBER: _ClassVar[int] + decision: Decision + reason: str + body: bytes + has_body: bool + header_mutations: _containers.RepeatedCompositeFieldContainer[HeaderMutation] + findings: _containers.RepeatedCompositeFieldContainer[Finding] + metadata: _containers.ScalarMap[str, str] + reason_code: str + def __init__(self, decision: _Optional[_Union[Decision, str]] = ..., reason: _Optional[str] = ..., body: _Optional[bytes] = ..., has_body: _Optional[bool] = ..., header_mutations: _Optional[_Iterable[_Union[HeaderMutation, _Mapping]]] = ..., findings: _Optional[_Iterable[_Union[Finding, _Mapping]]] = ..., metadata: _Optional[_Mapping[str, str]] = ..., reason_code: _Optional[str] = ...) -> None: ... diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2_grpc.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2_grpc.py new file mode 100644 index 00000000..613a6c70 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/bindings/supervisor_middleware_pb2_grpc.py @@ -0,0 +1,327 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from egress_watchdog.bindings import supervisor_middleware_pb2 as supervisor__middleware__pb2 + +GRPC_GENERATED_VERSION = '1.81.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in supervisor_middleware_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class SupervisorMiddlewareStub: + """SupervisorMiddleware discovers and configures one operator-run middleware. + It evaluates HTTP requests and WebSocket messages before credentials. + Phase-specific services share the same registration. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Describe = channel.unary_unary( + '/openshell.middleware.v1.SupervisorMiddleware/Describe', + request_serializer=supervisor__middleware__pb2.MiddlewareDescribeRequest.SerializeToString, + response_deserializer=supervisor__middleware__pb2.MiddlewareManifest.FromString, + _registered_method=True) + self.ValidateConfig = channel.unary_unary( + '/openshell.middleware.v1.SupervisorMiddleware/ValidateConfig', + request_serializer=supervisor__middleware__pb2.ValidateConfigRequest.SerializeToString, + response_deserializer=supervisor__middleware__pb2.ValidateConfigResponse.FromString, + _registered_method=True) + self.EvaluateHttpRequest = channel.unary_unary( + '/openshell.middleware.v1.SupervisorMiddleware/EvaluateHttpRequest', + request_serializer=supervisor__middleware__pb2.HttpRequestEvaluation.SerializeToString, + response_deserializer=supervisor__middleware__pb2.HttpRequestResult.FromString, + _registered_method=True) + self.EvaluateWebSocketSession = channel.stream_stream( + '/openshell.middleware.v1.SupervisorMiddleware/EvaluateWebSocketSession', + request_serializer=supervisor__middleware__pb2.WebSocketSessionEvent.SerializeToString, + response_deserializer=supervisor__middleware__pb2.WebSocketSessionEventResult.FromString, + _registered_method=True) + + +class SupervisorMiddlewareServicer: + """SupervisorMiddleware discovers and configures one operator-run middleware. + It evaluates HTTP requests and WebSocket messages before credentials. + Phase-specific services share the same registration. + """ + + def Describe(self, request, context): + """Describe returns the service manifest and declared bindings. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ValidateConfig(self, request, context): + """ValidateConfig checks service-specific configuration for one binding. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def EvaluateHttpRequest(self, request, context): + """EvaluateHttpRequest returns an allow, deny, or mutation decision for one + buffered HTTP request. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def EvaluateWebSocketSession(self, request_iterator, context): + """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. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_SupervisorMiddlewareServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Describe': grpc.unary_unary_rpc_method_handler( + servicer.Describe, + request_deserializer=supervisor__middleware__pb2.MiddlewareDescribeRequest.FromString, + response_serializer=supervisor__middleware__pb2.MiddlewareManifest.SerializeToString, + ), + 'ValidateConfig': grpc.unary_unary_rpc_method_handler( + servicer.ValidateConfig, + request_deserializer=supervisor__middleware__pb2.ValidateConfigRequest.FromString, + response_serializer=supervisor__middleware__pb2.ValidateConfigResponse.SerializeToString, + ), + 'EvaluateHttpRequest': grpc.unary_unary_rpc_method_handler( + servicer.EvaluateHttpRequest, + request_deserializer=supervisor__middleware__pb2.HttpRequestEvaluation.FromString, + response_serializer=supervisor__middleware__pb2.HttpRequestResult.SerializeToString, + ), + 'EvaluateWebSocketSession': grpc.stream_stream_rpc_method_handler( + servicer.EvaluateWebSocketSession, + request_deserializer=supervisor__middleware__pb2.WebSocketSessionEvent.FromString, + response_serializer=supervisor__middleware__pb2.WebSocketSessionEventResult.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'openshell.middleware.v1.SupervisorMiddleware', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('openshell.middleware.v1.SupervisorMiddleware', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class SupervisorMiddleware: + """SupervisorMiddleware discovers and configures one operator-run middleware. + It evaluates HTTP requests and WebSocket messages before credentials. + Phase-specific services share the same registration. + """ + + @staticmethod + def Describe(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/openshell.middleware.v1.SupervisorMiddleware/Describe', + supervisor__middleware__pb2.MiddlewareDescribeRequest.SerializeToString, + supervisor__middleware__pb2.MiddlewareManifest.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ValidateConfig(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/openshell.middleware.v1.SupervisorMiddleware/ValidateConfig', + supervisor__middleware__pb2.ValidateConfigRequest.SerializeToString, + supervisor__middleware__pb2.ValidateConfigResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def EvaluateHttpRequest(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/openshell.middleware.v1.SupervisorMiddleware/EvaluateHttpRequest', + supervisor__middleware__pb2.HttpRequestEvaluation.SerializeToString, + supervisor__middleware__pb2.HttpRequestResult.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def EvaluateWebSocketSession(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/openshell.middleware.v1.SupervisorMiddleware/EvaluateWebSocketSession', + supervisor__middleware__pb2.WebSocketSessionEvent.SerializeToString, + supervisor__middleware__pb2.WebSocketSessionEventResult.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class HttpResponsePreReturnStub: + """HttpResponsePreReturn evaluates one response for one middleware stage before + OpenShell returns it to the sandbox. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Evaluate = channel.stream_stream( + '/openshell.middleware.v1.HttpResponsePreReturn/Evaluate', + request_serializer=supervisor__middleware__pb2.HttpResponseEvent.SerializeToString, + response_deserializer=supervisor__middleware__pb2.HttpResponseEventResult.FromString, + _registered_method=True) + + +class HttpResponsePreReturnServicer: + """HttpResponsePreReturn evaluates one response for one middleware stage before + OpenShell returns it to the sandbox. + """ + + def Evaluate(self, request_iterator, context): + """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. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_HttpResponsePreReturnServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Evaluate': grpc.stream_stream_rpc_method_handler( + servicer.Evaluate, + request_deserializer=supervisor__middleware__pb2.HttpResponseEvent.FromString, + response_serializer=supervisor__middleware__pb2.HttpResponseEventResult.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'openshell.middleware.v1.HttpResponsePreReturn', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('openshell.middleware.v1.HttpResponsePreReturn', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class HttpResponsePreReturn: + """HttpResponsePreReturn evaluates one response for one middleware stage before + OpenShell returns it to the sandbox. + """ + + @staticmethod + def Evaluate(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/openshell.middleware.v1.HttpResponsePreReturn/Evaluate', + supervisor__middleware__pb2.HttpResponseEvent.SerializeToString, + supervisor__middleware__pb2.HttpResponseEventResult.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/ledger.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/ledger.py new file mode 100644 index 00000000..708cefc0 --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/ledger.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Accounting of allowed egress per sandbox and host, and the byte budget.""" + +from __future__ import annotations + +import time +from collections import defaultdict, deque +from collections.abc import Callable, Mapping +from dataclasses import dataclass + +WINDOW_SECONDS = 60.0 +ON_EXCEED_ACTIONS = ("deny", "finding") + + +@dataclass(frozen=True) +class Budget: + """Request bytes one sandbox may send to one host in a rolling minute.""" + + bytes_per_minute: int + on_exceed: str = "deny" + + @classmethod + def from_config(cls, config: Mapping[str, object]) -> Budget | None: + """Read the `budget` block of a policy's middleware config, if any.""" + unknown = set(config) - {"budget"} + if unknown: + raise ValueError(f"unknown config keys: {sorted(unknown)}") + block = config.get("budget") + if block is None: + return None + if not isinstance(block, Mapping): + raise ValueError("budget must be a mapping") + unknown = set(block) - {"bytes_per_minute", "on_exceed"} + if unknown: + raise ValueError(f"unknown budget keys: {sorted(unknown)}") + limit = block.get("bytes_per_minute") + # Struct numbers arrive as floats. + if not isinstance(limit, int | float) or limit != int(limit) or limit <= 0: + raise ValueError("budget.bytes_per_minute must be a positive integer") + on_exceed = block.get("on_exceed", "deny") + if on_exceed not in ON_EXCEED_ACTIONS: + raise ValueError(f"budget.on_exceed must be one of {ON_EXCEED_ACTIONS}") + return cls(bytes_per_minute=int(limit), on_exceed=str(on_exceed)) + + +@dataclass(frozen=True) +class Verdict: + """What the watchdog decided for one request.""" + + allow: bool + window_bytes: int + reason_code: str = "" + reason: str = "" + finding: bool = False + + +class Ledger: + """Rolling per-sandbox, per-host byte counts and the quarantine set. + + A sandbox that exceeds a deny budget is quarantined: every later request it + makes through the watchdog is refused without further accounting. This is + stage one of containment and needs no gateway call. + """ + + def __init__(self, clock: Callable[[], float] = time.monotonic) -> None: + self._clock = clock + self._sends: dict[tuple[str, str], deque[tuple[float, int]]] = defaultdict(deque) + self._quarantined: dict[str, str] = {} + + def record( + self, sandbox_id: str, host: str, request_bytes: int, budget: Budget | None + ) -> Verdict: + if sandbox_id in self._quarantined: + return Verdict( + allow=False, + window_bytes=self.window_bytes(sandbox_id, host), + reason_code="sandbox_quarantined", + reason=f"sandbox quarantined: {self._quarantined[sandbox_id]}", + ) + sends = self._sends[(sandbox_id, host)] + now = self._clock() + while sends and sends[0][0] <= now - WINDOW_SECONDS: + sends.popleft() + projected = sum(size for _, size in sends) + request_bytes + + if budget is not None and projected > budget.bytes_per_minute: + reason = f"{host} byte budget exceeded: {projected} > {budget.bytes_per_minute}/min" + if budget.on_exceed == "deny": + self._quarantined[sandbox_id] = reason + return Verdict( + allow=False, + window_bytes=projected - request_bytes, + reason_code="egress_budget_exceeded", + reason=reason, + finding=True, + ) + sends.append((now, request_bytes)) + return Verdict( + allow=True, + window_bytes=projected, + reason_code="egress_budget_exceeded", + reason=reason, + finding=True, + ) + + sends.append((now, request_bytes)) + return Verdict(allow=True, window_bytes=projected) + + def window_bytes(self, sandbox_id: str, host: str) -> int: + return sum(size for _, size in self._sends.get((sandbox_id, host), ())) + + def is_quarantined(self, sandbox_id: str) -> bool: + return sandbox_id in self._quarantined diff --git a/projects/use-case-examples/agent-containment/src/egress_watchdog/server.py b/projects/use-case-examples/agent-containment/src/egress_watchdog/server.py new file mode 100644 index 00000000..efc2f0dc --- /dev/null +++ b/projects/use-case-examples/agent-containment/src/egress_watchdog/server.py @@ -0,0 +1,228 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Egress watchdog: an OpenShell supervisor middleware that accounts for allowed traffic. + +Every call is TLS and carries a gateway-signed Ed25519 JWT; there is no +insecure mode. The watchdog is a component sandbox nodes must not be able to +touch, so it never runs unauthenticated, even in development. +""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import sys +import time +from collections.abc import Sequence +from pathlib import Path +from typing import TextIO + +import grpc +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from google.protobuf import json_format + +from egress_watchdog.bindings import extension_pb2 as ext_pb2 +from egress_watchdog.bindings import supervisor_middleware_pb2 as pb2 +from egress_watchdog.bindings import supervisor_middleware_pb2_grpc as pb2_grpc +from egress_watchdog.ledger import Budget, Ledger + +SERVICE_NAME = "egress-watchdog" +SERVICE_VERSION = "0.1.0" +AUDIENCE = f"urn:openshell:extension:middleware:{SERVICE_NAME}" +# The supervisor skips middleware for larger bodies and applies on_error, so +# attach the watchdog with on_error: fail_closed to make this a request ceiling. +MAX_PAYLOAD_BYTES = 4 * 1024 * 1024 +MAX_MESSAGE_BYTES = MAX_PAYLOAD_BYTES + 1024 * 1024 + + +class Authenticator: + """Verify the gateway's extension JWT on each call.""" + + def __init__(self, public_key_pem: bytes, gateway_id: str) -> None: + key = serialization.load_pem_public_key(public_key_pem) + if not isinstance(key, Ed25519PublicKey): + raise ValueError("gateway JWT public key must be Ed25519") + self._key = key + self._issuer = f"openshell-gateway:{gateway_id}" + + def verify(self, metadata: Sequence[tuple[str, str | bytes]]) -> dict[str, object]: + """Return the token claims, or raise PermissionError.""" + values = [value for key, value in metadata if key == "authorization"] + if len(values) != 1 or not isinstance(values[0], str): + raise PermissionError("missing bearer token") + scheme, _, token = values[0].partition(" ") + if scheme.lower() != "bearer" or not token: + raise PermissionError("missing bearer token") + try: + header = jwt.get_unverified_header(token) + if header.get("typ") != "openshell-ext+jwt": + raise PermissionError("unexpected token type") + claims = jwt.decode( + token, + self._key, + algorithms=["EdDSA"], + audience=AUDIENCE, + issuer=self._issuer, + options={"require": ["exp", "aud", "iss"]}, + ) + except jwt.PyJWTError as error: + raise PermissionError(f"invalid token: {error}") from error + if claims.get("caller_kind") not in ("gateway", "supervisor"): + raise PermissionError("unexpected caller_kind") + return claims + + +def build_manifest() -> pb2.MiddlewareManifest: + return pb2.MiddlewareManifest( + name=SERVICE_NAME, + service_version=SERVICE_VERSION, + expected_audience=AUDIENCE, + bindings=[ + pb2.MiddlewareBinding( + operation=pb2.SUPERVISOR_MIDDLEWARE_OPERATION_HTTP_REQUEST, + phase=pb2.SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS, + max_payload_bytes=MAX_PAYLOAD_BYTES, + ) + ], + extension=ext_pb2.PeerMetadata( + protocol_version=ext_pb2.ProtocolVersion(major=1, minor=0), + implementation_name=f"openshell-research/{SERVICE_NAME}", + implementation_version=SERVICE_VERSION, + supported_capabilities=["openshell.supervisor-middleware.contract"], + ), + ) + + +def request_bytes(request: pb2.HttpRequestEvaluation) -> int: + """Bytes the request sends: the larger of Content-Length and the body we received.""" + declared = 0 + for header in request.headers: + if header.name.lower() == "content-length" and header.value.isdigit(): + declared = int(header.value) + return max(declared, len(request.body)) + + +class Watchdog(pb2_grpc.SupervisorMiddlewareServicer): + def __init__(self, authenticator: Authenticator, ledger: Ledger, log: TextIO) -> None: + self._authenticator = authenticator + self._ledger = ledger + self._log = log + + async def _authenticate(self, context: grpc.aio.ServicerContext) -> dict[str, object]: + try: + return self._authenticator.verify(context.invocation_metadata() or ()) + except PermissionError as error: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, str(error)) + raise # abort raises; this keeps type checkers honest + + async def Describe(self, request, context): + await self._authenticate(context) + return build_manifest() + + async def ValidateConfig(self, request, context): + await self._authenticate(context) + try: + Budget.from_config(json_format.MessageToDict(request.config)) + except ValueError as error: + return pb2.ValidateConfigResponse(valid=False, reason=str(error)) + return pb2.ValidateConfigResponse(valid=True) + + async def EvaluateHttpRequest(self, request, context): + claims = await self._authenticate(context) + sandbox_id = request.context.sandbox_id + if claims.get("caller_kind") != "supervisor" or claims.get("sandbox_id") != sandbox_id: + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, "token does not match the request's sandbox" + ) + if request.phase != pb2.SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS: + return pb2.HttpRequestResult( + decision=pb2.DECISION_DENY, + reason="unsupported middleware phase", + reason_code="unsupported_phase", + ) + + budget = Budget.from_config(json_format.MessageToDict(request.config)) + size = request_bytes(request) + verdict = self._ledger.record(sandbox_id, request.target.host, size, budget) + self._write_log_line(request, size, verdict.window_bytes, verdict) + + result = pb2.HttpRequestResult( + decision=pb2.DECISION_ALLOW if verdict.allow else pb2.DECISION_DENY, + reason=verdict.reason, + reason_code=verdict.reason_code, + ) + if verdict.finding: + result.findings.append( + pb2.Finding( + type="egress_budget", + label=request.target.host, + count=1, + confidence="high", + severity="high" if not verdict.allow else "medium", + ) + ) + return result + + def _write_log_line(self, request, size: int, window_bytes: int, verdict) -> None: + # One line per request is the accounting record. Never log bodies or queries. + line = { + "ts": round(time.time(), 3), + "sandbox_id": request.context.sandbox_id, + "sandbox": request.context.sandbox, + "binary": request.context.originating_process.binary, + "method": request.target.method, + "host": request.target.host, + "path": request.target.path, + "request_bytes": size, + "window_bytes": window_bytes, + "decision": "allow" if verdict.allow else "deny", + "reason_code": verdict.reason_code, + } + print(json.dumps(line), file=self._log, flush=True) + + +def create_server(watchdog: Watchdog) -> grpc.aio.Server: + server = grpc.aio.server( + options=( + ("grpc.max_receive_message_length", MAX_MESSAGE_BYTES), + ("grpc.max_send_message_length", MAX_MESSAGE_BYTES), + ) + ) + pb2_grpc.add_SupervisorMiddlewareServicer_to_server(watchdog, server) + return server + + +async def serve(arguments: argparse.Namespace) -> None: + authenticator = Authenticator(arguments.jwt_public_key.read_bytes(), arguments.gateway_id) + server = create_server(Watchdog(authenticator, Ledger(), sys.stdout)) + credentials = grpc.ssl_server_credentials( + [(arguments.tls_key.read_bytes(), arguments.tls_cert.read_bytes())] + ) + if server.add_secure_port(arguments.listen, credentials) == 0: + raise RuntimeError(f"could not bind watchdog to {arguments.listen}") + await server.start() + print(f"{SERVICE_NAME} listening on {arguments.listen}", file=sys.stderr, flush=True) + try: + await server.wait_for_termination() + finally: + await server.stop(grace=0) + + +def main(argv: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser(description="Run the two-box egress watchdog") + parser.add_argument("--listen", required=True, help="host:port to bind") + parser.add_argument("--tls-cert", type=Path, required=True) + parser.add_argument("--tls-key", type=Path, required=True) + parser.add_argument( + "--jwt-public-key", type=Path, required=True, help="gateway Ed25519 public.pem" + ) + parser.add_argument("--gateway-id", default="openshell") + asyncio.run(serve(parser.parse_args(argv))) + + +if __name__ == "__main__": + main() diff --git a/projects/use-case-examples/agent-containment/tests/test_ledger.py b/projects/use-case-examples/agent-containment/tests/test_ledger.py new file mode 100644 index 00000000..0dc7389b --- /dev/null +++ b/projects/use-case-examples/agent-containment/tests/test_ledger.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from egress_watchdog.ledger import Budget, Ledger + + +class FakeClock: + def __init__(self) -> None: + self.now = 1000.0 + + def __call__(self) -> float: + return self.now + + +def test_budget_from_config_reads_struct_floats() -> None: + budget = Budget.from_config({"budget": {"bytes_per_minute": 1024.0, "on_exceed": "finding"}}) + assert budget == Budget(bytes_per_minute=1024, on_exceed="finding") + + +def test_no_budget_block_means_accounting_only() -> None: + assert Budget.from_config({}) is None + + +@pytest.mark.parametrize( + "config", + [ + {"budgets": {}}, + {"budget": {"bytes_per_minute": 0}}, + {"budget": {"bytes_per_minute": 1.5}}, + {"budget": {"bytes_per_minute": 10, "on_exceed": "stop"}}, + {"budget": {"bytes_per_minute": 10, "requests_per_minute": 5}}, + ], +) +def test_invalid_config_is_rejected(config: dict) -> None: + with pytest.raises(ValueError): + Budget.from_config(config) + + +def test_deny_budget_quarantines_the_sandbox() -> None: + ledger = Ledger(clock=FakeClock()) + budget = Budget(bytes_per_minute=100) + + assert ledger.record("sb-1", "httpbin.org", 60, budget).allow + over = ledger.record("sb-1", "httpbin.org", 60, budget) + assert not over.allow + assert over.reason_code == "egress_budget_exceeded" + assert over.finding + + # Stage one: every later request is refused, even to other hosts and with no budget. + later = ledger.record("sb-1", "pypi.org", 1, None) + assert not later.allow + assert later.reason_code == "sandbox_quarantined" + assert ledger.record("sb-2", "httpbin.org", 60, budget).allow + + +def test_window_rolls_after_a_minute() -> None: + clock = FakeClock() + ledger = Ledger(clock=clock) + budget = Budget(bytes_per_minute=100) + + assert ledger.record("sb-1", "httpbin.org", 90, budget).allow + clock.now += 61 + verdict = ledger.record("sb-1", "httpbin.org", 90, budget) + assert verdict.allow + assert verdict.window_bytes == 90 + + +def test_finding_budget_allows_and_flags() -> None: + ledger = Ledger(clock=FakeClock()) + budget = Budget(bytes_per_minute=100, on_exceed="finding") + + ledger.record("sb-1", "httpbin.org", 90, budget) + verdict = ledger.record("sb-1", "httpbin.org", 90, budget) + assert verdict.allow + assert verdict.finding + assert not ledger.is_quarantined("sb-1") diff --git a/projects/use-case-examples/agent-containment/tests/test_server.py b/projects/use-case-examples/agent-containment/tests/test_server.py new file mode 100644 index 00000000..0d5d2b2e --- /dev/null +++ b/projects/use-case-examples/agent-containment/tests/test_server.py @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import io +import json +import time + +import grpc +import jwt +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from google.protobuf import struct_pb2 + +from egress_watchdog.bindings import supervisor_middleware_pb2 as pb2 +from egress_watchdog.bindings import supervisor_middleware_pb2_grpc as pb2_grpc +from egress_watchdog.ledger import Ledger +from egress_watchdog.server import AUDIENCE, Authenticator, Watchdog, build_manifest, create_server + +SIGNING_KEY = Ed25519PrivateKey.generate() +PUBLIC_PEM = SIGNING_KEY.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo +) + + +def make_token(**overrides: object) -> str: + claims: dict[str, object] = { + "iss": "openshell-gateway:openshell", + "aud": AUDIENCE, + "exp": int(time.time()) + 300, + "caller_kind": "supervisor", + "sandbox_id": "sb-1", + } + headers = {"typ": overrides.pop("typ", "openshell-ext+jwt")} + claims.update(overrides) + return jwt.encode(claims, SIGNING_KEY, algorithm="EdDSA", headers=headers) + + +def bearer(token: str) -> list[tuple[str, str]]: + return [("authorization", f"Bearer {token}")] + + +def test_manifest_declares_protocol_and_audience() -> None: + manifest = build_manifest() + assert manifest.expected_audience == AUDIENCE + assert manifest.extension.protocol_version.major == 1 + assert "openshell.supervisor-middleware.contract" in manifest.extension.supported_capabilities + + +def test_authenticator_accepts_a_gateway_token() -> None: + claims = Authenticator(PUBLIC_PEM, "openshell").verify(bearer(make_token())) + assert claims["sandbox_id"] == "sb-1" + + +@pytest.mark.parametrize( + "metadata", + [ + [], + bearer(make_token(aud="urn:openshell:extension:middleware:other")), + bearer(make_token(iss="openshell-gateway:someone-else")), + bearer(make_token(typ="JWT")), + bearer(make_token(exp=int(time.time()) - 10)), + bearer(make_token(caller_kind="user")), + bearer( + jwt.encode( + {"iss": "openshell-gateway:openshell", "aud": AUDIENCE, "exp": 2**31}, + Ed25519PrivateKey.generate(), + algorithm="EdDSA", + headers={"typ": "openshell-ext+jwt"}, + ) + ), + ], + ids=["missing", "audience", "issuer", "typ", "expired", "caller", "foreign-key"], +) +def test_authenticator_rejects(metadata: list[tuple[str, str]]) -> None: + with pytest.raises(PermissionError): + Authenticator(PUBLIC_PEM, "openshell").verify(metadata) + + +def evaluation(sandbox_id: str, size: int, budget: int | None) -> pb2.HttpRequestEvaluation: + config = struct_pb2.Struct() + if budget is not None: + config.update({"budget": {"bytes_per_minute": budget}}) + return pb2.HttpRequestEvaluation( + phase=pb2.SUPERVISOR_MIDDLEWARE_PHASE_PRE_CREDENTIALS, + context=pb2.RequestContext(sandbox_id=sandbox_id, sandbox="demo"), + target=pb2.HttpRequestTarget(scheme="https", host="httpbin.org", method="POST", path="/"), + headers=[pb2.HttpHeader(name="Content-Length", value=str(size))], + config=config, + ) + + +async def run_against_server(calls): + log = io.StringIO() + server = create_server(Watchdog(Authenticator(PUBLIC_PEM, "openshell"), Ledger(), log)) + port = server.add_insecure_port("127.0.0.1:0") + await server.start() + try: + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + return await calls(pb2_grpc.SupervisorMiddlewareStub(channel)), log.getvalue() + finally: + await server.stop(grace=0) + + +def test_budget_denies_and_logs_over_grpc() -> None: + async def calls(stub): + metadata = bearer(make_token()) + first = await stub.EvaluateHttpRequest(evaluation("sb-1", 600, 1000), metadata=metadata) + second = await stub.EvaluateHttpRequest(evaluation("sb-1", 600, 1000), metadata=metadata) + return first, second + + (first, second), log = asyncio.run(run_against_server(calls)) + assert first.decision == pb2.DECISION_ALLOW + assert second.decision == pb2.DECISION_DENY + assert second.reason_code == "egress_budget_exceeded" + lines = [json.loads(line) for line in log.splitlines()] + assert [line["decision"] for line in lines] == ["allow", "deny"] + assert lines[0]["request_bytes"] == 600 + + +def test_token_for_another_sandbox_is_refused() -> None: + async def calls(stub): + with pytest.raises(grpc.aio.AioRpcError) as error: + await stub.EvaluateHttpRequest( + evaluation("sb-2", 1, None), metadata=bearer(make_token(sandbox_id="sb-1")) + ) + return error.value.code() + + code, _ = asyncio.run(run_against_server(calls)) + assert code == grpc.StatusCode.PERMISSION_DENIED + + +def test_unauthenticated_call_is_refused() -> None: + async def calls(stub): + with pytest.raises(grpc.aio.AioRpcError) as error: + await stub.Describe(pb2.MiddlewareDescribeRequest()) + return error.value.code() + + code, _ = asyncio.run(run_against_server(calls)) + assert code == grpc.StatusCode.UNAUTHENTICATED diff --git a/projects/use-case-examples/agent-containment/uv.lock b/projects/use-case-examples/agent-containment/uv.lock new file mode 100644 index 00000000..96d15409 --- /dev/null +++ b/projects/use-case-examples/agent-containment/uv.lock @@ -0,0 +1,457 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e6d153808bf353e152abd2fd4d8f09670d956ac78379ac46e60d7efbf04c/cryptography-50.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81", size = 3873097, upload-time = "2026-09-30T15:29:56.097Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, +] + +[[package]] +name = "egress-watchdog" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "pyjwt", extra = ["crypto"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "grpcio-tools" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "grpcio", specifier = ">=1.81.1,<2" }, + { name = "protobuf", specifier = ">=6.33.5,<7" }, + { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10,<3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "grpcio-tools", specifier = ">=1.81.1,<2" }, + { name = "pytest", specifier = ">=8,<10" }, + { name = "ruff", specifier = ">=0.12,<1" }, +] + +[[package]] +name = "grpcio" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" }, + { url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" }, + { url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" }, + { url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" }, + { url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" }, + { url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" }, + { url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" }, + { url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" }, + { url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pycparser" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]