Allow Leo paid research gate by chat title (#32)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
This commit is contained in:
parent
c1eb4f5718
commit
fd6132e6ce
5 changed files with 139 additions and 6 deletions
|
|
@ -17,6 +17,7 @@ from pathlib import Path
|
|||
|
||||
APPROVAL_REF_RE = re.compile(r"^[A-Za-z0-9._:@/-]{8,256}$")
|
||||
CHAT_ID_RE = re.compile(r"^-?\d+$")
|
||||
CHAT_TITLE_RE = re.compile(r"^[^\r\n=]{1,128}$")
|
||||
MAX_SMART_RESEARCH_USD = 0.06
|
||||
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||
parser.add_argument("--secrets-dir", default="/opt/teleo-eval/secrets")
|
||||
parser.add_argument("--allow-paid", action="store_true", help="Enable paid smart research for one allowed chat")
|
||||
parser.add_argument("--allowed-chat-id", help="Telegram chat id allowed to trigger paid smart research")
|
||||
parser.add_argument("--allowed-chat-title", help="Telegram chat title allowed to trigger paid smart research")
|
||||
parser.add_argument("--max-usd", default="0.01", help="Maximum spend per Telegram smart-research call")
|
||||
parser.add_argument(
|
||||
"--approval-ref-from-stdin",
|
||||
|
|
@ -75,8 +77,12 @@ def validate_max_usd(value: str) -> str:
|
|||
def validate_paid_inputs(args: argparse.Namespace) -> str | None:
|
||||
if not args.allow_paid:
|
||||
return None
|
||||
if not args.allowed_chat_id or not CHAT_ID_RE.match(args.allowed_chat_id):
|
||||
raise ValueError("--allowed-chat-id is required for --allow-paid and must be an integer")
|
||||
if args.allowed_chat_id and not CHAT_ID_RE.match(args.allowed_chat_id):
|
||||
raise ValueError("--allowed-chat-id must be an integer")
|
||||
if args.allowed_chat_title and not CHAT_TITLE_RE.match(args.allowed_chat_title.strip()):
|
||||
raise ValueError("--allowed-chat-title shape is invalid")
|
||||
if not args.allowed_chat_id and not args.allowed_chat_title:
|
||||
raise ValueError("--allowed-chat-id or --allowed-chat-title is required for --allow-paid")
|
||||
approval_ref = read_approval_ref(from_stdin=args.approval_ref_from_stdin)
|
||||
if not APPROVAL_REF_RE.match(approval_ref):
|
||||
raise ValueError("approval ref shape is invalid")
|
||||
|
|
@ -125,7 +131,14 @@ def run_command(command: list[str], *, dry_run: bool) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def build_env_content(*, allow_paid: bool, allowed_chat_id: str | None, max_usd: str, approval_ref_path: Path) -> str:
|
||||
def build_env_content(
|
||||
*,
|
||||
allow_paid: bool,
|
||||
allowed_chat_id: str | None,
|
||||
allowed_chat_title: str | None,
|
||||
max_usd: str,
|
||||
approval_ref_path: Path,
|
||||
) -> str:
|
||||
lines = [
|
||||
f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOW_PAID={'1' if allow_paid else '0'}",
|
||||
f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_MAX_USD={max_usd}",
|
||||
|
|
@ -133,6 +146,8 @@ def build_env_content(*, allow_paid: bool, allowed_chat_id: str | None, max_usd:
|
|||
]
|
||||
if allow_paid and allowed_chat_id:
|
||||
lines.insert(1, f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_ID={allowed_chat_id}")
|
||||
if allow_paid and allowed_chat_title:
|
||||
lines.insert(2, f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_TITLE={allowed_chat_title.strip()}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
|
|
@ -149,6 +164,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
env_content = build_env_content(
|
||||
allow_paid=args.allow_paid,
|
||||
allowed_chat_id=args.allowed_chat_id,
|
||||
allowed_chat_title=args.allowed_chat_title,
|
||||
max_usd=max_usd,
|
||||
approval_ref_path=approval_ref_path,
|
||||
)
|
||||
|
|
@ -175,6 +191,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"approvalRefWritten": approval_ref_written,
|
||||
"approvalRefPresent": bool(args.allow_paid),
|
||||
"allowedChatIdPresent": bool(args.allowed_chat_id),
|
||||
"allowedChatTitlePresent": bool(args.allowed_chat_title),
|
||||
"maxUsd": max_usd,
|
||||
"paidEnabled": bool(args.allow_paid),
|
||||
"dryRun": args.dry_run,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ from http_chat_proxy import (
|
|||
extract_smart_research_goal,
|
||||
post_chat_proxy,
|
||||
should_attach_structured_market_context,
|
||||
smart_research_chat_matches_gate,
|
||||
smart_research_payment_fields_for_message,
|
||||
smart_research_command_names,
|
||||
split_smart_research_auto_resume_reply,
|
||||
|
|
@ -740,14 +741,20 @@ def sanitize_message(text: str) -> str:
|
|||
return text[:2000]
|
||||
|
||||
|
||||
def _smart_research_payment_gate(chat_id: int) -> dict:
|
||||
def _smart_research_payment_gate(chat_id: int, chat_title: str | None = None) -> dict:
|
||||
"""Return paid smart-research fields only when all server-side gates pass."""
|
||||
max_allowed_usd = 0.06
|
||||
if os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOW_PAID") != "1":
|
||||
return {"allow_paid_execution": False}
|
||||
|
||||
allowed_chat_id = os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_ID", "").strip()
|
||||
if not allowed_chat_id or allowed_chat_id != str(chat_id):
|
||||
allowed_chat_title = os.getenv("LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_TITLE", "").strip()
|
||||
if not smart_research_chat_matches_gate(
|
||||
chat_id=chat_id,
|
||||
chat_title=chat_title,
|
||||
allowed_chat_id=allowed_chat_id,
|
||||
allowed_chat_title=allowed_chat_title,
|
||||
):
|
||||
return {"allow_paid_execution": False}
|
||||
|
||||
try:
|
||||
|
|
@ -1391,7 +1398,10 @@ async def handle_tagged(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
if AGENT_HTTP_RESEARCH_PROXY_URL and smart_research_goal:
|
||||
payment_gate = smart_research_payment_fields_for_message(
|
||||
paid_work_order_id=paid_work_order_id,
|
||||
configured_payment_gate=_smart_research_payment_gate(msg.chat_id),
|
||||
configured_payment_gate=_smart_research_payment_gate(
|
||||
msg.chat_id,
|
||||
msg.chat.title if getattr(msg, "chat", None) else None,
|
||||
),
|
||||
)
|
||||
proxy_research_goal = smart_research_goal
|
||||
if should_attach_structured_market_context(smart_research_goal):
|
||||
|
|
|
|||
|
|
@ -77,6 +77,36 @@ _SOCIAL_DISCUSSION_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
def _normalize_chat_title(value: str | None) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "")).strip().casefold()
|
||||
|
||||
|
||||
def smart_research_chat_matches_gate(
|
||||
*,
|
||||
chat_id: int,
|
||||
chat_title: str | None = None,
|
||||
allowed_chat_id: str | None = None,
|
||||
allowed_chat_title: str | None = None,
|
||||
) -> bool:
|
||||
"""Return whether a Telegram chat matches the configured paid-research gate."""
|
||||
if allowed_chat_id and allowed_chat_id.strip() == str(chat_id):
|
||||
return True
|
||||
|
||||
if not allowed_chat_title:
|
||||
return False
|
||||
|
||||
normalized_title = _normalize_chat_title(chat_title)
|
||||
if not normalized_title:
|
||||
return False
|
||||
|
||||
allowed_titles = {
|
||||
_normalize_chat_title(part)
|
||||
for part in re.split(r"[,;\n]", allowed_chat_title)
|
||||
if _normalize_chat_title(part)
|
||||
}
|
||||
return normalized_title in allowed_titles
|
||||
|
||||
|
||||
def smart_research_command_names(
|
||||
command_prefixes: tuple[str, ...] | list[str] = DEFAULT_SMART_RESEARCH_COMMAND_PREFIXES,
|
||||
) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ def test_installs_paid_gate_from_stdin_without_echoing_secret_or_chat_id(tmp_pat
|
|||
assert proof["paidEnabled"] is True
|
||||
assert proof["approvalRefPresent"] is True
|
||||
assert proof["allowedChatIdPresent"] is True
|
||||
assert proof["allowedChatTitlePresent"] is False
|
||||
assert proof["maxUsd"] == "0.06"
|
||||
assert proof["secretValuesIncluded"] is False
|
||||
|
||||
|
|
@ -75,6 +76,53 @@ def test_installs_paid_gate_from_stdin_without_echoing_secret_or_chat_id(tmp_pat
|
|||
assert stat.S_IMODE(os.stat(approval_path).st_mode) == 0o600
|
||||
|
||||
|
||||
def test_installs_paid_gate_with_title_fallback_without_echoing_secret(tmp_path):
|
||||
approval_ref = "approval_ref_livingip_x402_20260622"
|
||||
chat_title = "Leo"
|
||||
proof_path = tmp_path / "proof.json"
|
||||
proc = run_installer(
|
||||
[
|
||||
"--agent",
|
||||
"leo",
|
||||
"--secrets-dir",
|
||||
str(tmp_path / "secrets"),
|
||||
"--allow-paid",
|
||||
"--allowed-chat-title",
|
||||
chat_title,
|
||||
"--max-usd",
|
||||
"0.01",
|
||||
"--approval-ref-from-stdin",
|
||||
"--no-chown",
|
||||
"--output",
|
||||
str(proof_path),
|
||||
],
|
||||
approval_ref=approval_ref,
|
||||
)
|
||||
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
combined_output = proc.stdout + proc.stderr + proof_path.read_text()
|
||||
assert approval_ref not in combined_output
|
||||
|
||||
proof = json.loads(proof_path.read_text())
|
||||
env_path = Path(proof["envPath"])
|
||||
approval_path = Path(proof["approvalRefPath"])
|
||||
assert proof["ok"] is True
|
||||
assert proof["agent"] == "leo"
|
||||
assert proof["paidEnabled"] is True
|
||||
assert proof["approvalRefPresent"] is True
|
||||
assert proof["allowedChatIdPresent"] is False
|
||||
assert proof["allowedChatTitlePresent"] is True
|
||||
assert proof["maxUsd"] == "0.01"
|
||||
assert proof["secretValuesIncluded"] is False
|
||||
|
||||
env_content = env_path.read_text()
|
||||
assert "LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOW_PAID=1" in env_content
|
||||
assert "LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_TITLE=Leo" in env_content
|
||||
assert "LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_ALLOWED_CHAT_ID=" not in env_content
|
||||
assert f"LIVINGIP_LEO_TELEGRAM_SMART_RESEARCH_APPROVAL_REF_FILE={approval_path}" in env_content
|
||||
assert approval_path.read_text().strip() == approval_ref
|
||||
|
||||
|
||||
def test_installs_disabled_gate_without_approval_ref(tmp_path):
|
||||
proof_path = tmp_path / "proof.json"
|
||||
proc = run_installer(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from http_chat_proxy import ( # noqa: E402
|
|||
extract_chat_proxy_reply,
|
||||
extract_smart_research_goal,
|
||||
should_attach_structured_market_context,
|
||||
smart_research_chat_matches_gate,
|
||||
split_smart_research_auto_resume_reply,
|
||||
smart_research_payment_fields_for_message,
|
||||
smart_research_command_names,
|
||||
|
|
@ -192,6 +193,33 @@ def test_smart_research_payment_fields_resume_uses_capped_gate():
|
|||
assert payment_fields == configured_gate
|
||||
|
||||
|
||||
def test_smart_research_chat_gate_allows_exact_chat_id():
|
||||
assert smart_research_chat_matches_gate(
|
||||
chat_id=-1001234567890,
|
||||
chat_title="Different title",
|
||||
allowed_chat_id="-1001234567890",
|
||||
allowed_chat_title=None,
|
||||
)
|
||||
|
||||
|
||||
def test_smart_research_chat_gate_allows_explicit_title_fallback():
|
||||
assert smart_research_chat_matches_gate(
|
||||
chat_id=-1001111111111,
|
||||
chat_title=" Leo ",
|
||||
allowed_chat_id="8331406290",
|
||||
allowed_chat_title="Leo",
|
||||
)
|
||||
|
||||
|
||||
def test_smart_research_chat_gate_rejects_unmatched_chat():
|
||||
assert not smart_research_chat_matches_gate(
|
||||
chat_id=-1001111111111,
|
||||
chat_title="Other group",
|
||||
allowed_chat_id="8331406290",
|
||||
allowed_chat_title="Leo",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("http_status", "payload", "expected"),
|
||||
[
|
||||
|
|
|
|||
Loading…
Reference in a new issue