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
2 changes: 1 addition & 1 deletion autobot-backend/api/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from auth_middleware import check_admin_permission
from autobot_shared.error_boundaries import ErrorCategory, with_error_handling
from autobot_shared.logging_manager import get_logger
from autobot_shared.webhook_signature import verify_webhook_signature
from services.gateway.gateway_manager import GatewayManager
from services.gateway.ingest_governor import ingest_governor
from services.whatsapp_service import (
Expand All @@ -42,7 +43,6 @@
get_whatsapp_app_secret,
get_whatsapp_verify_token,
save_whatsapp_config,
verify_webhook_signature,
)
from utils.chat_utils import generate_request_id

Expand Down
3 changes: 2 additions & 1 deletion autobot-backend/api/whatsapp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

import pytest

from autobot_shared.webhook_signature import verify_webhook_signature
from initialization.router_registry.integration_routers import INTEGRATION_ROUTER_CONFIGS
from services.whatsapp_service import flatten_messages, verify_webhook_signature
from services.whatsapp_service import flatten_messages


class TestRouterRegistration:
Expand Down
2 changes: 1 addition & 1 deletion autobot-backend/llc/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
async def get_adapter_for_agent(agent_id: str) -> Optional[LLCAdapter]:
"""Return the registered adapter for *agent_id*, or None if unregistered.

Concrete registry is built in GH#8225 (HeartbeatScheduler). Until then,
Concrete registry is built in GH#8225 (LLCHeartbeatScheduler). Until then,
always returns None so LivenessMonitor and the runs API degrade gracefully
to no-ops on cancel().
"""
Expand Down
2 changes: 1 addition & 1 deletion autobot-backend/llc/adapters/process_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _build_minimal_env(env_extra: dict, context: dict) -> dict:
return env


class ProcessAdapter:
class LLCProcessAdapter:
"""Adapter that manages agent runs as OS subprocesses."""

async def invoke(self, agent_config: dict, context: dict) -> str:
Expand Down
43 changes: 23 additions & 20 deletions autobot-backend/llc/adapters/tests/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
# SPDX-License-Identifier: Apache-2.0
# AutoBot - AI-Powered Automation Platform
# Author: mrveiss
"""Tests for LLC adapter protocol, ProcessAdapter, HttpAdapter, and registry (GH#8226)."""
"""Tests for LLC adapter protocol, LLCProcessAdapter, HttpAdapter, and registry (GH#8226)."""

import signal
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from llc.adapters import AdapterRunStatus, get_adapter, register_adapter
from llc.adapters.base import LLCAdapter
from llc.adapters.base import LLCAdapter, _registry
from llc.adapters.http_adapter import HttpAdapter
from llc.adapters.process_adapter import ProcessAdapter
from llc.adapters.process_adapter import LLCProcessAdapter
from llc.models.enums import LLCRunStatus

_PSUTIL_PROCESS = "llc.adapters.subprocess_support.psutil.Process"
Expand All @@ -24,7 +24,7 @@

class TestLLCAdapterProtocol:
def test_process_adapter_satisfies_protocol(self) -> None:
assert isinstance(ProcessAdapter(), LLCAdapter)
assert isinstance(LLCProcessAdapter(), LLCAdapter)

def test_http_adapter_satisfies_protocol(self) -> None:
assert isinstance(HttpAdapter(), LLCAdapter)
Expand Down Expand Up @@ -55,15 +55,15 @@ def test_all_status_values(self) -> None:


# ---------------------------------------------------------------------------
# ProcessAdapter
# LLCProcessAdapter
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
class TestProcessAdapter:
async def test_invoke_disabled_by_default_raises(self) -> None:
"""GH#11059: ProcessAdapter is fail-closed — invoke raises unless an operator enables it."""
adapter = ProcessAdapter()
"""GH#11059: LLCProcessAdapter is fail-closed — invoke raises unless an operator enables it."""
adapter = LLCProcessAdapter()
with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as spawn:
with pytest.raises(PermissionError):
await adapter.invoke({"command": "echo hello"}, {})
Expand All @@ -72,7 +72,7 @@ async def test_invoke_disabled_by_default_raises(self) -> None:
async def test_invoke_returns_encoded_run_id(self) -> None:
"""PR#16284 review: run_id packs pid + psutil create_time for later
PID-reuse-safe identity verification."""
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
fake_proc = MagicMock()
fake_proc.pid = 12345

Expand All @@ -88,7 +88,7 @@ async def test_invoke_returns_encoded_run_id(self) -> None:

async def test_invoke_uses_exec_with_split_argv_not_shell(self) -> None:
"""GH#11059: command runs via exec on shlex-split argv — no shell metacharacter RCE."""
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
fake_proc = MagicMock()
fake_proc.pid = 7

Expand All @@ -113,7 +113,7 @@ async def mock_exec(*argv, **kwargs):
assert captured_kwargs["start_new_session"] is True

async def test_invoke_passes_context_as_env(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
fake_proc = MagicMock()
fake_proc.pid = 99

Expand All @@ -138,7 +138,7 @@ async def test_invoke_minimal_env_excludes_backend_secrets(self, monkeypatch) ->
"""GH#11059: the subprocess must NOT inherit backend secrets from os.environ."""
monkeypatch.setenv("DATABASE_URL", "postgres://secret")
monkeypatch.setenv("PATH", "/usr/bin")
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
fake_proc = MagicMock()
fake_proc.pid = 1

Expand All @@ -158,13 +158,13 @@ async def mock_exec(*argv, **kwargs):
assert captured_env.get("PATH") == "/usr/bin" # safe var passes through

async def test_status_running_when_pid_alive(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
with patch("os.kill", return_value=None): # signal 0 succeeds → process alive
result = await adapter.status({}, "12345")
assert result.status is LLCRunStatus.RUNNING

async def test_status_completed_when_pid_gone(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()

def _raise(*_):
raise ProcessLookupError
Expand All @@ -174,20 +174,20 @@ def _raise(*_):
assert result.status is LLCRunStatus.COMPLETED

async def test_status_failed_on_oserror(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
with patch("os.kill", side_effect=OSError("bad")):
result = await adapter.status({}, "12345")
assert result.status is LLCRunStatus.FAILED
assert "bad" in (result.error or "")

async def test_status_failed_on_invalid_pid(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
result = await adapter.status({}, "abc")
assert result.status is LLCRunStatus.FAILED
assert result.error is not None

async def test_cancel_sends_sigterm(self) -> None:
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
killed = []

sent_sigterm = [False]
Expand Down Expand Up @@ -218,7 +218,7 @@ def smart_kill(pid, sig):
async def test_cancel_without_recorded_identity_never_signals(self) -> None:
"""PR#16284 review: a bare-pid run_id (no create_time) means cancel()
can only probe -- it never signals, and never raises."""
adapter = ProcessAdapter()
adapter = LLCProcessAdapter()
with patch("os.kill") as mock_kill:
await adapter.cancel({}, "99999")
mock_kill.assert_not_called()
Expand Down Expand Up @@ -365,6 +365,9 @@ async def status(self, agent_config: dict, run_id: str) -> AdapterRunStatus:
async def cancel(self, agent_config: dict, run_id: str) -> None:
pass

register_adapter("fake_test", FakeAdapter())
retrieved = get_adapter("fake_test")
assert isinstance(retrieved, FakeAdapter)
# patch.dict restores the registry so "fake_test" cannot leak into
# tests that enumerate registered adapters.
with patch.dict(_registry):
register_adapter("fake_test", FakeAdapter())
retrieved = get_adapter("fake_test")
assert isinstance(retrieved, FakeAdapter)
8 changes: 4 additions & 4 deletions autobot-backend/llc/api/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
router = APIRouter(prefix="/agents", tags=["llc-agents"])


class TriggerResponse(BaseModel):
class AgentHeartbeatTriggerResponse(BaseModel):
run_id: uuid.UUID
status: str

Expand Down Expand Up @@ -88,13 +88,13 @@ async def list_agents(
]


@router.post("/{agent_id}/heartbeat/trigger", response_model=TriggerResponse, status_code=202)
@router.post("/{agent_id}/heartbeat/trigger", response_model=AgentHeartbeatTriggerResponse, status_code=202)
async def trigger_heartbeat(
agent_id: str,
session: AsyncSession = Depends(get_async_session),
_current_user: dict = Depends(get_current_user),
ctx: TenantContext = Depends(require_org_context),
) -> TriggerResponse:
) -> AgentHeartbeatTriggerResponse:
"""Manually trigger a heartbeat run for *agent_id* immediately.

Requires an authenticated user with org context. The agent must belong to
Expand All @@ -115,4 +115,4 @@ async def trigger_heartbeat(

await session.commit()
sched.dispatch_run(agent_cfg, run.id)
return TriggerResponse(run_id=run.id, status=run.status)
return AgentHeartbeatTriggerResponse(run_id=run.id, status=run.status)
2 changes: 1 addition & 1 deletion autobot-backend/llc/api/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
from llc.deps import get_session, require_board_role

from ..models.enums import MembershipRole
from ..services.company import CompanyNotFoundError
from ..services.controls_service import (
AgentNotFoundError,
CompanyNotFoundError,
ControlsService,
SprintNotFoundError,
)
Expand Down
15 changes: 1 addition & 14 deletions autobot-backend/llc/api/github_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
- Validates GitHub PR URLs with strict parsing and repo matching
"""

import hashlib
import hmac
import json
import os
import re
Expand All @@ -25,6 +23,7 @@
from sqlalchemy.ext.asyncio import AsyncSession

from autobot_shared.logging_manager import get_logger
from autobot_shared.webhook_signature import verify_webhook_signature
from llc.deps import get_session

from ..models.enums import WorkItemStatus
Expand Down Expand Up @@ -110,18 +109,6 @@ def validate_github_pr_url(url: str, expected_repo: Optional[str] = None) -> boo
return True


def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
"""Verify a GitHub webhook HMAC-SHA256 signature.

Prevents spoofed webhook payloads from untrusted sources.
"""
if not signature or not signature.startswith("sha256="):
return False

expected_sig = "sha256=" + hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected_sig, signature)


def _system_comment(item: LLCWorkItem, body: str) -> LLCWorkItemComment:
return LLCWorkItemComment(
company_id=item.company_id,
Expand Down
8 changes: 4 additions & 4 deletions autobot-backend/llc/api/routines.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class RoutineRunRead(BaseModel):
created_at: datetime


class TriggerResponse(BaseModel):
class RoutineTriggerResponse(BaseModel):
routine_id: uuid.UUID
message: str = "Routine scheduled for immediate execution"

Expand Down Expand Up @@ -280,15 +280,15 @@ async def list_routine_runs(

@router.post(
"/routines/{routine_id}/trigger",
response_model=TriggerResponse,
response_model=RoutineTriggerResponse,
status_code=status.HTTP_202_ACCEPTED,
)
async def trigger_routine(
routine_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
_current_user: dict = Depends(get_current_user),
ctx: TenantContext = Depends(require_org_context),
) -> TriggerResponse:
) -> RoutineTriggerResponse:
routine = await _load_owned_routine(session, routine_id, ctx)
if routine.status != RoutineStatus.ACTIVE:
raise HTTPException(status_code=409, detail="Routine is not active")
Expand All @@ -300,4 +300,4 @@ async def trigger_routine(
await redis.zadd(_SCHEDULE_KEY, {f"routine:{routine_id}": time.time()}, nx=False)
except Exception:
pass
return TriggerResponse(routine_id=routine_id)
return RoutineTriggerResponse(routine_id=routine_id)
4 changes: 2 additions & 2 deletions autobot-backend/llc/api/work_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
WorkItemType,
)
from ..models.work_item import LLCWorkItem, LLCWorkItemComment
from ..scheduler.heartbeat_scheduler import HeartbeatScheduler
from ..scheduler.heartbeat_scheduler import LLCHeartbeatScheduler
from ..services.activity_log import ActivityLogQuery, LLCActivityLogService
from ..services.attachment_service import (
AttachmentNotFound,
Expand Down Expand Up @@ -120,7 +120,7 @@ class CoworkerRequest(BaseModel):
_get_relation_service = lazy_singleton(WorkItemRelationService)
_get_attachment_service = lazy_singleton(AttachmentService)
_get_comment_wake_service = lazy_singleton(CommentWakeService)
_get_heartbeat_scheduler = lazy_singleton(HeartbeatScheduler)
_get_heartbeat_scheduler = lazy_singleton(LLCHeartbeatScheduler)
_get_activity_service = lazy_singleton(LLCActivityLogService)
_get_ac_suggester = lazy_singleton(AcSuggester)

Expand Down
4 changes: 2 additions & 2 deletions autobot-backend/llc/health/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from ..models.budget import LLCAgentBudget
from ..models.enums import ApprovalStatus

# HeartbeatScheduler import is lazy (inside _collect_metrics) to avoid circular-import
# LLCHeartbeatScheduler import is lazy (inside _collect_metrics) to avoid circular-import
# chains when the probe module is loaded in test environments.

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -147,7 +147,7 @@ def _compute_status(metrics: dict) -> str:


def _is_scheduler_running(scheduler: object) -> bool:
"""Check whether the HeartbeatScheduler background task is alive."""
"""Check whether the LLCHeartbeatScheduler background task is alive."""
return bool(getattr(scheduler, "is_running", False))


Expand Down
2 changes: 1 addition & 1 deletion autobot-backend/llc/models/heartbeat_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Author: mrveiss
"""LLC heartbeat run model (GH#8225/GH#8228).

Defines the schema shared by HeartbeatScheduler (GH#8225) and LivenessMonitor
Defines the schema shared by LLCHeartbeatScheduler (GH#8225) and LivenessMonitor
(GH#8228). Both issues read and write this table.
"""

Expand Down
Loading
Loading