Touch agent presence on authenticated requests
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Depends, Header, HTTPException, Request, status
|
from fastapi import Depends, Header, HTTPException, Request, status
|
||||||
@@ -9,11 +10,15 @@ from sqlmodel import col, select
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from app.core.agent_tokens import verify_agent_token
|
from app.core.agent_tokens import verify_agent_token
|
||||||
|
from app.core.time import utcnow
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_LAST_SEEN_TOUCH_INTERVAL = timedelta(seconds=30)
|
||||||
|
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AgentAuthContext:
|
class AgentAuthContext:
|
||||||
@@ -49,6 +54,34 @@ def _resolve_agent_token(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _touch_agent_presence(
|
||||||
|
request: Request,
|
||||||
|
session: AsyncSession,
|
||||||
|
agent: Agent,
|
||||||
|
) -> None:
|
||||||
|
"""Best-effort update of last_seen/status for any authenticated agent request.
|
||||||
|
|
||||||
|
Heartbeats are the primary presence mechanism, but agents may still make API
|
||||||
|
calls (task comments, memory updates, etc). Touch presence so the UI reflects
|
||||||
|
real activity even if the heartbeat loop isn't running.
|
||||||
|
"""
|
||||||
|
|
||||||
|
now = utcnow()
|
||||||
|
if agent.last_seen_at is not None and now - agent.last_seen_at < _LAST_SEEN_TOUCH_INTERVAL:
|
||||||
|
return
|
||||||
|
|
||||||
|
agent.last_seen_at = now
|
||||||
|
agent.updated_at = now
|
||||||
|
if agent.status not in {"updating", "deleting"}:
|
||||||
|
agent.status = "online"
|
||||||
|
session.add(agent)
|
||||||
|
|
||||||
|
# For safe HTTP methods, endpoints typically do not commit. Persist the touch
|
||||||
|
# so agents that only poll/read still show as online.
|
||||||
|
if request.method.upper() in _SAFE_METHODS:
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def get_agent_auth_context(
|
async def get_agent_auth_context(
|
||||||
request: Request,
|
request: Request,
|
||||||
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
|
||||||
@@ -72,6 +105,7 @@ async def get_agent_auth_context(
|
|||||||
resolved[:6],
|
resolved[:6],
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
await _touch_agent_presence(request, session, agent)
|
||||||
return AgentAuthContext(actor_type="agent", agent=agent)
|
return AgentAuthContext(actor_type="agent", agent=agent)
|
||||||
|
|
||||||
|
|
||||||
@@ -103,4 +137,5 @@ async def get_agent_auth_context_optional(
|
|||||||
resolved[:6],
|
resolved[:6],
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
await _touch_agent_presence(request, session, agent)
|
||||||
return AgentAuthContext(actor_type="agent", agent=agent)
|
return AgentAuthContext(actor_type="agent", agent=agent)
|
||||||
|
|||||||
Reference in New Issue
Block a user