feat(instance-proxy): add HTTP proxy for tool instances
Add API proxy endpoint so users can access running tool instances
through the backend API instead of internal Docker network.
Backend:
- Add container_name field to ToolInstance model
- Create /instances/{id}/proxy/{path:path} endpoint with ownership checks
- Proxy HTTP requests to containers via docker network using container names
- Support all HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
- Store proxy URL in instance.url instead of localhost
- Add Alembic migration 0007 for container_name column
- Add get_container_name() utility to docker.py
Frontend:
- Update Open button to use full proxy URL (API_BASE_URL + instance.url)
Closes instance-proxy OpenSpec change.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-20
|
||||
@@ -0,0 +1,83 @@
|
||||
## Context
|
||||
|
||||
Currently, tool instances run as Docker containers on the internal Docker network. The backend stores their URL as `http://localhost:{port}`, which is only accessible from inside the API container. Users clicking "Open" in the frontend get a 404 because their browser can't reach the internal container.
|
||||
|
||||
The API and containers share a Docker network, so the API can reach containers by their container name or IP.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Users can access running tool instances through the API via HTTPS
|
||||
- Proxy enforces ownership (only instance owner can access)
|
||||
- Support both HTTP and WebSocket traffic
|
||||
- Minimal latency overhead
|
||||
- Works with existing Docker setup
|
||||
|
||||
**Non-Goals:**
|
||||
- Public URLs / custom domains for instances (that's Option 2/3)
|
||||
- Load balancing across multiple instances
|
||||
- Advanced path rewriting (just pass-through)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Proxy via FastAPI route (not separate service)
|
||||
|
||||
**Decision:** Implement proxying as a FastAPI endpoint using `httpx` for async forwarding.
|
||||
|
||||
**Rationale:**
|
||||
- Keeps everything in one deployable unit
|
||||
- Easy access to existing auth dependencies (`get_current_user_id`)
|
||||
- Can reuse existing session cookie auth
|
||||
- No extra infrastructure needed
|
||||
|
||||
**Alternative considered:** Separate nginx/traefik proxy service
|
||||
- Rejected: adds operational complexity for a single feature
|
||||
|
||||
### Use container name for internal routing
|
||||
|
||||
**Decision:** Store container name in ToolInstance model and route to `http://{container_name}:{port}`
|
||||
|
||||
**Rationale:**
|
||||
- Container names are stable and DNS-resolvable within Docker network
|
||||
- More reliable than IPs which can change
|
||||
- Already using container names in docker.py
|
||||
|
||||
### Path: `/instances/{id}/proxy/{path:path}`
|
||||
|
||||
**Decision:** All proxied traffic goes through `/instances/{id}/proxy/*`
|
||||
|
||||
**Rationale:**
|
||||
- Clear URL structure
|
||||
- Easy to apply auth middleware
|
||||
- `path:path` captures everything after `/proxy/`
|
||||
|
||||
### WebSocket upgrade handling
|
||||
|
||||
**Decision:** Support WebSocket upgrade by inspecting the `Upgrade: websocket` header and establishing a bidirectional pipe.
|
||||
|
||||
**Rationale:**
|
||||
- code-server and jupyter use WebSockets for real-time features
|
||||
- FastAPI doesn't natively support proxying WebSockets, but we can use `starlette.websockets` to handle the upgrade
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk]** API becomes bandwidth bottleneck for all instance traffic
|
||||
→ **Mitigation:** Document this limitation. Future migration to Option 2 (Traefik labels) possible.
|
||||
|
||||
**[Risk]** Container name collision
|
||||
→ **Mitigation:** Instance names already include UUID suffix, collision probability is negligible.
|
||||
|
||||
**[Risk]** Large file uploads/downloads through proxy
|
||||
→ **Mitigation:** Use streaming response in httpx. Monitor memory usage.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Deploy backend changes (proxy endpoint + model updates)
|
||||
2. Update frontend links to use proxy URL
|
||||
3. Test with code-server instance
|
||||
4. Monitor API performance
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should we add rate limiting to the proxy endpoint?
|
||||
- Do we need to rewrite response headers (Location, Set-Cookie)?
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
Tool instances (code-server, jupyter-notebook) run inside Docker containers with internal network addresses. Currently the "Open" button links to `http://localhost:{port}`, which only works from inside the API container and fails when opened from the user's browser. We need a way to expose these instances to users over HTTPS.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a proxy endpoint to the backend API: `/instances/{id}/proxy/{path:path}`
|
||||
- Proxy requests from the API to the running container (via docker network or internal IP)
|
||||
- Update frontend "Open" button to use the proxy URL instead of `localhost`
|
||||
- Add WebSocket proxy support for real-time features (terminal already uses WebSocket)
|
||||
- Ensure only the instance owner can access the proxied content
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `instance-proxy`: HTTP proxying for running tool instances through the API
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- None (this is purely an infrastructure/transport feature, not a change to existing capability requirements)
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend: New proxy endpoint, container network discovery, request forwarding
|
||||
- Frontend: Update instance "Open" link to use proxy URL
|
||||
- Docker: Containers must be reachable from API container (already true via docker network)
|
||||
- Security: Owner-only access enforced at proxy level
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Proxy endpoint exists for running instances
|
||||
The API SHALL expose an endpoint that forwards HTTP requests to a running tool instance.
|
||||
|
||||
#### Scenario: Access running instance
|
||||
- **WHEN** an authenticated user sends a GET request to `/instances/{id}/proxy/`
|
||||
- **THEN** the request is forwarded to the instance's container
|
||||
- **AND** the response is returned to the user
|
||||
|
||||
#### Scenario: Access instance subpath
|
||||
- **WHEN** an authenticated user sends a request to `/instances/{id}/proxy/api/status`
|
||||
- **THEN** the request is forwarded to `{container_url}/api/status`
|
||||
- **AND** the response is returned to the user
|
||||
|
||||
### Requirement: Only instance owner can access proxy
|
||||
The proxy endpoint SHALL verify that the authenticated user owns the instance before forwarding.
|
||||
|
||||
#### Scenario: Owner accesses instance
|
||||
- **WHEN** the instance owner requests `/instances/{id}/proxy/`
|
||||
- **THEN** the request is forwarded to the instance
|
||||
|
||||
#### Scenario: Non-owner attempts access
|
||||
- **WHEN** a user who does not own the instance requests `/instances/{id}/proxy/`
|
||||
- **THEN** the API returns 403 Forbidden
|
||||
|
||||
### Requirement: Proxy handles WebSocket upgrades
|
||||
The proxy endpoint SHALL support WebSocket upgrade requests for real-time features.
|
||||
|
||||
#### Scenario: WebSocket connection to instance
|
||||
- **WHEN** a user sends a request with `Upgrade: websocket` header
|
||||
- **THEN** the API establishes a bidirectional WebSocket connection to the instance
|
||||
- **AND** messages are relayed between user and instance
|
||||
|
||||
### Requirement: Frontend uses proxy URL for instance access
|
||||
The frontend SHALL link to the proxy endpoint instead of the internal container URL.
|
||||
|
||||
#### Scenario: User clicks Open button
|
||||
- **WHEN** a user clicks "Open" on a running instance
|
||||
- **THEN** a new tab opens to `/instances/{id}/proxy/`
|
||||
- **AND** the proxied instance content is displayed
|
||||
@@ -0,0 +1,26 @@
|
||||
## 1. Backend - Proxy Endpoint
|
||||
|
||||
- [ ] 1.1 Add `container_name` field to ToolInstance model and update start_instance to store it
|
||||
- [ ] 1.2 Create proxy endpoint `/instances/{id}/proxy/{path:path}` in tool_instances.py
|
||||
- [ ] 1.3 Implement HTTP forwarding using httpx with streaming support
|
||||
- [ ] 1.4 Add ownership check before proxying
|
||||
- [ ] 1.5 Add WebSocket upgrade support for the proxy endpoint
|
||||
- [ ] 1.6 Handle response header forwarding (Content-Type, cookies, etc.)
|
||||
|
||||
## 2. Backend - Instance URL Update
|
||||
|
||||
- [ ] 2.1 Update start_instance to set instance URL to proxy path instead of localhost
|
||||
- [ ] 2.2 Ensure container_name is captured during start
|
||||
|
||||
## 3. Frontend - Update Instance Links
|
||||
|
||||
- [ ] 3.1 Update InstanceList "Open" button to use proxy URL
|
||||
- [ ] 3.2 Update SessionsPage "Open" button to use proxy URL
|
||||
- [ ] 3.3 Ensure URLs open in new tab
|
||||
|
||||
## 4. Testing & Quality
|
||||
|
||||
- [ ] 4.1 Test proxy with code-server instance
|
||||
- [ ] 4.2 Verify WebSocket features work (terminal inside code-server)
|
||||
- [ ] 4.3 Run quality gates (ruff, mypy, typecheck, lint, build)
|
||||
- [ ] 4.4 Deploy and test end-to-end
|
||||
Reference in New Issue
Block a user