feat(system): fix(system): DPLAN-0163 — git_gate hardening, standalone project fixes, 113-test suite

Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
patrick
2026-05-03 23:16:39 -07:00
co-authored by @devpulse
parent 57e8ce4e9b
commit f7678581ee
6 changed files with 160 additions and 9 deletions
+35 -4
View File
@@ -22,8 +22,8 @@ from pathlib import Path
BLOCKED_GIT_VERBS = (
"commit", "push", "pull", "merge", "rebase", "reset",
"checkout", "switch", "branch", "cherry-pick", "revert",
"rm", "mv", "restore", "clean", "config", "tag",
"checkout", "switch", "cherry-pick", "revert",
"rm", "mv", "restore", "clean", "config",
)
BLOCKED_GIT_RE = re.compile(
@@ -35,6 +35,18 @@ BLOCKED_GIT_STASH_RE = re.compile(
r"(?<![@\w/.])git\s+stash\s+(drop|clear|pop|apply)\b"
)
BLOCKED_GIT_BRANCH_RE = re.compile(
r"(?<![@\w/.])git\s+branch\s+.*(-[dDmMcC]\b|--delete|--move|--copy|--force|--set-upstream-to|--unset-upstream)"
)
BLOCKED_GIT_TAG_RE = re.compile(
r"(?<![@\w/.])git\s+tag\s+.*(-d\b|--delete|--force|-f\b)"
)
BLOCKED_GIT_REMOTE_RE = re.compile(
r"(?<![@\w/.])git\s+remote\s+(add|remove|rename|set-url|set-branches|set-head|prune)\b"
)
BLOCKED_GH_API_RE = re.compile(
r"(?<![@\w/.])gh\s+api\b"
)
@@ -93,6 +105,22 @@ def _cwd_branch(cwd: str) -> str:
return ""
def _is_project_owner(cwd: str) -> bool:
"""Check if the current branch's passport has citizenship.owner: true."""
p = Path(cwd)
for d in [p] + list(p.parents):
passport = d / ".trinity" / "passport.json"
if passport.is_file():
try:
data = json.loads(passport.read_text(encoding="utf-8"))
return bool(data.get("citizenship", {}).get("owner"))
except Exception:
return False
if (d / ".git").exists():
break
return False
def main():
try:
data = json.load(sys.stdin)
@@ -108,10 +136,13 @@ def main():
# (PR descriptions, commit messages, examples in docs), not code to enforce.
scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd)
scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", scan)
if BLOCKED_GIT_STASH_RE.search(scan) or BLOCKED_GIT_RE.search(scan):
if (BLOCKED_GIT_RE.search(scan) or BLOCKED_GIT_STASH_RE.search(scan)
or BLOCKED_GIT_BRANCH_RE.search(scan) or BLOCKED_GIT_TAG_RE.search(scan)
or BLOCKED_GIT_REMOTE_RE.search(scan)):
_block(GIT_REDIRECT)
if BLOCKED_GH_API_RE.search(scan) or BLOCKED_GH_RE.search(scan):
_block(GH_REDIRECT)
if not (_cwd_branch(cwd) in TRUSTED_HOOK_EDITORS or _is_project_owner(cwd)):
_block(GH_REDIRECT)
return
if tool_name in EDIT_TOOLS:
+1
View File
@@ -146,3 +146,4 @@ claude_4_7_transition_notes.md
*.bak
src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/auth.cpython-312.pyc
src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/__init__.cpython-312.pyc
.backup_system
@@ -312,6 +312,16 @@ def global_prompt_md(name: str) -> str:
"Projects use the registry.\n"
"- **Memory** — update `.trinity/local.json` at session end. "
"Memory is presence.\n"
"\n"
"## Maintenance\n"
"\n"
"- **Upgrade scaffold**: `drone @cli aipass init update` refreshes "
"managed project files (hooks, prompts, settings) to latest templates.\n"
"- **Entry point**: each agent's `apps/{name}.py` auto-configures "
"`sys.path` and `AIPASS_BRANCH_NAME` env var. If prax logs to "
"`unknown_branch/`, check that these are set.\n"
"- **Standalone projects** use `src/{name}/` layout (not `src/aipass/{name}/`). "
"Module discovery adapts automatically.\n"
)
+56 -2
View File
@@ -27,6 +27,9 @@ _spec.loader.exec_module(_mod)
BLOCKED_GIT_RE = _mod.BLOCKED_GIT_RE
BLOCKED_GIT_STASH_RE = _mod.BLOCKED_GIT_STASH_RE
BLOCKED_GIT_BRANCH_RE = _mod.BLOCKED_GIT_BRANCH_RE
BLOCKED_GIT_TAG_RE = _mod.BLOCKED_GIT_TAG_RE
BLOCKED_GIT_REMOTE_RE = _mod.BLOCKED_GIT_REMOTE_RE
BLOCKED_GH_RE = _mod.BLOCKED_GH_RE
BLOCKED_GH_API_RE = _mod.BLOCKED_GH_API_RE
BLOCKED_EDIT_PATTERNS = _mod.BLOCKED_EDIT_PATTERNS
@@ -44,7 +47,6 @@ class TestGitWriteBlocking:
"git reset HEAD~1",
"git checkout -b new-branch",
"git switch -c new-branch",
"git branch -D old",
"git cherry-pick abc123",
"git revert HEAD",
"git rm file.txt",
@@ -52,7 +54,6 @@ class TestGitWriteBlocking:
"git restore --staged file.py",
"git clean -fd",
"git config user.name 'x'",
"git tag v1.0",
])
def test_blocks_write_verbs(self, cmd):
"""Each blocked git verb triggers the regex."""
@@ -68,6 +69,44 @@ class TestGitWriteBlocking:
"""Destructive stash subcommands are blocked."""
assert BLOCKED_GIT_STASH_RE.search(cmd), f"Should block: {cmd}"
@pytest.mark.parametrize("cmd", [
"git branch -D old",
"git branch -d old",
"git branch -m old new",
"git branch -M old new",
"git branch -c old new",
"git branch --delete old",
"git branch --move old new",
"git branch --copy old new",
"git branch --force old",
"git branch --set-upstream-to=origin/main",
"git branch --unset-upstream",
])
def test_blocks_branch_destructive(self, cmd):
"""Destructive branch subcommands are blocked."""
assert BLOCKED_GIT_BRANCH_RE.search(cmd), f"Should block: {cmd}"
@pytest.mark.parametrize("cmd", [
"git tag -d v1.0",
"git tag --delete v1.0",
"git tag -f v1.0",
"git tag --force v1.0",
])
def test_blocks_tag_destructive(self, cmd):
"""Destructive tag operations are blocked."""
assert BLOCKED_GIT_TAG_RE.search(cmd), f"Should block: {cmd}"
@pytest.mark.parametrize("cmd", [
"git remote add origin url",
"git remote remove origin",
"git remote rename old new",
"git remote set-url origin url",
"git remote prune origin",
])
def test_blocks_remote_destructive(self, cmd):
"""Destructive remote operations are blocked."""
assert BLOCKED_GIT_REMOTE_RE.search(cmd), f"Should block: {cmd}"
class TestLongFormFlagBypass:
"""DPLAN-0163 Finding 1: long-form flags must not bypass detection."""
@@ -135,11 +174,26 @@ class TestReadOnlyAllowed:
"git remote -v",
"git blame file.py",
"git shortlog -sn",
"git branch",
"git branch -r",
"git branch -a",
"git branch --list",
"git branch -v",
"git branch --show-current",
"git branch --contains abc123",
"git tag",
"git tag -l",
"git tag --list",
"git remote",
"git remote show origin",
])
def test_allows_read_only(self, cmd):
"""Read-only git subcommands must pass through."""
assert not BLOCKED_GIT_RE.search(cmd), f"Should allow: {cmd}"
assert not BLOCKED_GIT_STASH_RE.search(cmd), f"Should allow: {cmd}"
assert not BLOCKED_GIT_BRANCH_RE.search(cmd), f"Should allow: {cmd}"
assert not BLOCKED_GIT_TAG_RE.search(cmd), f"Should allow: {cmd}"
assert not BLOCKED_GIT_REMOTE_RE.search(cmd), f"Should allow: {cmd}"
class TestDroneNotBlocked:
+39
View File
@@ -0,0 +1,39 @@
# Backup System ignore patterns (gitignore-style)
# Lines starting with # are comments. Blank lines are ignored.
# Backup system's own directory
.backup_system/
# Version control
.git/
.svn/
.hg/
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
.venv/
venv/
.tox/
# Node
node_modules/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build artifacts
build/
dist/
# Logs
*.log
@@ -7,12 +7,19 @@ Auto-discovery architecture:
- No manual imports or routing needed
"""
import sys
import importlib
import os
import sys
from pathlib import Path
from typing import List, Any
from aipass.prax import logger
PROJECT_ROOT = str(Path(__file__).resolve().parent.parent)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
os.environ.setdefault("AIPASS_BRANCH_NAME", "{{BRANCH}}")
from aipass.prax import logger # noqa: E402
# =============================================================================
# MODULE DISCOVERY
@@ -21,6 +28,15 @@ from aipass.prax import logger
MODULES_DIR = Path(__file__).parent / "modules"
def _module_import_path(stem: str) -> str:
"""Return the correct import path for a module, handling both layouts."""
try:
importlib.import_module(f"aipass.{{BRANCH}}.apps.modules.{stem}")
return f"aipass.{{BRANCH}}.apps.modules.{stem}"
except ImportError:
return f"apps.modules.{stem}"
def discover_modules() -> List[Any]:
"""Auto-discover modules in modules/ directory."""
modules = []
@@ -32,7 +48,7 @@ def discover_modules() -> List[Any]:
if file_path.name.startswith("_"):
continue
module_name = f"aipass.{{BRANCH}}.apps.modules.{file_path.stem}"
module_name = _module_import_path(file_path.stem)
try:
module = importlib.import_module(module_name)