From 140a42c01f3f230eaa02e0f7bdb724f0b2587431 Mon Sep 17 00:00:00 2001 From: Raghu Banda Date: Thu, 16 Jul 2026 12:33:36 -0400 Subject: [PATCH 1/2] feat(langgraph): add OpenShell sandbox integration for react_agent Add Containerfile.openshell and deployment documentation for running the LangGraph ReAct agent inside an NVIDIA OpenShell sandbox with policy-enforced network isolation and filesystem constraints. Follows the official BYOC pattern from NVIDIA/OpenShell. Handles the components/auth path dependency by installing it separately before the main package (avoids relative path resolution issues from /sandbox). Tested on OpenShift 4.18 cluster: - Image builds successfully via oc start-build (134 packages) - /health returns {"status":"healthy","agent_initialized":true} - /images/rh_logo.svg returns 200 (playground assets served) - /chat/completions works end-to-end with gemini-2.5-flash via Llama Stack endpoint --- .../react_agent/Containerfile.openshell | 60 ++++++++++ .../templates/react_agent/OPENSHELL.md | 107 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 agents/langgraph/templates/react_agent/Containerfile.openshell create mode 100644 agents/langgraph/templates/react_agent/OPENSHELL.md diff --git a/agents/langgraph/templates/react_agent/Containerfile.openshell b/agents/langgraph/templates/react_agent/Containerfile.openshell new file mode 100644 index 00000000..10ec6d2d --- /dev/null +++ b/agents/langgraph/templates/react_agent/Containerfile.openshell @@ -0,0 +1,60 @@ +# LangGraph ReAct agent for NVIDIA OpenShell sandbox +# +# BYOC (Bring Your Own Container) image following the OpenShell pattern: +# https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container +# +# Build (copy repo-level images/ and components/auth/ into context first): +# cp -r ../../../../images ./images +# mkdir -p ./components && cp -r ../../../../components/auth ./components/auth +# trap 'rm -rf ./images ./components' EXIT +# podman build --platform linux/amd64 -t langgraph-sandbox:latest -f Containerfile.openshell . +# +# Run (OpenShell replaces CMD -- pass the start command after --): +# openshell sandbox create --from langgraph-sandbox:latest --forward 8080 \ +# -e API_KEY= -e BASE_URL= -e MODEL_ID= \ +# -- uvicorn main:app --host 0.0.0.0 --port 8080 + +FROM registry.access.redhat.com/ubi9/python-312@sha256:e95978812895b9abb2bdc109b501078da2a47c8dbb9fa23758af40ed50ab6023 + +USER 0 + +# iproute: required by OpenShell for network namespace management +# nftables: optional, enables bypass detection (log + reject for direct connections) +RUN dnf install -y --nodocs iproute nftables && dnf clean all && rm -rf /var/cache/dnf + +# uv for fast dependency installation +COPY --from=ghcr.io/astral-sh/uv@sha256:fc93e9ecd7218e9ec8fba117af89348eef8fd2463c50c13347478769aaedd0ce /uv /usr/local/bin/uv + +# Application directory (OpenShell convention: /sandbox) +RUN install -d -o 1001 -g 0 -m 775 /sandbox +WORKDIR /sandbox + +# Install the auth component first (resolves the path dependency issue), +# then install the main package with tracing extra. +COPY --chown=1001:0 components/auth/ ./components/auth/ +RUN uv pip install --python /opt/app-root/bin/python3 --no-cache ./components/auth/ + +COPY --chown=1001:0 pyproject.toml . +COPY --chown=1001:0 src/ ./src/ + +# Remove the uv path source for agent-auth (already installed above from ./components/auth/) +# so uv doesn't try to resolve the relative path ../../../../components/auth from /sandbox. +RUN sed -i '/^\[tool\.uv\.sources\]/,/^\[/{/agent-auth/d}' pyproject.toml && \ + uv pip install --python /opt/app-root/bin/python3 --no-cache ".[tracing]" + +# Application code and assets +COPY --chown=1001:0 main.py . +COPY --chown=1001:0 playground/ ./playground/ +COPY --chown=1001:0 images/ ./images/ + +USER 1001 + +ENV PORT=8080 \ + PYTHONPATH=/sandbox \ + PATH="/opt/app-root/bin:${PATH}" + +EXPOSE 8080 + +# NOTE: OpenShell's sandbox supervisor replaces CMD at runtime. +# Pass the start command explicitly on the CLI after --. +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/agents/langgraph/templates/react_agent/OPENSHELL.md b/agents/langgraph/templates/react_agent/OPENSHELL.md new file mode 100644 index 00000000..06361a99 --- /dev/null +++ b/agents/langgraph/templates/react_agent/OPENSHELL.md @@ -0,0 +1,107 @@ +# LangGraph ReAct Agent — OpenShell Sandbox Deployment + +Run the LangGraph ReAct agent inside an [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) sandbox with policy-enforced network isolation and filesystem constraints. + +This follows the [Bring Your Own Container](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container) pattern: a standard Linux container image with no OpenShell-specific dependencies. + +## Prerequisites + +- [Podman](https://podman.io/) or Docker installed +- [OpenShell CLI](https://github.com/NVIDIA/OpenShell) installed and connected to a gateway +- An OpenAI-compatible inference endpoint reachable from the sandbox (vLLM, OGX, Llama Stack, or remote API) + +## Build + +```bash +cd agents/langgraph/templates/react_agent + +# Copy repo-level assets into build context +cp -r ../../../../images ./images +mkdir -p ./components && cp -r ../../../../components/auth ./components/auth +trap 'rm -rf ./images ./components' EXIT + +podman build --platform linux/amd64 -t quay.io//langgraph-sandbox:latest -f Containerfile.openshell . +podman push quay.io//langgraph-sandbox:latest +``` + +Ensure the quay.io repository is public so the cluster can pull without imagePullSecrets. + +## Run + +OpenShell's sandbox supervisor replaces the image's CMD/ENTRYPOINT at runtime. You **must** pass the application start command explicitly after `--`: + +```bash +openshell sandbox create \ + --name langgraph-agent \ + --from quay.io//langgraph-sandbox:latest \ + --forward 8080 \ + -e API_KEY=not-needed-for-local \ + -e BASE_URL=http://vllm-svc.my-ns.svc.cluster.local:8000/v1 \ + -e MODEL_ID=llama3.1:8b \ + -- uvicorn main:app --host 0.0.0.0 --port 8080 +``` + +Flags: + +- `--forward 8080` opens an SSH tunnel so `localhost:8080` on your machine reaches the agent inside the sandbox +- `-e` injects environment variables via OpenShell providers (never written to disk) +- `-- ` is the process the supervisor executes via SSH once the sandbox is ready + +## Verify + +```bash +# Health check (should return {"status": "healthy", "agent_initialized": true}) +curl -s http://localhost:8080/health | python3 -m json.tool + +# Chat completion +curl -s -X POST http://localhost:8080/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"What is OpenShift?"}],"stream":false}' \ + | python3 -m json.tool +``` + +## Network Policy + +The ReAct agent only needs outbound access to the configured LLM endpoint. Apply a restrictive policy: + +```yaml +sandbox: + network: + egress: + - host: "vllm-svc.my-ns.svc.cluster.local" + port: 8000 + methods: ["POST"] + paths: + - "/v1/chat/completions" + - "/v1/completions" +``` + +```bash +openshell policy set langgraph-agent --policy policy.yaml --wait +``` + +## Cleanup + +```bash +openshell sandbox delete langgraph-agent +``` + +## How It Works + +OpenShell isolates the sandbox container and routes all outbound traffic through its policy engine. The LangGraph agent runs as a normal FastAPI/uvicorn process inside the container — no code changes required. The key differences from a standard Helm deployment: + +| Aspect | Standard (Helm) | OpenShell Sandbox | +|--------|----------------|-------------------| +| Base image | UBI9 Python 3.12 | Same (UBI9 Python 3.12) | +| Network isolation | K8s NetworkPolicy | OpenShell L7 policy engine | +| Credential injection | Helm secrets / env vars | OpenShell providers (`-e` flags) | +| Process supervision | Container runtime PID 1 | OpenShell supervisor via SSH | +| Start command | Dockerfile CMD | Explicit `-- ` | + +## Notes + +- The ReAct agent is a long-running FastAPI server. Unlike CLI agents (Claude Code, Codex), it does not need interactive terminal access. +- `BASE_URL` must be reachable from within the sandbox. Use cluster-internal DNS for in-cluster endpoints. +- If the model endpoint is unreachable, `/health` still returns healthy but `/chat/completions` will fail with a 500. +- Build with `--platform linux/amd64` when targeting x86_64 clusters from Apple Silicon machines. +- The `components/auth` dependency is pre-installed in the image for token-based auth support. From 2bfeb277e82313a4825c968aca08fa0b9400fe12 Mon Sep 17 00:00:00 2001 From: Raghu Banda Date: Thu, 16 Jul 2026 23:07:07 -0400 Subject: [PATCH 2/2] fix(langgraph): address CodeRabbit review feedback - Move USER 1001 before pip install so packages are owned by the runtime user (avoids __pycache__ permission issues) - Replace trap EXIT with explicit rm -rf in build docs (trap doesn't fire in interactive shells until terminal closes) --- .../langgraph/templates/react_agent/Containerfile.openshell | 4 ++-- agents/langgraph/templates/react_agent/OPENSHELL.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/agents/langgraph/templates/react_agent/Containerfile.openshell b/agents/langgraph/templates/react_agent/Containerfile.openshell index 10ec6d2d..eff74ec0 100644 --- a/agents/langgraph/templates/react_agent/Containerfile.openshell +++ b/agents/langgraph/templates/react_agent/Containerfile.openshell @@ -29,6 +29,8 @@ COPY --from=ghcr.io/astral-sh/uv@sha256:fc93e9ecd7218e9ec8fba117af89348eef8fd246 RUN install -d -o 1001 -g 0 -m 775 /sandbox WORKDIR /sandbox +USER 1001 + # Install the auth component first (resolves the path dependency issue), # then install the main package with tracing extra. COPY --chown=1001:0 components/auth/ ./components/auth/ @@ -47,8 +49,6 @@ COPY --chown=1001:0 main.py . COPY --chown=1001:0 playground/ ./playground/ COPY --chown=1001:0 images/ ./images/ -USER 1001 - ENV PORT=8080 \ PYTHONPATH=/sandbox \ PATH="/opt/app-root/bin:${PATH}" diff --git a/agents/langgraph/templates/react_agent/OPENSHELL.md b/agents/langgraph/templates/react_agent/OPENSHELL.md index 06361a99..cb110f73 100644 --- a/agents/langgraph/templates/react_agent/OPENSHELL.md +++ b/agents/langgraph/templates/react_agent/OPENSHELL.md @@ -18,10 +18,12 @@ cd agents/langgraph/templates/react_agent # Copy repo-level assets into build context cp -r ../../../../images ./images mkdir -p ./components && cp -r ../../../../components/auth ./components/auth -trap 'rm -rf ./images ./components' EXIT podman build --platform linux/amd64 -t quay.io//langgraph-sandbox:latest -f Containerfile.openshell . podman push quay.io//langgraph-sandbox:latest + +# Clean up build context +rm -rf ./images ./components ``` Ensure the quay.io repository is public so the cluster can pull without imagePullSecrets.