Refuse an origin.repo of /, the home directory or a relative path

Every run hashes the python files below origin.repo, so saving or syncing
a flow with one of those now answers 422 with what to do instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 10:55:40 +02:00
co-authored by Claude Opus 5.5
parent 068ff45108
commit 7f99450593
3 changed files with 46 additions and 1 deletions
+31
View File
@@ -1,7 +1,9 @@
"""The flow API. Everything the editor can do is available here first."""
import asyncio
import os
import time
from pathlib import Path
from typing import Any
import orjson
@@ -400,6 +402,34 @@ def _check_flavors(definition: FlowDef) -> None:
)
def _check_origin(definition: FlowDef) -> None:
"""Refuse an ``origin.repo`` that is not a project's own directory.
Every run of a code-defined flow hashes the python files below it, so
``/`` or a home directory makes each run walk far more than the project,
and a relative path names nothing the engine can find.
"""
repo = definition.origin.repo if definition.origin is not None else ""
if not repo:
return
if not os.path.isabs(repo):
problem = "is a relative path"
elif os.path.realpath(repo) == "/":
problem = "is the filesystem root"
elif os.path.realpath(repo) == os.path.realpath(Path.home()):
problem = "is the home directory"
else:
return
raise HTTPException(
status_code=422,
detail=(
f"origin.repo '{repo}' {problem}. It has to be the absolute path "
"of the repository the flow is declared in, since every run hashes "
"the python files below it. Sync from inside that repository."
),
)
def _worker_env(node: NodeDef | None) -> dict[str, str]:
return node.resources.env if node is not None and node.resources else {}
@@ -468,6 +498,7 @@ async def save_flow(
raise HTTPException(status_code=400, detail="Node names must be unique")
_check_flavors(definition)
_check_origin(definition)
if not user.is_superuser:
_refuse_code_changes(controller, name, definition)
+10 -1
View File
@@ -9,7 +9,7 @@ from fastapi.testclient import TestClient
from fluksio.core.config import settings
from fluksio.sdk import FLOWS, MARKER, SyncError
from fluksio.sdk.client import Client, sync
from fluksio.sdk.client import ApiError, Client, sync
EXAMPLES = Path(__file__).parents[4] / "examples"
PREFIX = settings.API_V1_STR
@@ -47,6 +47,15 @@ def commits() -> int:
return int(result.stdout.strip()) if result.returncode == 0 else 0
@pytest.mark.parametrize("repo", ["/", str(Path.home()), "examples"])
def test_sync_refuses_a_repo_every_run_would_walk_the_disk_for(api, flows, repo):
with pytest.raises(ApiError) as refused:
sync([flows["train"]], api, origin={**ORIGIN, "repo": repo})
assert refused.value.status == 422
assert "origin.repo" in str(refused.value.detail)
def test_sync_stores_a_runnable_flow_with_its_origin(api, flows):
reports = sync([flows["train"]], api, origin=ORIGIN)