fix(proxy): use internal container port instead of host port

The proxy was using instance.port which is a dynamically allocated
host port (e.g., 10001). But containers communicate on the Docker
network using their internal ports (8443 for code-server, 8888 for
jupyter). This caused connection failures when opening instances.

- Add default_port field to ToolType model (null for terminal-only tools)
- Create migration 0010 for default_port column
- Update seed data: code-server=8443, jupyter=8888, opencode=null
- Update proxy to use tool type's default_port instead of instance.port
- Update frontend ToolType interface to include default_port

Fixes: Opening instances now routes to correct internal container port
This commit is contained in:
Fusion
2026-05-20 13:55:42 +02:00
parent b4a7627718
commit 98795e31dd
6 changed files with 43 additions and 2 deletions
@@ -0,0 +1,28 @@
"""add default_port to tool_types
Revision ID: 0010_tool_type_default_port
Revises: 0009_tool_configs
Create Date: 2026-05-20 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0010_tool_type_default_port"
down_revision: Union[str, None] = "0009_tool_configs"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"tool_types",
sa.Column("default_port", sa.Integer(), nullable=True)
)
def downgrade() -> None:
op.drop_column("tool_types", "default_port")
+7 -2
View File
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models.tool_instance import ToolInstance
from src.models.tool_type import ToolType
logger = logging.getLogger(__name__)
@@ -43,8 +44,12 @@ async def _proxy_request(
detail="instance is not running",
)
# Build target URL
target_url = f"http://{instance.container_name}:{instance.port}"
# Get the tool type to find the internal port
tool_type = await session.get(ToolType, instance.tool_type_id)
internal_port = tool_type.default_port if tool_type and tool_type.default_port else instance.port
# Build target URL using internal port
target_url = f"http://{instance.container_name}:{internal_port}"
if path:
target_url += f"/{path}"
+3
View File
@@ -37,6 +37,7 @@ class ToolTypeCreate(BaseModel):
name: str
display_name: str
description: str | None = None
default_port: int | None = None
compose_template: str
required_variables: list[str] = []
@@ -117,6 +118,7 @@ class ToolTypeResponse(BaseModel):
description: str | None
category: str
interfaces: list[str]
default_port: int | None
compose_template: str
required_variables: list[str]
is_builtin: bool
@@ -159,6 +161,7 @@ async def create_tool_type(
name=data.name,
display_name=data.display_name,
description=data.description,
default_port=data.default_port,
compose_template=data.compose_template,
required_variables=data.required_variables,
is_builtin=False,
+3
View File
@@ -123,6 +123,7 @@ services:
ports:
- "8443:8443"
restart: unless-stopped""",
"default_port": 8443,
"required_variables": ["REPO_PATH", "TOOL_NAME"],
},
{
@@ -131,6 +132,7 @@ services:
"description": "Jupyter Lab for interactive development",
"category": "notebook",
"interfaces": ["web"],
"default_port": 8888,
"compose_template": """version: "3.8"
services:
jupyter:
@@ -187,6 +189,7 @@ volumes:
interfaces=tool_data["interfaces"],
compose_template=tool_data["compose_template"],
required_variables=tool_data["required_variables"],
default_port=tool_data.get("default_port"),
is_builtin=True,
)
session.add(tool_type)
+1
View File
@@ -19,6 +19,7 @@ class ToolType(UUIDPrimaryKeyMixin, TimestampMixin, Base):
description: Mapped[str | None] = mapped_column(Text, nullable=True)
category: Mapped[str] = mapped_column(String(50), nullable=False, default="other")
interfaces: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
default_port: Mapped[int | None] = mapped_column(nullable=True)
compose_template: Mapped[str] = mapped_column(Text, nullable=False)
required_variables: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)