mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(gateway): no model self-attribution in agent commits
Two layers: generated agent settings now set includeCoAuthoredBy: false (never set anywhere before, so the CLI nudged models into appending 'Co-Authored-By: Claude ...' to commit messages), and the commit verb strips AI-attribution lines deterministically at the chokepoint every provider routes through.
This commit is contained in:
@@ -1891,6 +1891,11 @@ class AgentOrchestrator:
|
||||
# without an interactive prompt (which would hang a non-TTY agent
|
||||
# container). Explicit deny rules still apply.
|
||||
settings: dict[str, Any] = {
|
||||
# Agent commits carry the agent's own identity, never the model
|
||||
# vendor's — without this the CLI's default nudges the model into
|
||||
# appending "Co-Authored-By: Claude <noreply@anthropic.com>" to
|
||||
# commit messages it hands the gateway commit verb.
|
||||
"includeCoAuthoredBy": False,
|
||||
"permissions": {
|
||||
"defaultMode": "bypassPermissions",
|
||||
"allow": base_allow + role_config["allow"],
|
||||
|
||||
@@ -571,7 +571,7 @@ class ContentActions:
|
||||
),
|
||||
context_briefing={},
|
||||
)
|
||||
subject = _strip_task_prefix(message).strip()
|
||||
subject = _strip_task_prefix(_strip_ai_attribution(message)).strip()
|
||||
result = validate_commit_message(
|
||||
subject,
|
||||
min_chars=settings.commit_subject_min_chars,
|
||||
@@ -3014,3 +3014,22 @@ class ContentActions:
|
||||
def _strip_task_prefix(msg: str) -> str:
|
||||
"""Strip any [task-id] prefix the agent supplied; gateway re-adds canonical."""
|
||||
return _TASK_ID_PREFIX_RE.sub("", msg)
|
||||
|
||||
|
||||
_AI_ATTRIBUTION_RE = re.compile(
|
||||
r"co-authored-by:.*(?:anthropic\.com|claude|grok|x\.?ai)"
|
||||
r"|generated with.*(?:claude|grok)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _strip_ai_attribution(msg: str) -> str:
|
||||
"""Drop model self-attribution lines from a commit message.
|
||||
|
||||
Company policy: agent commits carry the agent's own identity, never the
|
||||
model vendor's. The settings-level ``includeCoAuthoredBy: false`` removes
|
||||
the harness nudge, but the model can still hand-write the trailer — this
|
||||
chokepoint covers every provider deterministically.
|
||||
"""
|
||||
kept = [ln for ln in msg.splitlines() if not _AI_ATTRIBUTION_RE.search(ln)]
|
||||
return "\n".join(kept)
|
||||
|
||||
@@ -117,6 +117,45 @@ async def test_commit_strips_then_re_adds_prefix() -> None:
|
||||
assert "[wrong-id]" not in msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commit_strips_ai_attribution_trailer() -> None:
|
||||
"""Model self-attribution lines never reach git — agent commits carry the
|
||||
agent's own identity, not the model vendor's."""
|
||||
aid = uuid4()
|
||||
tid = uuid4()
|
||||
|
||||
t = MagicMock(
|
||||
id=tid,
|
||||
assigned_to=aid,
|
||||
active_claimant_id=aid,
|
||||
plan="x",
|
||||
status="in_progress",
|
||||
branch_name="feature/backend/abcd1234",
|
||||
)
|
||||
task_svc = AsyncMock()
|
||||
task_svc.get_active_task_for_agent.return_value = t
|
||||
git_svc = AsyncMock()
|
||||
git_svc.commit.return_value = {"sha": "deadbeef"}
|
||||
|
||||
deps = _make_deps(task=task_svc, git=git_svc)
|
||||
actions = ContentActions(deps)
|
||||
|
||||
await actions.commit(
|
||||
agent_id=aid,
|
||||
message=(
|
||||
"docs(qa): capture the quality-gate diagnosis notes\n\n"
|
||||
"Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>\n"
|
||||
"🤖 Generated with Claude Code"
|
||||
),
|
||||
)
|
||||
|
||||
msg = git_svc.commit.await_args.kwargs["message"]
|
||||
assert "Co-Authored-By" not in msg
|
||||
assert "anthropic.com" not in msg
|
||||
assert "Generated with" not in msg
|
||||
assert "capture the quality-gate diagnosis notes" in msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_commit_prefix_collapses_multiple_spaces() -> None:
|
||||
"""`[old] foo` should become `[new] foo`, not `[new] foo`."""
|
||||
|
||||
@@ -86,6 +86,18 @@ class TestSharedClaudeCredentialsDenied:
|
||||
assert "Read(//home/agent/.claude/.credentials.json)" in deny, deny
|
||||
assert "Read(//home/agent/.claude.json)" in deny, deny
|
||||
|
||||
def test_settings_set_include_co_authored_by_false(self) -> None:
|
||||
"""Suppresses the CLI's default Claude co-author commit trailer —
|
||||
agent commits carry the agent's identity, not the model vendor's."""
|
||||
orch = _orch()
|
||||
path = orch._generate_agent_settings(
|
||||
agent_id="be-dev-1",
|
||||
role="developer",
|
||||
workspace_path=_WS,
|
||||
cell_workspace_path=_CELL,
|
||||
)
|
||||
assert json.loads(Path(path).read_text())["includeCoAuthoredBy"] is False
|
||||
|
||||
def test_deny_uses_absolute_double_slash_form(self) -> None:
|
||||
"""Per the #167 gotcha: a single leading / resolves against the
|
||||
settings.json project root, not the container filesystem root — an
|
||||
|
||||
Reference in New Issue
Block a user