Files
mjalmoudy94 d41e00ad10 feat(delivery): git workspaces + SSRF host check (v2 phase A3-2)
The platform's side of D6: git runs on the host, in a gitdir that is never
mounted into a container, so repository credentials never reach the model.

- adapters/git_workspace.py: LocalGitWorkspaceAdapter implementing
  RepoWorkspacePort. Bare shared gitdir + a dev work tree mounted at
  /workspace + one linked worktree per task under work/.worktrees/<id>
  (excluded via the gitdir's info/exclude, not the project's .gitignore).
  Explicit --git-dir/--work-tree, core.hooksPath=/dev/null, fsmonitor off,
  quotePath off, GIT_CONFIG_NOSYSTEM, a private HOME per project, prompts off,
  one asyncio.Lock per project. Token via GIT_ASKPASS reading a 0600 file; SSH
  key as a 0600 temp file with a pinned known_hosts. Everything deleted in a
  finally; only a *path* ever travels through the environment.
- The bug this phase existed to find: --git-dir=<shared> does not address a
  linked worktree, so `git commit` in a task worktree committed straight to
  `dev`. Operations now address <gitdir>/worktrees/<task_id> explicitly — and
  never read the worktree's .git file, which a repository can rewrite.
- domain/platform/rules.py: is_blocked_address (pure) + assert_remote_host_allowed
  (resolves, refuses if any address is loopback/private/link-local/metadata).
  Shared by the git remote test and, later, every deploy target.
- ProjectExternalGit domain model + repository; container_delivery wires the
  real adapter behind GIT_WORKSPACE=local with a resolver that joins the
  project, its repo and its (decrypted) credentials.

Tests: 394 API (+48) against real git and a local bare repo; ruff and mypy clean.

Report: docs/plan/reports/agent-3-phase-2.md
2026-10-14 00:18:13 +03:30

76 lines
2.1 KiB
TOML

[project]
name = "smart-org-api"
version = "0.1.0"
description = "Smart Organization backend — FastAPI + SQLAlchemy, Clean/Hexagonal architecture."
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.34.0",
"sqlalchemy[asyncio]>=2.0.36",
"psycopg[binary]>=3.2.3",
"aiosqlite>=0.20.0",
"alembic>=1.14.0",
"pydantic>=2.10.0",
"pydantic-settings>=2.7.0",
"email-validator>=2.2.0",
"PyJWT>=2.10.0",
"argon2-cffi>=23.1.0",
"cryptography>=44.0.0",
"redis>=5.2.0",
"httpx>=0.28.0",
"python-multipart>=0.0.20",
"asyncssh>=2.17.0",
"aioftp>=0.23.0",
"mcp>=1.2.0",
]
[project.optional-dependencies]
# Remote sandbox providers. Kept optional: these SDKs are large and move fast,
# and a default install must run with SANDBOX_PROVIDER=mock and zero credentials.
# Enable with: pip install -e ".[sandbox-remote]"
sandbox-remote = [
"daytona>=0.100",
"e2b>=1.0",
]
dev = [
"pytest>=8.3.0",
"pytest-asyncio>=0.25.0",
"ruff>=0.9.0",
"mypy>=1.14.0",
]
[build-system]
requires = ["setuptools>=75.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["app*"]
[tool.ruff]
line-length = 100
target-version = "py312"
# Alembic's autogenerated revisions are machine-written: their long lines and
# import style are not ours to fix, and editing them by hand invites mistakes.
exclude = ["alembic/versions"]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "C4", "SIM"]
ignore = ["B008"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]
# `docker` marks tests that need a real container runtime, `deploy_it` those
# that need a live sshd/ftp server. Both are skipped unless the matching
# environment flag is set, so the default run needs no infrastructure.
markers = [
"docker: needs a running docker/podman (set SANDBOX_DOCKER_IT=1)",
"deploy_it: needs a live ssh/ftp test server (set DEPLOY_IT=1)",
]
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
warn_unused_ignores = false