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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user