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
74 lines
2.2 KiB
Python
74 lines
2.2 KiB
Python
"""Projects domain: projects, their Git repositories and per-project credentials."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
|
|
from app.domain.base import new_id, utcnow
|
|
|
|
|
|
@dataclass(slots=True, kw_only=True)
|
|
class Project:
|
|
organization_id: str
|
|
name: str
|
|
slug: str
|
|
description: str = ""
|
|
created_by_user_id: str | None = None
|
|
default_branch: str = "main"
|
|
id: str = field(default_factory=new_id)
|
|
created_at: datetime = field(default_factory=utcnow)
|
|
|
|
|
|
@dataclass(slots=True, kw_only=True)
|
|
class ProjectGitRepo:
|
|
"""The Git repository attached to a project.
|
|
|
|
Named ``ProjectGitRepo`` rather than ``ProjectRepository`` on purpose: the
|
|
latter collided with the *infrastructure* repository that manages ``Project``
|
|
rows, and the shadowing silently broke every Git endpoint at runtime.
|
|
"""
|
|
|
|
project_id: str
|
|
provider: str = "gitea"
|
|
owner: str = ""
|
|
repo_name: str = ""
|
|
clone_url: str = ""
|
|
web_url: str = ""
|
|
default_branch: str = "main"
|
|
id: str = field(default_factory=new_id)
|
|
created_at: datetime = field(default_factory=utcnow)
|
|
|
|
@property
|
|
def full_name(self) -> str:
|
|
return f"{self.owner}/{self.repo_name}"
|
|
|
|
|
|
@dataclass(slots=True, kw_only=True)
|
|
class ProjectGitCredential:
|
|
"""Per-project Git identity. The password is encrypted at rest and never
|
|
returned to the frontend."""
|
|
|
|
project_id: str
|
|
username: str
|
|
encrypted_password: str
|
|
id: str = field(default_factory=new_id)
|
|
created_at: datetime = field(default_factory=utcnow)
|
|
|
|
|
|
@dataclass(slots=True, kw_only=True)
|
|
class ProjectExternalGit:
|
|
"""Credentials for a *user-supplied* git remote (v2 ``git_source=external``).
|
|
|
|
Separate from :class:`ProjectGitCredential`, which is the platform's own
|
|
Gitea identity. Keeping them apart means switching a project to an external
|
|
remote can never accidentally reuse — or expose — the platform token.
|
|
"""
|
|
|
|
project_id: str
|
|
auth_type: str = "none" # none | token | ssh_key
|
|
username: str | None = None
|
|
secret_encrypted: str | None = None
|
|
known_hosts: str | None = None
|
|
updated_at: datetime = field(default_factory=utcnow)
|