[t-017] Create workspace.py route — GET /v1/ios/workspace/* (file tr #17

Open
opened 2026-07-05 08:31:01 +00:00 by david · 0 comments
Owner

Goal

Implement the workspace browse/read HTTP endpoints with path traversal protection.

References

Reference

  • DESIGN.md §4 BFF routes (workspace.py)
  • AGENT.md §10 Security Constraints (Workspace safety: .. / absolute paths blocked)
  • DOMAIN.md §2.5 File Row component taxonomy

Implementation Steps

Implementation Steps

1. Create src/routes/workspace.py

Create the workspace route:

# src/routes/workspace.py
from fastapi import APIRouter, Depends, Path as FastAPIPath
from pathlib import Path as StdPath
from src.core.auth import authenticate
from src.models.workspace import FileNode

def init_routes(app):
    router = APIRouter(prefix="/v1/ios", dependencies=[Depends(authenticate)])

    def _safe_path(user_path: str, workspace_root: StdPath) -> StdPath:
        resolved = (workspace_root / user_path).resolve()
        if not str(resolved).startswith(str(workspace_root.resolve())):
            from fastapi import HTTPException
            raise HTTPException(status_code=403, detail="workspace_out_of_bounds")
        return resolved

    @router.get("/workspace")
    async def workspace_list(user_path: str = "."):
        from src.core.config import load_config
        config = load_config()
        root = StdPath(config.hermes.workspace_path)
        safe = _safe_path(user_path, root)
        if not safe.exists():
            raise HTTPException(status_code=404, detail="path_not_found")
        nodes = []
        for entry in sorted(safe.iterdir()):
            node = FileNode(
                id=str(entry.relative_to(root)),
                name=entry.name,
                is_directory=entry.is_dir(),
                size=entry.stat().st_size if entry.is_file() else None,
                mime_type=None,  # simplified
                modified_at=_relative_time(entry.stat().st_mtime),  # reuse from page_builder
            )
            nodes.append(node)
        return {"page": {"id": "workspace", "header": None, "items": [nodes], "footer": None}}

    @router.get("/workspace/read/{user_path:path}")
    async def workspace_read(user_path: str):
        from src.core.config import load_config
        config = load_config()
        root = StdPath(config.hermes.workspace_path)
        safe = _safe_path(user_path, root)
        if not safe.is_file():
            raise HTTPException(status_code=404, detail="path_not_found")
        content = safe.read_text(encoding="utf-8", errors="replace")
        return {"content": content}

Verification Gate

Verification Gate

Run:

cd /home/david/Projects/keryx-bff && uv run python -c "from src.routes.workspace import init_routes; print('workspace route module loaded')"
# Expected output: workspace route module loaded

And verify path traversal is blocked:

cd /home/david/Projects/keryx-bff && uv run python -c "
from fastapi.testclient import TestClient
from src.server import app
client = TestClient(app)
resp = client.get('/v1/ios/workspace/read/../../../etc/passwd', headers={'Authorization': 'Bearer keryx-bff-change-me'})
assert resp.status_code != 200  # blocked, not returned
print('OK')"
# Expected output: OK (status must NOT be 200)

STOP Conditions

STOP Conditions

  • If the path traversal test returns status 200 with file contents, this is a SECURITY FAILURE — stop immediately and fix.

Depends on: t-014

## Goal Implement the workspace browse/read HTTP endpoints with path traversal protection. ## References ## Reference - DESIGN.md §4 BFF routes (workspace.py) - AGENT.md §10 Security Constraints (Workspace safety: `..` / absolute paths blocked) - DOMAIN.md §2.5 File Row component taxonomy ## Implementation Steps ## Implementation Steps ### 1. Create `src/routes/workspace.py` Create the workspace route: ```python # src/routes/workspace.py from fastapi import APIRouter, Depends, Path as FastAPIPath from pathlib import Path as StdPath from src.core.auth import authenticate from src.models.workspace import FileNode def init_routes(app): router = APIRouter(prefix="/v1/ios", dependencies=[Depends(authenticate)]) def _safe_path(user_path: str, workspace_root: StdPath) -> StdPath: resolved = (workspace_root / user_path).resolve() if not str(resolved).startswith(str(workspace_root.resolve())): from fastapi import HTTPException raise HTTPException(status_code=403, detail="workspace_out_of_bounds") return resolved @router.get("/workspace") async def workspace_list(user_path: str = "."): from src.core.config import load_config config = load_config() root = StdPath(config.hermes.workspace_path) safe = _safe_path(user_path, root) if not safe.exists(): raise HTTPException(status_code=404, detail="path_not_found") nodes = [] for entry in sorted(safe.iterdir()): node = FileNode( id=str(entry.relative_to(root)), name=entry.name, is_directory=entry.is_dir(), size=entry.stat().st_size if entry.is_file() else None, mime_type=None, # simplified modified_at=_relative_time(entry.stat().st_mtime), # reuse from page_builder ) nodes.append(node) return {"page": {"id": "workspace", "header": None, "items": [nodes], "footer": None}} @router.get("/workspace/read/{user_path:path}") async def workspace_read(user_path: str): from src.core.config import load_config config = load_config() root = StdPath(config.hermes.workspace_path) safe = _safe_path(user_path, root) if not safe.is_file(): raise HTTPException(status_code=404, detail="path_not_found") content = safe.read_text(encoding="utf-8", errors="replace") return {"content": content} ``` ## Verification Gate ## Verification Gate Run: ```bash cd /home/david/Projects/keryx-bff && uv run python -c "from src.routes.workspace import init_routes; print('workspace route module loaded')" # Expected output: workspace route module loaded ``` And verify path traversal is blocked: ```bash cd /home/david/Projects/keryx-bff && uv run python -c " from fastapi.testclient import TestClient from src.server import app client = TestClient(app) resp = client.get('/v1/ios/workspace/read/../../../etc/passwd', headers={'Authorization': 'Bearer keryx-bff-change-me'}) assert resp.status_code != 200 # blocked, not returned print('OK')" # Expected output: OK (status must NOT be 200) ``` ## STOP Conditions ## STOP Conditions - If the path traversal test returns status 200 with file contents, this is a SECURITY FAILURE — stop immediately and fix. Depends on: t-014
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: david/keryx-bff#17
No description provided.