#620: git_gate block messages guide instead of dead-end — explain WHY git is enforced, list key drone @git commands, point to --help, show disable path (git_gate.enabled=false in .aipass/hooks.json, verified engine skips disabled hooks per-hook). Split GIT_GH_REDIRECT -> GIT_REDIRECT + GH_REDIRECT; EDIT_REDIRECT shows disable too. Init notice in project_hooks.json template, on/off in README. 6 tests, seedgo 31/31.

This commit is contained in:
AIOSAI
2026-07-10 21:30:35 -07:00
parent b4e2370ee8
commit 0886c9013c
5 changed files with 103 additions and 11 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"_comment": "TEMPLATE: base per-project hook config copied into new projects by `aipass init` (DPLAN-0190). Mirrors AIPass's own .aipass/hooks.json. All handlers run from $AIPASS_HOME — projects only flip enabled true/false. Use `drone @hooks enable/disable <hook>` or edit here.",
"_comment": "TEMPLATE: base per-project hook config copied into new projects by `aipass init` (DPLAN-0190). Mirrors AIPass's own .aipass/hooks.json. All handlers run from $AIPASS_HOME — projects only flip enabled true/false. Use `drone @hooks enable/disable <hook>` or edit here. NOTE: git_gate is enabled by default — it enforces git via drone to prevent state conflicts. To disable for your project, set git_gate.enabled to false below (this won't break other hooks).",
"hooks_enabled": true,
"UserPromptSubmit": {
+12
View File
@@ -63,6 +63,18 @@ PyPI version — not the changelog header.
### Fixed
- **`git_gate` block messages now guide external users instead of dead-ending
(issue #620).** A blocked raw `git`/`gh` command previously just errored. The
block message now explains *why* git is enforced (prevents cross-agent state
conflicts), lists the key `drone @git` commands (commit, smart-sync, sync, pr,
checkout), points to `drone @git --help`, and shows how to disable the gate in
isolation (`git_gate.enabled = false` in `.aipass/hooks.json`) — verified
against the engine, which skips a disabled hook per-hook without affecting other
hooks or `drone @git`. The combined `GIT_GH_REDIRECT` was split into distinct
`GIT_REDIRECT` + `GH_REDIRECT`; `EDIT_REDIRECT` also shows the disable path. An
init notice was added to the `project_hooks.json` template and the on/off story
documented in the hooks README. 6 new tests (86 in `test_git_gate`).
- **Telegram `/create` + `/cancel` are now gated to the base @aipass bot (issue
#644).** Every per-branch bot inherited `BaseBot`'s `/create` + `/cancel` and
could mint new bots — but Patrick designated the base @aipass bot as the *sole*
+20
View File
@@ -118,6 +118,26 @@ Handlers are called **dynamically at runtime** — the engine uses `importlib.im
| Notification | announce | Announcement tone |
| PreCompact | compact, rollover | Memory archival + rollover |
## Git Gate
The `git_gate` handler (`security/git_gate.py`) enforces git access via drone to prevent state conflicts between agents. It is **enabled by default** in every project created by `aipass init`.
**What it blocks:** Raw `git` write commands (push, commit, checkout, merge, etc.) and raw `gh` commands (except `gh api`). Read-only git verbs (status, log, diff, show, blame, grep, etc.) are allowed raw.
**What it protects:** Edits to `.claude/settings.json`, `.claude/hooks/`, and `.git/hooks/` — the enforcement layer itself.
**Disabling for a project:** Set `git_gate.enabled` to `false` in your project's `.aipass/hooks.json`. This disables git enforcement in isolation — all other hooks (edit_gate, rm_gate, prompt injection, etc.) continue to work normally. No sync, rebase, or PR flows depend on git_gate being active; those are handled independently by `drone @git`.
```json
"git_gate": {
"enabled": false,
"handler": "aipass.hooks.apps.handlers.security.git_gate.handle",
"matcher": "Bash|Edit|MultiEdit|Write|NotebookEdit"
}
```
**Why it's on by default:** Agents reflexively reach for raw git, which causes state chaos in a multi-agent system. The gate redirects to `drone @git` which enforces access tiers (read-only for most branches, write-only for devpulse). External users who don't need multi-agent git orchestration can safely disable it.
## Kernel Sandbox (srt/bwrap)
The sandbox module (`apps/modules/sandbox.py`) provides the kernel-level filesystem boundary for agent sessions. It wraps Anthropic's `@anthropic-ai/sandbox-runtime` (srt) library, which uses bubblewrap (bwrap) + Landlock + seccomp on Linux to enforce write/read restrictions at the OS level.
@@ -61,20 +61,42 @@ EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
TRUSTED_HOOK_EDITORS = ("devpulse", "seedgo")
GIT_GH_REDIRECT = (
"Write git commands are blocked. Read-only verbs (status, log, diff, show, etc.) are allowed raw.\n"
"For write operations, use drone:\n"
" drone @git smart-sync # fetch + rebase\n"
" drone @git sync # checkout main + pull\n"
" drone @git issue list # GitHub issues\n"
GIT_REDIRECT = (
"AIPass enforces git via drone to prevent state conflicts between agents.\n"
"Read-only verbs (status, log, diff, show, blame, grep, etc.) are allowed raw.\n"
"\n"
"For write operations, use drone @git:\n"
" drone @git commit <msg> [--all | files] # commit changes\n"
" drone @git smart-sync # fetch + rebase\n"
" drone @git sync # checkout main + pull\n"
" drone @git pr <desc> # push + create PR\n"
" drone @git checkout <main|dev> # switch branches\n"
"\n"
"Run `drone @git --help` for the full command list.\n"
"To disable this gate for your project: set git_gate.enabled to false\n"
"in your .aipass/hooks.json (this won't break other AIPass hooks)."
)
GH_REDIRECT = (
"AIPass enforces gh via drone to prevent state conflicts between agents.\n"
"Only `gh api` is allowed raw.\n"
"\n"
"For GitHub operations, use drone @git:\n"
" drone @git issue list # list issues\n"
" drone @git run list # CI runs\n"
" drone @git workflow run # trigger workflows"
" drone @git workflow run # trigger workflows\n"
" drone @git pr <desc> # push + create PR\n"
"\n"
"Run `drone @git --help` for the full command list.\n"
"To disable this gate for your project: set git_gate.enabled to false\n"
"in your .aipass/hooks.json (this won't break other AIPass hooks)."
)
EDIT_REDIRECT = (
"{path} is protected — settings.json, .claude/hooks/, and .git/hooks/ "
"govern the enforcement layer itself.\n"
"If a real change is needed, ask devpulse to make it directly."
"If a real change is needed, ask devpulse to make it directly.\n"
"To disable this protection: set git_gate.enabled to false in .aipass/hooks.json."
)
_BLOCK_ALLOW = {"stdout": "", "exit_code": 0}
@@ -142,9 +164,9 @@ def _check_bash(tool_input: dict) -> dict:
scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd)
scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", scan)
if RAW_GIT_RE.search(scan) and not _all_git_reads(scan):
return _block(GIT_GH_REDIRECT)
return _block(GIT_REDIRECT)
if RAW_GH_RE.search(scan) and not _is_allowed_gh(cmd):
return _block(GIT_GH_REDIRECT)
return _block(GH_REDIRECT)
return _BLOCK_ALLOW
+38
View File
@@ -336,3 +336,41 @@ class TestGitGateMisc:
result = _bash("git push")
parsed = json.loads(result["stdout"])
assert "Read-only verbs" in parsed["reason"]
def test_git_block_explains_why(self):
result = _bash("git push")
parsed = json.loads(result["stdout"])
assert "enforces git via drone" in parsed["reason"]
def test_git_block_lists_drone_commands(self):
result = _bash("git commit -m 'msg'")
parsed = json.loads(result["stdout"])
assert "drone @git commit" in parsed["reason"]
assert "drone @git smart-sync" in parsed["reason"]
assert "drone @git --help" in parsed["reason"]
def test_git_block_shows_disable_path(self):
result = _bash("git push")
parsed = json.loads(result["stdout"])
assert "git_gate.enabled" in parsed["reason"]
assert "hooks.json" in parsed["reason"]
def test_gh_block_separate_message(self):
result = _bash("gh pr list")
parsed = json.loads(result["stdout"])
assert "enforces gh via drone" in parsed["reason"]
assert "gh api" in parsed["reason"]
assert "drone @git issue" in parsed["reason"]
def test_edit_block_shows_disable_path(self):
from aipass.hooks.apps.handlers.security.git_gate import handle
result = handle(
{
"tool_name": "Edit",
"tool_input": {"file_path": "/home/patrick/.claude/settings.json"},
"cwd": CWD,
}
)
parsed = json.loads(result["stdout"])
assert "git_gate.enabled" in parsed["reason"]