Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions code_puppy/plugins/paw_sign/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Paw-sign plugin.

Appends a playful puppy-themed signature to commit messages that
code-puppy composes, so the audience knows the commit came from a very
good boy. Intercepts ``git commit -m`` shell commands via the
``pre_tool_call`` hook and tacks on an extra ``-m`` paragraph.
"""
107 changes: 107 additions & 0 deletions code_puppy/plugins/paw_sign/detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Detection + rewriting for paw-signing git commit commands.

The single public entry point is :func:`paw_sign_command`. It is deliberately
conservative: it only rewrites a command when doing so is provably safe, and
returns ``None`` (leave it alone) for anything ambiguous. A corrupted
``git commit`` is far worse than a missing paw print.

Safety strategy
---------------
We use :mod:`shlex` purely to *analyse* the command (respecting quotes), never
to rebuild it. We only append when the ``git commit -m`` invocation is the
**last** command in the chain -- then a plain string append unambiguously
flows into that commit, preserving any shell substitutions, heredocs, or exotic
quoting in the original untouched.

Examples that get signed::

git commit -m "fix the thing"
git add . && git commit -m "fix the thing"
git commit --message "wip" --amend

Examples we leave alone (return ``None``)::

git commit -m "wip" && git push # commit isn't last
git commit --amend # editor commit, no -m
git status # not a commit at all
"""

import shlex
from typing import List, Optional

# NOTE: the paw print is written as a unicode escape (U+1F43E) on purpose.
# This repo's emoji_filter strips literal emoji from file writes, which would
# silently eat the paw. The escape is plain ASCII in source and resolves to the
# real glyph at runtime.
PAW_SIGN = "[-- paw-signed: code-puppy \U0001f43e --]"

# A stable substring (no emoji) used for idempotency -- survives even if the
# emoji has been stripped from a previously-signed command.
_ALREADY_SIGNED = "paw-signed: code-puppy"

# Shell operators that separate one command from the next.
_OPERATORS = frozenset({"&&", "||", ";", "|", "&"})


def _split_segments(tokens: List[str]) -> List[List[str]]:
"""Split a flat token list into command segments on shell operators."""
segments: List[List[str]] = [[]]
for tok in tokens:
if tok in _OPERATORS:
segments.append([])
else:
segments[-1].append(tok)
return segments


def _is_commit_with_message(segment: List[str]) -> bool:
"""Return True if a token segment is ``git commit`` carrying a message flag.

Handles ``-m``, ``--message``, ``--message=...`` and combined short flags
such as ``-am`` / ``-sm``. Deliberately rejects editor-style commits
(``git commit`` / ``git commit --amend`` with no message flag).
"""
if len(segment) < 2 or segment[0] != "git" or segment[1] != "commit":
return False
for tok in segment[2:]:
if tok in ("-m", "--message") or tok.startswith("--message="):
return True
# Combined short flags like -am, -sm. Guard against long flags such as
# --amend, which contain an "m" but are not message flags.
if tok.startswith("-") and not tok.startswith("--") and "m" in tok:
return True
return False


def paw_sign_command(command: str) -> Optional[str]:
"""Return ``command`` with the paw-sign appended, or ``None`` for no change.

Args:
command: The shell command about to run.

Returns:
The rewritten command string when it is a safe-to-sign ``git commit``
whose message-bearing invocation is the last in the chain; otherwise
``None`` (meaning: leave the original command untouched).
"""
# Cheap pre-filters before paying for shlex.
if not command or "commit" not in command:
return None
if _ALREADY_SIGNED in command:
return None

try:
tokens = shlex.split(command)
except ValueError:
# Unbalanced quotes / un-parseable -- never risk mangling it.
return None

if not tokens:
return None

segments = _split_segments(tokens)
# Only safe to append at the end when the *last* segment is the commit.
if not _is_commit_with_message(segments[-1]):
return None

return f"{command.rstrip()} -m {shlex.quote(PAW_SIGN)}"
54 changes: 54 additions & 0 deletions code_puppy/plugins/paw_sign/register_callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Wire the paw-sign plugin into the runtime.

Hooks ``pre_tool_call`` and mutates the ``agent_run_shell_command`` args in
place, appending a puppy signature to ``git commit -m`` messages composed by
code-puppy. Never blocks a tool call; failures are swallowed so a bad rewrite
can never crash the agent.
"""

import logging
from typing import Any, Dict, Optional

from code_puppy.callbacks import register_callback
from code_puppy.plugins.paw_sign.detector import paw_sign_command

logger = logging.getLogger(__name__)

_SHELL_TOOL = "agent_run_shell_command"


def _on_pre_tool_call(
tool_name: str, tool_args: Dict[str, Any], context: Any = None
) -> Optional[Dict[str, Any]]:
"""Append the paw-sign to qualifying ``git commit`` shell commands.

Mutates ``tool_args["command"]`` in place (the same pattern the emoji_filter
plugin uses). Returns ``None`` always -- this is a silent, non-blocking
rewrite.
"""
if tool_name != _SHELL_TOOL or not isinstance(tool_args, dict):
return None

command = tool_args.get("command")
if not isinstance(command, str):
return None

try:
signed = paw_sign_command(command)
except Exception as exc: # never break tool execution over a signature
logger.debug("paw_sign pre_tool_call failed: %s", exc)
return None

if signed is not None:
tool_args["command"] = signed

return None


def register() -> None:
"""Register the paw-sign pre_tool_call callback."""
register_callback("pre_tool_call", _on_pre_tool_call)


# Auto-register when this module is imported by the plugin loader.
register()
116 changes: 116 additions & 0 deletions code_puppy/plugins/paw_sign/test_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for the paw-sign command detector."""

from code_puppy.plugins.paw_sign.detector import (
PAW_SIGN,
_is_commit_with_message,
_split_segments,
paw_sign_command,
)


# --- the happy path: commands that should get signed ------------------------


def test_simple_commit_gets_signed():
out = paw_sign_command('git commit -m "fix the thing"')
assert out == "git commit -m \"fix the thing\" -m '%s'" % PAW_SIGN


def test_add_then_commit_signs_the_commit():
out = paw_sign_command('git add . && git commit -m "wip"')
assert out is not None
assert out.endswith("-m '%s'" % PAW_SIGN)
# original commit body untouched
assert 'git commit -m "wip"' in out


def test_long_message_flag():
out = paw_sign_command('git commit --message "hello"')
assert out is not None
assert out.endswith("-m '%s'" % PAW_SIGN)


def test_message_equals_form():
out = paw_sign_command('git commit --message="hello"')
assert out is not None
assert out.endswith("-m '%s'" % PAW_SIGN)


def test_combined_short_flags():
out = paw_sign_command('git commit -am "quick"')
assert out is not None
assert out.endswith("-m '%s'" % PAW_SIGN)


def test_amend_with_message_is_signed():
out = paw_sign_command('git commit --amend -m "redo"')
assert out is not None
assert out.endswith("-m '%s'" % PAW_SIGN)


def test_paw_sign_contains_emoji_glyph():
# The escape really does resolve to the paw-print glyph at runtime.
assert "\U0001f43e" in PAW_SIGN


# --- the conservative path: commands we must leave alone --------------------


def test_editor_commit_not_signed():
assert paw_sign_command("git commit") is None
assert paw_sign_command("git commit --amend") is None


def test_commit_not_last_in_chain_not_signed():
# Appending at the end would attach to `git push`, so we bail.
assert paw_sign_command('git commit -m "wip" && git push') is None


def test_non_commit_command_not_signed():
assert paw_sign_command("git status") is None
assert paw_sign_command("echo hello") is None
assert paw_sign_command("ls -la") is None


def test_empty_or_none_not_signed():
assert paw_sign_command("") is None
assert paw_sign_command(None) is None


def test_unbalanced_quotes_not_signed():
assert paw_sign_command('git commit -m "unterminated') is None


def test_idempotent_already_signed():
once = paw_sign_command('git commit -m "fix"')
assert once is not None
# Running it again must be a no-op (no double paw).
assert paw_sign_command(once) is None


def test_commit_word_in_string_not_misfired():
# "commit" appears but it's not a git commit command.
assert paw_sign_command('echo "let us commit to this"') is None


def test_trailing_operator_not_signed():
# A trailing ; leaves an empty last segment -> not safe to append.
assert paw_sign_command('git commit -m "wip" ;') is None


def test_signoff_no_message_not_signed():
assert paw_sign_command("git commit -s") is None


# --- helper-level unit tests ------------------------------------------------


def test_split_segments_basic():
segs = _split_segments(["git", "add", ".", "&&", "git", "commit"])
assert segs == [["git", "add", "."], ["git", "commit"]]


def test_is_commit_with_message_rejects_long_m_flags():
# --amend contains an "m" but must NOT count as a message flag.
assert _is_commit_with_message(["git", "commit", "--amend"]) is False
assert _is_commit_with_message(["git", "commit", "-m", "x"]) is True
Loading