fix: send_animation metadata, MarkdownV2 inline code splitting, tirith cosign-free install (#1626)

* fix: Anthropic OAuth compatibility — Claude Code identity fingerprinting

Anthropic routes OAuth/subscription requests based on Claude Code's
identity markers. Without them, requests get intermittent 500 errors
(~25% failure rate observed). This matches what pi-ai (clawdbot) and
OpenCode both implement for OAuth compatibility.

Changes (OAuth tokens only — API key users unaffected):

1. Headers: user-agent 'claude-cli/2.1.2 (external, cli)' + x-app 'cli'
2. System prompt: prepend 'You are Claude Code, Anthropic's official CLI'
3. System prompt sanitization: replace Hermes/Nous references
4. Tool names: prefix with 'mcp_' (Claude Code convention for non-native tools)
5. Tool name stripping: remove 'mcp_' prefix from response tool calls

Before: 9/12 OK, 1 hard fail, 4 needed retries (~25% error rate)
After: 16/16 OK, 0 failures, 0 retries (0% error rate)

* fix: three gateway issues from user error logs

1. send_animation missing metadata kwarg (base.py)
   - Base class send_animation lacked the metadata parameter that the
     call site in base.py line 917 passes. Telegram's override accepted
     it, but any platform without an override (Discord, Slack, etc.)
     hit TypeError. Added metadata to base class signature.

2. MarkdownV2 split-inside-inline-code (base.py truncate_message)
   - truncate_message could split at a space inside an inline code span
     (e.g. `function(arg1, arg2)`), leaving an unpaired backtick and
     unescaped parentheses in the chunk. Telegram rejects with
     'character ( is reserved'. Added inline code awareness to the
     split-point finder — detects odd backtick counts and moves the
     split before the code span.

3. tirith auto-install without cosign (tirith_security.py)
   - Previously required cosign on PATH for auto-install, blocking
     install entirely with a warning if missing. Now proceeds with
     SHA-256 checksum verification only when cosign is unavailable.
     Cosign is still used for full supply chain verification when
     present. If cosign IS present but verification explicitly fails,
     install is still aborted (tampered release).
This commit is contained in:
Teknium
2026-03-16 23:39:41 -07:00
committed by GitHub
parent 19c8ad3d3d
commit e3f9894caf
3 changed files with 100 additions and 59 deletions

View File

@@ -528,6 +528,7 @@ class BasePlatformAdapter(ABC):
animation_url: str,
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> SendResult:
"""
Send an animated GIF natively via the platform API.
@@ -1129,6 +1130,27 @@ class BasePlatformAdapter(ABC):
if split_at < 1:
split_at = headroom
# Avoid splitting inside an inline code span (`...`).
# If the text before split_at has an odd number of unescaped
# backticks, the split falls inside inline code — the resulting
# chunk would have an unpaired backtick and any special characters
# (like parentheses) inside the broken span would be unescaped,
# causing MarkdownV2 parse errors on Telegram.
candidate = remaining[:split_at]
backtick_count = candidate.count("`") - candidate.count("\\`")
if backtick_count % 2 == 1:
# Find the last unescaped backtick and split before it
last_bt = candidate.rfind("`")
while last_bt > 0 and candidate[last_bt - 1] == "\\":
last_bt = candidate.rfind("`", 0, last_bt)
if last_bt > 0:
# Try to find a space or newline just before the backtick
safe_split = candidate.rfind(" ", 0, last_bt)
nl_split = candidate.rfind("\n", 0, last_bt)
safe_split = max(safe_split, nl_split)
if safe_split > headroom // 4:
split_at = safe_split
chunk_body = remaining[:split_at]
remaining = remaining[split_at:].lstrip()

View File

@@ -522,50 +522,59 @@ class TestCosignVerification:
assert path is None
assert reason == "cosign_verification_failed"
@patch("tools.tirith_security.tarfile.open")
@patch("tools.tirith_security._verify_checksum", return_value=True)
@patch("tools.tirith_security.shutil.which", return_value=None)
@patch("tools.tirith_security._download_file")
@patch("tools.tirith_security._detect_target", return_value="aarch64-apple-darwin")
def test_install_aborts_when_cosign_missing(self, mock_target, mock_dl,
mock_which):
"""_install_tirith returns cosign_missing when cosign is not on PATH."""
def test_install_proceeds_without_cosign(self, mock_target, mock_dl,
mock_which, mock_checksum,
mock_tarfile):
"""_install_tirith proceeds with SHA-256 only when cosign is not on PATH."""
from tools.tirith_security import _install_tirith
mock_tar = MagicMock()
mock_tar.__enter__ = MagicMock(return_value=mock_tar)
mock_tar.__exit__ = MagicMock(return_value=False)
mock_tar.getmembers.return_value = []
mock_tarfile.return_value = mock_tar
path, reason = _install_tirith()
# Reaches extraction (no binary in mock archive), but got past cosign
assert path is None
assert reason == "cosign_missing"
@patch("tools.tirith_security.logger.debug")
@patch("tools.tirith_security.logger.warning")
@patch("tools.tirith_security.shutil.which", return_value=None)
@patch("tools.tirith_security._download_file")
@patch("tools.tirith_security._detect_target", return_value="aarch64-apple-darwin")
def test_install_quiet_mode_downgrades_cosign_missing_log(self, mock_target, mock_dl,
mock_which, mock_warning,
mock_debug):
"""Startup prefetch should not surface cosign-missing as a warning."""
from tools.tirith_security import _install_tirith
path, reason = _install_tirith(log_failures=False)
assert path is None
assert reason == "cosign_missing"
mock_warning.assert_not_called()
mock_debug.assert_called()
assert reason == "binary_not_in_archive"
assert mock_checksum.called # SHA-256 verification ran
@patch("tools.tirith_security.tarfile.open")
@patch("tools.tirith_security._verify_checksum", return_value=True)
@patch("tools.tirith_security._verify_cosign", return_value=None)
@patch("tools.tirith_security.shutil.which", return_value="/usr/local/bin/cosign")
@patch("tools.tirith_security._download_file")
@patch("tools.tirith_security._detect_target", return_value="aarch64-apple-darwin")
def test_install_aborts_when_cosign_exec_fails(self, mock_target, mock_dl,
mock_which, mock_cosign):
"""_install_tirith returns cosign_exec_failed when cosign exists but fails."""
def test_install_proceeds_when_cosign_exec_fails(self, mock_target, mock_dl,
mock_which, mock_cosign,
mock_checksum, mock_tarfile):
"""_install_tirith falls back to SHA-256 when cosign exists but fails to execute."""
from tools.tirith_security import _install_tirith
mock_tar = MagicMock()
mock_tar.__enter__ = MagicMock(return_value=mock_tar)
mock_tar.__exit__ = MagicMock(return_value=False)
mock_tar.getmembers.return_value = []
mock_tarfile.return_value = mock_tar
path, reason = _install_tirith()
assert path is None
assert reason == "cosign_exec_failed"
assert reason == "binary_not_in_archive" # got past cosign
assert mock_checksum.called
@patch("tools.tirith_security.tarfile.open")
@patch("tools.tirith_security._verify_checksum", return_value=True)
@patch("tools.tirith_security.shutil.which", return_value="/usr/local/bin/cosign")
@patch("tools.tirith_security._download_file")
@patch("tools.tirith_security._detect_target", return_value="aarch64-apple-darwin")
def test_install_aborts_when_cosign_artifacts_missing(self, mock_target,
mock_dl):
"""_install_tirith returns None when .sig/.pem downloads fail (404)."""
def test_install_proceeds_when_cosign_artifacts_missing(self, mock_target,
mock_dl, mock_which,
mock_checksum, mock_tarfile):
"""_install_tirith proceeds with SHA-256 when .sig/.pem downloads fail."""
from tools.tirith_security import _install_tirith
import urllib.request
@@ -574,10 +583,16 @@ class TestCosignVerification:
raise urllib.request.URLError("404 Not Found")
mock_dl.side_effect = _dl_side_effect
mock_tar = MagicMock()
mock_tar.__enter__ = MagicMock(return_value=mock_tar)
mock_tar.__exit__ = MagicMock(return_value=False)
mock_tar.getmembers.return_value = []
mock_tarfile.return_value = mock_tar
path, reason = _install_tirith()
assert path is None
assert reason == "cosign_artifacts_unavailable"
assert reason == "binary_not_in_archive" # got past cosign
assert mock_checksum.called
@patch("tools.tirith_security.tarfile.open")
@patch("tools.tirith_security._verify_checksum", return_value=True)

View File

@@ -12,9 +12,12 @@ the fail_open config setting. Programming errors propagate.
Auto-install: if tirith is not found on PATH or at the configured path,
it is automatically downloaded from GitHub releases to $HERMES_HOME/bin/tirith.
The download verifies SHA-256 checksums and cosign provenance (when cosign
is available). Installation runs in a background thread so startup never
blocks.
The download always verifies SHA-256 checksums. When cosign is available on
PATH, provenance verification (GitHub Actions workflow signature) is also
performed. If cosign is not installed, the download proceeds with SHA-256
verification only — still secure via HTTPS + checksum, just without supply
chain provenance proof. Installation runs in a background thread so startup
never blocks.
"""
import hashlib
@@ -314,34 +317,34 @@ def _install_tirith(*, log_failures: bool = True) -> tuple[str | None, str]:
log("tirith download failed: %s", exc)
return None, "download_failed"
# Cosign provenance verification is mandatory for auto-install.
# SHA-256 alone only proves self-consistency (both files come from the
# same endpoint), not provenance. Without cosign we cannot verify the
# release was produced by the expected GitHub Actions workflow.
try:
_download_file(f"{base_url}/checksums.txt.sig", sig_path)
_download_file(f"{base_url}/checksums.txt.pem", cert_path)
except Exception as exc:
log("tirith install skipped: cosign artifacts unavailable (%s). "
"Install tirith manually or install cosign for auto-install.", exc)
return None, "cosign_artifacts_unavailable"
# Check cosign availability before attempting verification so we can
# distinguish "not installed" (retryable) from "installed but broken."
if not shutil.which("cosign"):
log("tirith install skipped: cosign not found on PATH. "
"Install cosign for auto-install, or install tirith manually.")
return None, "cosign_missing"
cosign_result = _verify_cosign(checksums_path, sig_path, cert_path)
if cosign_result is not True:
# False = verification rejected, None = execution failure (timeout/OSError)
if cosign_result is None:
log("tirith install aborted: cosign execution failed")
return None, "cosign_exec_failed"
# Cosign provenance verification — preferred but not mandatory.
# When cosign is available, we verify that the release was produced
# by the expected GitHub Actions workflow (full supply chain proof).
# Without cosign, SHA-256 checksum + HTTPS still provides integrity
# and transport-level authenticity.
cosign_verified = False
if shutil.which("cosign"):
try:
_download_file(f"{base_url}/checksums.txt.sig", sig_path)
_download_file(f"{base_url}/checksums.txt.pem", cert_path)
except Exception as exc:
logger.info("cosign artifacts unavailable (%s), proceeding with SHA-256 only", exc)
else:
log("tirith install aborted: cosign provenance verification failed")
return None, "cosign_verification_failed"
cosign_result = _verify_cosign(checksums_path, sig_path, cert_path)
if cosign_result is True:
cosign_verified = True
elif cosign_result is False:
# Verification explicitly rejected — abort, the release
# may have been tampered with.
log("tirith install aborted: cosign provenance verification failed")
return None, "cosign_verification_failed"
else:
# None = execution failure (timeout/OSError) — proceed
# with SHA-256 only since cosign itself is broken.
logger.info("cosign execution failed, proceeding with SHA-256 only")
else:
logger.info("cosign not on PATH — installing tirith with SHA-256 verification only "
"(install cosign for full supply chain verification)")
if not _verify_checksum(archive_path, checksums_path, archive_name):
return None, "checksum_failed"
@@ -364,7 +367,8 @@ def _install_tirith(*, log_failures: bool = True) -> tuple[str | None, str]:
shutil.move(src, dest)
os.chmod(dest, os.stat(dest).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
logger.info("tirith installed to %s", dest)
verification = "cosign + SHA-256" if cosign_verified else "SHA-256 only"
logger.info("tirith installed to %s (%s)", dest, verification)
return dest, ""
finally: