feat: add live config profile refresh
- Standardize built-in tool users for shared writable profile mounts - Mount canonical non-Git profile sources across compatible instances - Report restart-required outcomes and guard active profile deletion - Surface restart feedback in config profile editing Quality gates: frontend build passed; backend py_compile and LSP passed. Skipped: backend pytest/Ruff unavailable; Docker/manual checks not approved.
This commit is contained in:
@@ -5,10 +5,125 @@ import logging
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from src.database import SessionLocal
|
||||
from src.models import ToolType
|
||||
from src.models import ToolDefinitionManifest, ToolType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# All supported built-in tools run their long-lived process with these IDs.
|
||||
# Canonical writable Config Profile mounts can therefore be shared without
|
||||
# per-instance ownership changes.
|
||||
BUILTIN_USER_UID = 1000
|
||||
BUILTIN_USER_GID = 1000
|
||||
|
||||
BUILTIN_TOOL_TYPES = [
|
||||
{
|
||||
"name": "code-server",
|
||||
"display_name": "VS Code Server",
|
||||
"description": "VS Code running in the browser via code-server",
|
||||
"category": "editor",
|
||||
"interface_type": "web",
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/London
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/config/workspace
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: 'no'""",
|
||||
"default_port": 8443,
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "jupyter-notebook",
|
||||
"display_name": "Jupyter Notebook",
|
||||
"description": "Jupyter Lab for interactive development",
|
||||
"category": "notebook",
|
||||
"interface_type": "web",
|
||||
"default_port": 8888,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
jupyter:
|
||||
image: jupyter/scipy-notebook:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- JUPYTER_ENABLE_LAB=yes
|
||||
- NB_UID=1000
|
||||
- NB_GID=1000
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/jovyan/work
|
||||
ports:
|
||||
- "8888:8888"
|
||||
restart: 'no'""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"description": "AI coding assistant - run opencode in terminal",
|
||||
"category": "ai-assistant",
|
||||
"interface_type": "terminal",
|
||||
"default_port": 3000,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
opencode:
|
||||
image: node:20-slim
|
||||
container_name: {{TOOL_NAME}}
|
||||
working_dir: /home/node/{{WORKSPACE_NAME}}
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/node/{{WORKSPACE_NAME}}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: >
|
||||
sh -ec "apt-get update && apt-get install -y git ca-certificates &&
|
||||
npm install -g opencode-ai &&
|
||||
exec setpriv --reuid=node --regid=node --init-groups opencode server"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: 'no'""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _standardize_builtin_manifest_user(manifest: dict) -> bool:
|
||||
"""Set the built-in manifest user to the shared UID/GID in place.
|
||||
|
||||
The helper deliberately recognizes only Headquarter's conventional
|
||||
``user`` account so it cannot rewrite a future custom tool definition.
|
||||
"""
|
||||
user = manifest.get("user")
|
||||
if not isinstance(user, dict) or user.get("name") != "user":
|
||||
return False
|
||||
|
||||
changed = user.get("uid") != BUILTIN_USER_UID or user.get("gid") != BUILTIN_USER_GID
|
||||
if changed:
|
||||
user["uid"] = BUILTIN_USER_UID
|
||||
user["gid"] = BUILTIN_USER_GID
|
||||
return changed
|
||||
|
||||
|
||||
async def _standardize_pi_agent_manifest(session) -> None:
|
||||
"""Bring the built-in Pi Agent manifest in line with shared mount IDs."""
|
||||
manifest_definition = await session.scalar(
|
||||
select(ToolDefinitionManifest).where(
|
||||
ToolDefinitionManifest.name == "pi-agent",
|
||||
ToolDefinitionManifest.created_by_id.is_(None),
|
||||
)
|
||||
)
|
||||
if manifest_definition is None:
|
||||
return
|
||||
|
||||
manifest = dict(manifest_definition.manifest)
|
||||
if _standardize_builtin_manifest_user(manifest):
|
||||
manifest_definition.manifest = manifest
|
||||
logger.info("Standardized built-in Pi Agent user to 1000:1000")
|
||||
|
||||
|
||||
async def _table_exists(session, table_name: str) -> bool:
|
||||
"""Check if a table exists in the database."""
|
||||
@@ -45,88 +160,9 @@ async def seed_builtin_tool_types():
|
||||
)
|
||||
return
|
||||
|
||||
builtin_types = [
|
||||
{
|
||||
"name": "code-server",
|
||||
"display_name": "VS Code Server",
|
||||
"description": "VS Code running in the browser via code-server",
|
||||
"category": "editor",
|
||||
"interface_type": "web",
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/London
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/config/workspace
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: 'no'""",
|
||||
"default_port": 8443,
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "jupyter-notebook",
|
||||
"display_name": "Jupyter Notebook",
|
||||
"description": "Jupyter Lab for interactive development",
|
||||
"category": "notebook",
|
||||
"interface_type": "web",
|
||||
"default_port": 8888,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
jupyter:
|
||||
image: jupyter/scipy-notebook:latest
|
||||
container_name: {{TOOL_NAME}}
|
||||
environment:
|
||||
- JUPYTER_ENABLE_LAB=yes
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/jovyan/work
|
||||
ports:
|
||||
- "8888:8888"
|
||||
restart: 'no'""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
{
|
||||
"name": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"description": "AI coding assistant - run opencode in terminal",
|
||||
"category": "ai-assistant",
|
||||
"interface_type": "terminal",
|
||||
"default_port": 3000,
|
||||
"compose_template": """version: "3.8"
|
||||
services:
|
||||
opencode:
|
||||
image: node:20-slim
|
||||
container_name: {{TOOL_NAME}}
|
||||
working_dir: /home/user/{{WORKSPACE_NAME}}
|
||||
volumes:
|
||||
- {{REPO_PATH}}:/home/user/{{WORKSPACE_NAME}}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: >
|
||||
sh -c "set -x &&
|
||||
apt-get update && apt-get install -y git ca-certificates &&
|
||||
echo 'Installing opencode...' &&
|
||||
npm install -g opencode-ai 2>&1 || echo 'ERROR: npm install failed' &&
|
||||
which opencode || echo 'ERROR: opencode not in PATH' &&
|
||||
npm bin -g &&
|
||||
ls -la $(npm bin -g) || echo 'ERROR: global bin dir not found' &&
|
||||
echo 'export PATH=\"$(npm bin -g):\\$PATH\"' >> /root/.bashrc &&
|
||||
echo 'cd /home/user/{{WORKSPACE_NAME}}' >> /root/.bashrc &&
|
||||
echo 'OpenCode installation complete' &&
|
||||
exec tail -f /dev/null"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: 'no'""",
|
||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||
},
|
||||
]
|
||||
await _standardize_pi_agent_manifest(session)
|
||||
|
||||
for tool_data in builtin_types:
|
||||
for tool_data in BUILTIN_TOOL_TYPES:
|
||||
existing = await session.scalar(
|
||||
select(ToolType).where(ToolType.name == tool_data["name"])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user