feat(tools): add structured GitTool with schema-enforced safety#61
feat(tools): add structured GitTool with schema-enforced safety#61glitch-ux wants to merge 1 commit intoHKUDS:mainfrom
Conversation
Replace raw git-via-BashTool with a dedicated GitTool that exposes typed, Pydantic-validated operations. Dangerous git commands cannot be expressed in the schema — safety is structural, not prompt-based. Supports 15 operations: status, diff, log, show, blame, branch_list, add, commit, push, pull, branch_create, branch_delete, checkout, stash, and tag. Includes 31 tests covering all operations, input validation, safety constraints, permission integration, and registry.
|
To expand on the motivation: the key shift here is moving git safety from pattern matching to schema design. Today, the permission system can only react to git commands after they're already composed as strings — matching against The
Net result: fewer permission prompts for safe operations, zero surface area for destructive ones. |
Motivation
Today, every git operation in OpenHarness flows through
BashToolas a raw command string. The permission system seestool_name="bash"and a flat string — it has no idea whether the agent is runninggit statusorgit push --force. The only defense is manually enumerating glob patterns indenied_commands, which is brittle, easy to bypass with flag variants, and provides no structured context in approval dialogs.This PR introduces a dedicated
GitToolthat replaces the raw-string approach with typed, Pydantic-validated operations. The core idea: if a dangerous flag doesn't exist in the schema, the model can't use it. Safety becomes a property of the tool's API surface, not a race between prompt instructions and glob patterns.Before vs After
BashTool(command="git push -f")— executes unless you pre-configureddenied_commands: ["*push*-f*", "*push*--force*", "*push*--force-with-lease*", ...]GitTool(operation="push", ref="main")— noforcefield exists in the schema. Structurally impossible.BashTool(command="git commit --no-verify -m 'msg'")— executes unless glob-matchedGitTool(operation="commit", message="msg")— noskip_hooksfield. Handler always builds["commit", "-m", message].BashTool(command="git add -A")— stages secrets, binaries, everythingGitTool(operation="add", files=["src/foo.py"])— requires explicit file list. Validator rejects.,-A,--all. Handler rejects any-prefix.BashTool(command="git reset --hard HEAD~5")— destroys workresetis not in the operation enum.BashTool(command="git branch -D feature")— force-deletesGitTool(operation="branch_delete", ref="feature")— handler hardcodes-d(safe delete). Git itself refuses if the branch has unmerged work.git push origin main --force?" — user must parse the raw stringstatus,diff,log,show,blame,branch_listreturnis_read_only=True— auto-allowed in default permission modebash(blocks everything, not just git)gitspecifically viadenied_tools: ["git"]while keepingbashavailable for non-git workHow it works
The tool follows the
LspToolpattern: a singleoperation: Literal[...]field selects one of 15 git operations, and a@model_validatorenforces per-operation required fields.15 operations supported:
status,diff,log,show,blame,branch_listadd,commit,push,pull,branch_create,branch_delete,checkout,stash,tagPermission system integration:
is_read_only()returnsTruefor the 6 inspect operations — auto-allowed in default permission mode, no user prompt needed@property commandsynthesizes a string like"git push origin main"that_extract_permission_command()inquery.pypicks up — so existingdenied_commandsglob patterns still work"git"integrates withdenied_tools/allowed_toolsfor blanket controlSubprocess safety: Uses the same
_run_git()pattern asswarm/worktree.py—GIT_TERMINAL_PROMPT=0andGIT_ASKPASS=""prevent interactive prompts from hanging the agent. All handlers use--before file arguments to prevent argument injection via filenames.Files changed
src/openharness/tools/git_tool.pysrc/openharness/tools/__init__.pycreate_default_tool_registry()tests/test_tools/test_git_tool.pyCHANGELOG.mdTest plan
uv run ruff check— all checks passeduv run pytest tests/test_tools/test_git_tool.py -v— 31/31 passeduv run pytest -q— 537 passed, 6 skipped, 1 xfailed, 0 regressionsTests cover: all 15 operations end-to-end, input validation (rejected patterns like
.,-A,--all,-prefixes),is_read_onlycorrectness,commandproperty synthesis, JSON schema exclusion ofcommand, registry integration, and non-git-repo error handling.