# Python Extensions, Gateway & Sandbox Router

> Optional Python add-ons: computer-use extension, GKE pod-snapshot extensions, the sandbox-router service, and the kind-based gateway harness.

- Repository: kubernetes-sigs/agent-sandbox
- GitHub: https://github.com/kubernetes-sigs/agent-sandbox
- Human wiki: https://grok-wiki.com/public/wiki/kubernetes-sigs-agent-sandbox-c3f2597a654a
- Complete Markdown: https://grok-wiki.com/public/wiki/kubernetes-sigs-agent-sandbox-c3f2597a654a/llms-full.txt

## Source Files

- `clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py`
- `clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions`
- `clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py`
- `clients/python/agentic-sandbox-client/sandbox-router/README.md`
- `clients/python/agentic-sandbox-client/gateway-kind/README.md`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py](clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py)
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/README.md](clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/README.md)
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/podsnapshot_client.py](clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/podsnapshot_client.py)
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/sandbox_with_snapshot_support.py](clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/sandbox_with_snapshot_support.py)
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/snapshot_engine.py](clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/snapshot_engine.py)
- [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/README.md](clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/README.md)
- [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py](clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py)
- [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.yaml](clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.yaml)
- [clients/python/agentic-sandbox-client/sandbox-router/gateway.yaml](clients/python/agentic-sandbox-client/sandbox-router/gateway.yaml)
- [clients/python/agentic-sandbox-client/sandbox-router/README.md](clients/python/agentic-sandbox-client/sandbox-router/README.md)
- [clients/python/agentic-sandbox-client/sandbox-router/Dockerfile](clients/python/agentic-sandbox-client/sandbox-router/Dockerfile)
- [clients/python/agentic-sandbox-client/gateway-kind/README.md](clients/python/agentic-sandbox-client/gateway-kind/README.md)
- [clients/python/agentic-sandbox-client/gateway-kind/gateway-kind.yaml](clients/python/agentic-sandbox-client/gateway-kind/gateway-kind.yaml)
- [clients/python/agentic-sandbox-client/gateway-kind/run-test-kind.sh](clients/python/agentic-sandbox-client/gateway-kind/run-test-kind.sh)
</details>

# Python Extensions, Gateway & Sandbox Router

The Python client ships a small but layered set of optional add‑ons that live outside the core `SandboxClient`/`Sandbox` pair. Two of them are pure client subclasses — the `computer_use` extension and the GKE Pod Snapshot extension — that bolt extra HTTP verbs or Kubernetes operations onto a sandbox handle without changing core code. The other two are deployment surfaces: a FastAPI reverse proxy (`sandbox-router`) that fronts the cluster on a single Gateway IP, and a `gateway-kind` harness that wires the same routing into a local KinD cluster via `cloud-provider-kind`. Together they form the optional perimeter that turns the in‑cluster CRDs from the controller into something a Python program can reach from outside the cluster, and that lets sandboxes do more than execute shell commands.

This page covers what each piece is, how they compose, and the contracts they expose. The router is the linchpin: it is required by the `computer_use` extension, by Gateway Mode in the SDK, and by the KinD harness.

## High-level Topology

The four pieces fit into one request path and one in‑cluster control path, both of which the SDK can exercise from a developer laptop.

```mermaid
flowchart LR
    subgraph Client["Python SDK process"]
      CU["ComputerUseSandboxClient<br/>(extensions.computer_use)"]
      PS["PodSnapshotSandboxClient<br/>(gke_extensions.snapshots)"]
      Base["SandboxClient / Sandbox"]
    end

    subgraph Gateway["Edge"]
      GKE["GKE Gateway<br/>(gateway.yaml)"]
      KIND["KinD Gateway<br/>(gateway-kind.yaml +<br/>cloud-provider-kind)"]
    end

    subgraph Router["sandbox-router (FastAPI)"]
      Svc["sandbox-router-svc<br/>ClusterIP :8080"]
      Pods["router pods<br/>uvicorn sandbox_router:app"]
    end

    subgraph K8s["agent-sandbox in-cluster"]
      SBox["Sandbox Pod<br/>id.ns.svc.cluster.local:port"]
      Snap["PodSnapshot /<br/>PodSnapshotManualTrigger CRs"]
    end

    CU -- "HTTP POST /agent + X-Sandbox-* headers" --> GKE
    CU -. "alternate path" .-> KIND
    GKE --> Svc --> Pods --> SBox
    KIND --> Svc

    Base -- "kube-apiserver" --> SBox
    PS -- "Custom Objects API" --> Snap
    Snap -- "manages" --> SBox
```

Sources: [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py:74-128](), [clients/python/agentic-sandbox-client/sandbox-router/gateway.yaml:1-52](), [clients/python/agentic-sandbox-client/gateway-kind/gateway-kind.yaml:1-32](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py:20-39]()

## Computer-Use Extension (`extensions/computer_use.py`)

The computer-use extension is the smallest of the four components: a pair of subclasses that wire a single new HTTP verb (`POST /agent`) onto the sandbox handle. It is meant for sandboxes that run a virtual-desktop / browser agent runtime inside the pod, where the SDK does not want to exec shells but instead post a natural-language query.

`SandboxWithComputerUseSupport` extends `Sandbox` and adds `agent(query: str, timeout: int = 60) -> ExecutionResult`. The method checks `self.is_active`, builds a `{"query": query}` payload, sends it through `self.connector` as an HTTP `POST` to the `agent` path, and parses the JSON body back into an `ExecutionResult` (so callers get the same `stdout`/`stderr`/`exit_code` shape as `execute()`). The whole method is decorated with `@trace_span("agent_query")` so that traces emitted by the SDK include a span for each agent call.

`ComputerUseSandboxClient` is a typed `SandboxClient[SandboxWithComputerUseSupport]` that simply overrides `sandbox_class`. Because the base client constructs and re‑hydrates handles through this class attribute, any sandbox returned by `create_sandbox(...)` or `get_sandbox(...)` is already a `SandboxWithComputerUseSupport` instance with `.agent()` available.

```python
# clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py
class SandboxWithComputerUseSupport(Sandbox):
    @trace_span("agent_query")
    def agent(self, query: str, timeout: int = 60) -> ExecutionResult:
        if not self.is_active:
            raise ConnectionError("Sandbox is not active. Cannot execute agent queries.")
        payload = {"query": query}
        response = self.connector.send_request("POST", "agent", json=payload, timeout=timeout)
        response_data = response.json()
        return ExecutionResult(**(response_data or {}))
```

The extension does not provision the runtime itself — the README points users at `examples/gemini-cu-sandbox/sandbox-gemini-computer-use.yaml` for the `SandboxTemplate` and at a `gemini-api-key` `Secret` for credentials.

Sources: [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py:15-39](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/README.md:6-43]()

## GKE Pod Snapshot Extension (`gke_extensions/snapshots/`)

The GKE Pod Snapshot extension wraps Google's experimental `podsnapshot.gke.io` CRDs into a sandbox‑centric API. It assumes a GKE Standard cluster running gVisor with the `PodSnapshot` controller installed; if the CRDs are missing the client refuses to construct.

### Class layout

```mermaid
classDiagram
    class SandboxClient~T~
    class Sandbox

    class PodSnapshotSandboxClient {
      +sandbox_class = SandboxWithSnapshotSupport
      -_check_snapshot_crd_installed() bool
    }
    class SandboxWithSnapshotSupport {
      -_snapshots: SnapshotEngine
      +snapshots: SnapshotEngine
      +suspend(snapshot_before_suspend, wait_timeout) SuspendResponse
      +resume(wait_timeout) ResumeResponse
      +is_suspended() bool
      +is_restored_from_snapshot(uid) RestoreCheckResult
      +terminate()
    }
    class SnapshotEngine {
      +create(trigger_name, timeout) SnapshotResponse
      +list(filter_by) ListSnapshotResult
      +delete(uid, timeout) DeleteSnapshotResult
      +delete_all(delete_by, label_value, timeout) DeleteSnapshotResult
      +delete_manual_triggers(max_retries)
    }

    SandboxClient <|-- PodSnapshotSandboxClient
    Sandbox <|-- SandboxWithSnapshotSupport
    SandboxWithSnapshotSupport o--> SnapshotEngine
    PodSnapshotSandboxClient ..> SandboxWithSnapshotSupport : sandbox_class
```

`PodSnapshotSandboxClient.__init__` calls `_check_snapshot_crd_installed()` which lists API resources under `PODSNAPSHOT_API_GROUP`/`PODSNAPSHOT_API_VERSION` and looks for `PODSNAPSHOT_API_KIND`. It treats `403` and `404` from the API server as "not installed" and raises `RuntimeError("Pod Snapshot Controller is not ready. ...")` otherwise.

Sources: [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/podsnapshot_client.py:24-63]()

### `SnapshotEngine` — CRD wrapper

`SnapshotEngine` is the only piece that touches `podsnapshot.gke.io` CRDs directly. `create()` builds a `PodSnapshotManualTrigger` manifest, sanitizes the trigger name into K8s-safe characters (`lower`, `_`→`-`, truncated to 38 characters before a `-{timestamp}-{suffix}` tail to stay under the 63‑char limit, defaulted to `snap` if empty), creates it via the Custom Objects API, and then blocks on `wait_for_snapshot_to_be_completed` starting from the freshly returned `resourceVersion` to avoid losing a watch event. Created trigger names are appended to `self.created_manual_triggers` so that `delete_manual_triggers(max_retries=3)` can later sweep them up on `terminate()`.

`list()` always scopes by `SANDBOX_NAME_HASH_LABEL={hash}`, optionally ANDs grouping labels, and sorts by `creationTimestamp` descending. `_execute_deletion()` is shared by `delete(uid)` and `delete_all(delete_by="all"|"labels")`; it calls `delete_namespaced_custom_object` followed by `wait_for_snapshot_deletion` to confirm removal.

Sources: [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/snapshot_engine.py:87-261](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/snapshot_engine.py:397-547]()

### Suspend / Resume lifecycle

`SandboxWithSnapshotSupport` maps suspend/resume onto `spec.replicas` of the `Sandbox` CR.

```mermaid
stateDiagram-v2
    [*] --> Running
    Running --> Snapshotting : suspend(snapshot_before_suspend=True)
    Snapshotting --> Suspending : SnapshotResponse.success
    Snapshotting --> Failed : snapshot failed
    Running --> Suspending : suspend(snapshot_before_suspend=False)
    Suspending --> Suspended : pod terminated\n(wait_for_pod_termination)
    Suspending --> Failed : timeout
    Suspended --> Resuming : resume()
    Resuming --> Restored : pod ready &&\nis_restored_from_snapshot(uid).success
    Resuming --> NotRestored : pod ready, no prior snapshot
    Resuming --> Failed : timeout / verification fail
    Restored --> Running
    NotRestored --> Running
```

`suspend()` first cheaply checks `is_suspended()` (defined as `spec.replicas == 0` on the `Sandbox` CR, with informational logs when `podIPs` lag the spec). It then forces a resolution of `get_sandbox_name_hash()` *before* scaling — once `replicas` hits zero the pod is gone and that label can no longer be read — and only then optionally calls `snapshots.create("suspend-{sandbox_id}")`, patches `spec.replicas=0`, and waits for the pod to terminate by UID. `resume()` is symmetric: it captures `_get_latest_snapshot_uid()` *before* scaling back to 1, then after `wait_for_pod_ready` it calls `is_restored_from_snapshot(uid)` to verify the restore actually happened. If no prior snapshot existed it returns success with `restored_from_snapshot=False` rather than treating it as an error. `terminate()` always calls `_snapshots.delete_manual_triggers()` in a `try/finally` so cleanup runs even if termination throws.

Sources: [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/sandbox_with_snapshot_support.py:48-133](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/sandbox_with_snapshot_support.py:144-329]()

### Result types

| Type | Defined in | Fields |
|---|---|---|
| `SuspendResponse` | `sandbox_with_snapshot_support.py` | `success`, `snapshot_response`, `error_reason`, `error_code` |
| `ResumeResponse` | `sandbox_with_snapshot_support.py` | `success`, `restored_from_snapshot`, `snapshot_uid`, `error_reason`, `error_code` |
| `SnapshotResponse` | `snapshot_engine.py` | `success`, `trigger_name`, `snapshot_uid`, `snapshot_timestamp`, `error_reason`, `error_code` |
| `ListSnapshotResult` | `snapshot_engine.py` | `success`, `snapshots: list[SnapshotDetail]`, `error_reason`, `error_code` |
| `DeleteSnapshotResult` | `snapshot_engine.py` | `success`, `deleted_snapshots: list[str]`, `error_reason`, `error_code` |
| `SnapshotFilter` | `snapshot_engine.py` | `ready_only=True`, `grouping_labels`, `extra="forbid"` |

All responses use a `SUCCESS_CODE = 0` / `ERROR_CODE = 1` convention shared across the module so callers can branch on `error_code` or on the boolean `success`.

Sources: [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/snapshot_engine.py:30-86](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/sandbox_with_snapshot_support.py:28-47]()

## `sandbox-router` Service

`sandbox-router` is a FastAPI + httpx + uvicorn process whose only job is "look at headers, build an in‑cluster URL, stream the request through". It exists because creating an `HTTPRoute` per ephemeral sandbox would not scale; a single static route on the Gateway hands every request to this service instead.

### Request handling

```mermaid
sequenceDiagram
    participant Client
    participant Gateway as Gateway (gateway.yaml / gateway-kind.yaml)
    participant Router as sandbox-router pod
    participant Sandbox as Sandbox pod

    Client->>Gateway: HTTP request<br/>X-Sandbox-ID, X-Sandbox-Namespace,<br/>X-Sandbox-Port, X-Sandbox-Pod-IP?
    Gateway->>Router: HTTPRoute -> sandbox-router-svc:8080
    Router->>Router: validate headers<br/>sanitize namespace<br/>parse port
    alt X-Sandbox-Pod-IP present
        Router->>Sandbox: http://{pod_ip}:{port}/{path}
    else fallback
        Router->>Sandbox: http://{id}.{ns}.svc.{cluster_domain}:{port}/{path}
    end
    Sandbox-->>Router: response (streamed)
    Router-->>Gateway: StreamingResponse
    Gateway-->>Client: response
```

The single catch‑all route at `@app.api_route("/{full_path:path}", methods=['GET','POST','PUT','DELETE','PATCH'])` enforces the contract:

- `X-Sandbox-ID` is required; missing returns `HTTP 400 "X-Sandbox-ID header is required."`.
- `X-Sandbox-Namespace` defaults to `default`; the router rejects anything where `namespace.replace("-", "").isalnum()` is false, explicitly to "prevent DNS injection".
- `X-Sandbox-Port` defaults to `8888` and must parse as `int`.
- `X-Sandbox-Pod-IP`, when present, short-circuits DNS and is used as the target host directly; otherwise the router constructs `f"{sandbox_id}.{namespace}.svc.{cluster_domain}"`.
- The original `Host` header is filtered out before forwarding; all other headers are preserved.
- Upstream connection failures map to `HTTP 502` ("Could not connect to the backend sandbox"), other exceptions to `HTTP 500`.

The response is returned as a `StreamingResponse` over `resp.aiter_bytes()` so large or long-lived bodies do not have to be buffered in router memory. A separate `GET /healthz` always returns `{"status":"ok"}` and is what the GKE `HealthCheckPolicy` and the pod's readiness/liveness probes hit.

Sources: [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py:68-137]()

### Configuration

The router reads two environment variables at import time and logs the effective values.

| Variable | Default | Behavior |
|---|---|---|
| `PROXY_TIMEOUT_SECONDS` | `180.0` | Passed to the shared `httpx.AsyncClient(timeout=...)`. Non‑numeric or non‑positive values log a warning and fall back to the default. |
| `CLUSTER_DOMAIN` | `cluster.local` | Used when building the in‑cluster DNS target. Empty strings log a warning and fall back to the default. |

```python
# clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py
cluster_domain = _get_cluster_domain()
proxy_timeout = _get_proxy_timeout()
client = httpx.AsyncClient(timeout=proxy_timeout)
```

Sources: [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.py:26-65](), [clients/python/agentic-sandbox-client/sandbox-router/README.md:46-66]()

### Deployment manifest

`sandbox_router.yaml` defines the `sandbox-router-svc` `ClusterIP` (port `8080` → targetPort `8080`) and a `sandbox-router-deployment` with `replicas: 2` for HA, a `topologySpreadConstraints` block on `topology.kubernetes.io/zone`, readiness/liveness probes against `/healthz`, and `securityContext: { runAsUser: 1000, runAsGroup: 1000 }`. The image reference is templated as `${ROUTER_IMAGE}` and resolved by the deployment scripts via `sed`. The container itself comes from the `Dockerfile`, which installs hashed dependencies (`pip install --no-cache-dir --require-hashes -r requirements.txt`), drops to UID `1000`, exposes `8080`, and runs `uvicorn sandbox_router:app --host 0.0.0.0 --port 8080`.

Sources: [clients/python/agentic-sandbox-client/sandbox-router/sandbox_router.yaml:1-70](), [clients/python/agentic-sandbox-client/sandbox-router/Dockerfile:1-22]()

### Front-door Gateway (GKE)

`gateway.yaml` provisions:

- A `gateway.networking.k8s.io/v1` `Gateway` named `external-http-gateway` using `gatewayClassName: gke-l7-global-external-managed`.
- A single `HTTPRoute` (`sandbox-router-route`) with `path.type=PathPrefix, value=/` that points every request at `sandbox-router-svc:8080`.
- A GKE‑specific `networking.gke.io/v1 HealthCheckPolicy` that overrides the GKE Gateway's default `/` health check to use `/healthz` against the same service.

The README is explicit that the manifest is GKE-specific and that other implementations require swapping the `gatewayClassName` and dropping `HealthCheckPolicy`.

Sources: [clients/python/agentic-sandbox-client/sandbox-router/gateway.yaml:1-52](), [clients/python/agentic-sandbox-client/sandbox-router/README.md:68-78]()

## `gateway-kind` Harness

`gateway-kind/` is the local-developer equivalent of `gateway.yaml`. KinD has no native load balancer, so this directory pairs `cloud-provider-kind` (installed via `make deploy-cloud-provider-kind`) with a `Gateway` whose `gatewayClassName: cloud-provider-kind` lets that controller assign a real IP.

`gateway-kind.yaml` is intentionally minimal: a `v1 Gateway` named `kind-gateway` listening on port `80/HTTP` with `allowedRoutes.namespaces.from: All`, plus a `v1beta1 HTTPRoute` (`sandbox-router-route`) routing `PathPrefix: /` to `sandbox-router-svc:8080`. There is no GKE‑specific health-check policy here; KinD's load balancer does not need one.

The README walks operators through the manual flow (`make build`, `make deploy-kind EXTENSIONS=true`, `make deploy-cloud-provider-kind`, applying the router and template manifests with the image tag substituted in, then `kubectl apply -f gateway-kind.yaml`) and confirms success by polling `kubectl get gateway` until `ADDRESS` is populated and finally running `python ../test_client.py --gateway-name="kind-gateway"`.

`run-test-kind.sh` automates the same sequence: it resolves an `IMAGE_TAG` via the `dev/tools/shared/utils.get_image_tag` helper, exports `SANDBOX_ROUTER_IMG=kind.local/sandbox-router:${IMAGE_TAG}` and `SANDBOX_PYTHON_RUNTIME_IMG=kind.local/python-runtime-sandbox:${IMAGE_TAG}`, applies the templated manifests, `kubectl rollout status` waits for the router deployment, `kubectl wait --for=condition=Programmed=True gateway/kind-gateway` waits for the gateway, then spins up a `.venv`, installs the SDK in editable mode, and runs `python3 ./test_client.py --gateway-name kind-gateway`. A `trap cleanup EXIT` tears down the venv, `cloud-provider-kind`, and the KinD cluster.

Sources: [clients/python/agentic-sandbox-client/gateway-kind/gateway-kind.yaml:1-32](), [clients/python/agentic-sandbox-client/gateway-kind/README.md:1-86](), [clients/python/agentic-sandbox-client/gateway-kind/run-test-kind.sh:17-99]()

## How the Pieces Compose

```text
+-------------------------+        +------------------------------+
| ComputerUseSandboxClient|        | PodSnapshotSandboxClient     |
|  (subclass of           |        |  (subclass of SandboxClient, |
|   SandboxClient)        |        |   asserts PodSnapshot CRDs)  |
+-----------+-------------+        +--------------+---------------+
            | sandbox_class                        | sandbox_class
            v                                      v
+-------------------------+        +------------------------------+
| SandboxWithComputerUse  |        | SandboxWithSnapshotSupport   |
|  .agent(query) -> POST  |        |  .snapshots: SnapshotEngine  |
|     /agent              |        |  .suspend()/.resume()        |
+-----------+-------------+        +--------------+---------------+
            |   HTTP via SDK connector            |  kube-apiserver
            v                                      v
+------------------------------+   +------------------------------+
| sandbox-router (FastAPI)     |   | podsnapshot.gke.io CRDs      |
|  Service: sandbox-router-svc |   |  (PodSnapshot,               |
|  Route: /{full_path:path}    |   |   PodSnapshotManualTrigger)  |
+--------------+---------------+   +------------------------------+
               ^
               | HTTPRoute parentRef
               |
+----------------------------------+
| Gateway (gke-l7-global-external- |
| managed) | (cloud-provider-kind) |
+----------------------------------+
```

The two SDK extensions are independent — they subclass different things and never talk to each other — but they share infrastructure assumptions. The computer-use path needs the router and a Gateway because the SDK reaches the in‑pod agent over HTTP; the snapshot path does not need the router for snapshotting itself (it talks to the API server), but a real workflow normally pairs it with the same Gateway+router so the resumed sandbox is still reachable. `gateway-kind` exists exclusively to let a developer reproduce the GKE Gateway path locally on KinD, so a `python test_client.py --gateway-name=kind-gateway` exercises the same router code that production does.

In short: the SDK extensions teach the `Sandbox` handle new tricks (a new HTTP verb, a CRD-driven lifecycle), the router gives the cluster a single front door that scales to thousands of ephemeral sandboxes by routing on a header, and `gateway-kind` makes that same front door work without a cloud load balancer.

Sources: [clients/python/agentic-sandbox-client/sandbox-router/README.md:1-25](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/extensions/computer_use.py:35-39](), [clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/snapshots/podsnapshot_client.py:24-44]()
