Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions 0010-host-side-api-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ See [HOW_TO.md](HOW_TO.md).
- **Repo provisioner depends on OpenShell#1272**: if content inspection hooks
ship in OpenShell, the scan-before-copy flow could be handled natively

## Changelog

### 2026-07-16: Remove `allowed_ips` requirement

OpenShell maintainer [johntmyers confirmed](https://github.com/NVIDIA/OpenShell/issues/1633#issuecomment-1)
that PR [NVIDIA/OpenShell#1560](https://github.com/NVIDIA/OpenShell/pull/1560) removed
the `allowed_ips` requirement when endpoints are explicitly declared with host+port in
the policy. Validated locally on OpenShell v0.0.83 (rootless Podman + pasta, Fedora 44):

- Both builder (`:9090`) and provisioner (`:9091`) reachable from sandbox without `allowed_ips`
- Undeclared endpoints still blocked by the proxy

Changes:
- Removed `allowed_ips: ["{{HOST_IP}}/32"]` from both policies
- Removed HOST_IP resolution and policy template rendering from `run.sh`
- Harness files now reference raw policies directly (no rendered copies)
- Added `role: experiment` to all harness files (required by current fullsend)

Servers still bind to `0.0.0.0` — the remaining motivation for
[NVIDIA/OpenShell#1633](https://github.com/NVIDIA/OpenShell/issues/1633) (`host.local`
supervisor-proxied endpoints) is eliminating `0.0.0.0` binding in favor of `127.0.0.1`.

## Findings

See [results/findings.md](results/findings.md) (populated after running).
62 changes: 48 additions & 14 deletions 0010-host-side-api-server/findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@ inside the sandbox and must be used in:
- **Env files** delivered into the sandbox (`BUILDER_URL`, `PROVISIONER_URL`)
- **Network policy endpoints** (`host: host.openshell.internal`)

The host IP is still needed for `allowed_ips` in network policies (SSRF
~~The host IP is still needed for `allowed_ips` in network policies (SSRF
allowlisting requires a CIDR, not a hostname). The orchestrator resolves this
at runtime via `getent hosts host.openshell.internal` and renders it into
policy templates with `sed "s/{{HOST_IP}}/$HOST_IP/g"`.
policy templates with `sed "s/{{HOST_IP}}/$HOST_IP/g"`.~~

**Update (2026-07-16):** `allowed_ips` is no longer required. See
[below](#2026-07-16-allowed_ips-no-longer-required-for-declared-endpoints).

### Server process contract

Expand Down Expand Up @@ -165,22 +168,22 @@ network_policies:
method: POST
path: /build
# ...additional allowed paths
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
```

Key elements:
- **`protocol: rest`**: Enables HTTP method + path matching at L7
- **`allowed_ips`**: Overrides SSRF protection for the host bridge IP (rendered
from template at orchestrator startup)
- **`binaries`**: Restricts which executables can reach the endpoint (`**/curl`
ensures only curl can call the API, not arbitrary processes)
- **Restricted policies** omit discovery endpoints (`/openapi.json`,
`/tools.json`) and non-essential operations (`/push`, `/images`) to test
agent behavior under reduced API surface

> **Note:** The original experiment included `allowed_ips: ["{{HOST_IP}}/32"]`
> in endpoint declarations. This is no longer needed — see
> [update below](#2026-07-16-allowed_ips-no-longer-required-for-declared-endpoints).

### Orchestrator lifecycle

The wrapper script (`run.sh`) manages the full lifecycle:
Expand All @@ -190,14 +193,16 @@ The wrapper script (`run.sh`) manages the full lifecycle:
3. **Generate bearer token** (UUID) and export as `API_TOKEN`
4. **Start API servers** (builder on :9090, provisioner on :9091) with `--token`
5. **Health-check** both servers (poll `/healthz` up to 15s, verify PIDs alive)
6. **Resolve host IP** via `getent hosts host.openshell.internal`
7. **Render policy templates** — substitute `{{HOST_IP}}` into both policy files
8. **Generate env file** with `BUILDER_URL` and `PROVISIONER_URL` using
6. **Generate env file** with `BUILDER_URL` and `PROVISIONER_URL` using
`host.openshell.internal` hostname
9. **Run `fullsend run`** with `--fullsend-dir .` (experiment directory is the
7. **Run `fullsend run`** with `--fullsend-dir .` (experiment directory is the
fullsend directory)
10. **Cleanup on exit** (trap): kill server PIDs, delete provider, remove
rendered policies and temp env file
8. **Cleanup on exit** (trap): kill server PIDs, delete provider, remove
temp env file

> **Note:** The original lifecycle included HOST_IP resolution (step 6) and
> policy template rendering (step 7) for `allowed_ips`. These steps were
> removed in the 2026-07-16 update.

The harness files deliver the env file into the sandbox via `host_files`:
```yaml
Expand Down Expand Up @@ -375,5 +380,34 @@ This has been filed as

All URLs delivered into the sandbox must use `host.openshell.internal`, never
raw IPs. The L7 proxy matches requests by hostname, and SSRF protection blocks
private IP addresses. The `allowed_ips` field in network policies handles the
SSRF allowlisting separately using the rendered host IP.
private IP addresses.

### 2026-07-16: `allowed_ips` no longer required for declared endpoints

OpenShell maintainer johntmyers
[confirmed](https://github.com/NVIDIA/OpenShell/issues/1633#issuecomment-1)
that [PR #1560](https://github.com/NVIDIA/OpenShell/pull/1560) removed the
`allowed_ips` requirement when endpoints are explicitly declared with host+port
in the policy.

**Validation (OpenShell v0.0.83, rootless Podman + pasta, Fedora 44 kernel 7.0.9):**

| Endpoint | Port | Policy has `allowed_ips`? | Result from sandbox |
|----------|------|--------------------------|---------------------|
| builder `/tools.json` | 9090 | No | 200 OK |
| provisioner `/tools.json` | 9091 | No | 200 OK |
| `example.com` (not in policy) | 80 | N/A | Blocked |

**What changed:**
- Removed `allowed_ips: ["{{HOST_IP}}/32"]` from both policies
- Removed HOST_IP resolution and policy template rendering from `run.sh`
- Harness files now reference raw policy files directly (no rendered copies)

**Implications:**
- The `{{HOST_IP}}` templating step was the most fragile part of the setup
(platform-dependent IP resolution, sed rendering, separate rendered files).
Its removal simplifies the orchestrator significantly.
- Servers still bind to `0.0.0.0` — the remaining motivation for
[NVIDIA/OpenShell#1633](https://github.com/NVIDIA/OpenShell/issues/1633)
is eliminating `0.0.0.0` binding in favor of `127.0.0.1` via supervisor-proxied
`host.local` endpoints.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-full-access.yaml
policy: policies/full-access.yaml

providers:
- api-server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-restricted.yaml
policy: policies/restricted.yaml

providers:
- api-server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-full-access.yaml
policy: policies/full-access.yaml

providers:
- api-server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-restricted.yaml
policy: policies/restricted.yaml

providers:
- api-server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-full-access.yaml
policy: policies/full-access.yaml

providers:
- api-server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
agent: agents/test-agent.md
role: experiment
model: opus
policy: policies/rendered-restricted.yaml
policy: policies/restricted.yaml

providers:
- api-server
Expand Down
4 changes: 0 additions & 4 deletions 0010-host-side-api-server/policies/full-access.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ network_policies:
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
provisioner:
Expand All @@ -72,7 +70,5 @@ network_policies:
- allow:
method: GET
path: /tools.json
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
4 changes: 0 additions & 4 deletions 0010-host-side-api-server/policies/restricted.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ network_policies:
- allow:
method: POST
path: /build
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
provisioner:
Expand All @@ -52,7 +50,5 @@ network_policies:
- allow:
method: GET
path: /repo/status/*
allowed_ips:
- "{{HOST_IP}}/32"
binaries:
- path: "**/curl"
35 changes: 4 additions & 31 deletions 0010-host-side-api-server/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ cd "$SCRIPT_DIR"
usage() {
echo "Usage: $0 <harness-name>"
echo ""
echo "Starts the host-side API servers, renders policies, and runs"
echo "fullsend run with the specified harness."
echo "Starts the host-side API servers and runs fullsend with the"
echo "specified harness."
echo ""
echo "Available harnesses:"
for f in harness/*.yaml; do
Expand Down Expand Up @@ -65,7 +65,6 @@ cleanup() {
[[ -n "$PROVISIONER_PID" ]] && kill "$PROVISIONER_PID" 2>/dev/null && wait "$PROVISIONER_PID" 2>/dev/null && echo "Stopped provisioner (pid $PROVISIONER_PID)"
openshell provider delete api-server 2>/dev/null && echo "Deleted provider api-server" || true
rm -f "$ENV_FILE"
rm -f policies/rendered-full-access.yaml policies/rendered-restricted.yaml
echo "Cleanup done."
}
trap cleanup EXIT
Expand All @@ -91,28 +90,6 @@ API_TOKEN="$(uuidgen)"
echo "=== Host-Side API Server Experiment ==="
echo "Harness: $HARNESS_NAME"

# ---------------------------------------------------------------------------
# Resolve host IP (needed before starting servers for bind address)
# ---------------------------------------------------------------------------

HOST_IP=""
if out=$(getent hosts host.openshell.internal 2>/dev/null); then
HOST_IP=$(echo "$out" | awk '{print $1}')
echo "Host IP (via host.openshell.internal): $HOST_IP"
fi

if [[ -z "$HOST_IP" ]]; then
HOST_IP=$(podman network inspect podman 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['subnets'][0]['gateway'])" 2>/dev/null)
if [[ -n "$HOST_IP" ]]; then
echo "Host IP (via podman bridge gateway): $HOST_IP"
fi
fi

if [[ -z "$HOST_IP" ]]; then
echo "ERROR: Could not resolve host IP"
exit 1
fi

# ---------------------------------------------------------------------------
# Start API servers (bound to all interfaces — rootless Podman can't bind to
# the bridge gateway IP since it lives inside the container namespace)
Expand Down Expand Up @@ -158,14 +135,10 @@ for port in 9090 9091; do
done

# ---------------------------------------------------------------------------
# Render policies
# Policies (no rendering needed — allowed_ips removed per #1560)
# ---------------------------------------------------------------------------

echo "Rendering policies..."
sed "s/{{HOST_IP}}/$HOST_IP/g" policies/full-access.yaml > policies/rendered-full-access.yaml
sed "s/{{HOST_IP}}/$HOST_IP/g" policies/restricted.yaml > policies/rendered-restricted.yaml
echo " policies/rendered-full-access.yaml"
echo " policies/rendered-restricted.yaml"
echo "Using policies directly (no HOST_IP templating needed)."

# ---------------------------------------------------------------------------
# Generate env file (server URLs only — token is handled by provider)
Expand Down
Loading