mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 06:46:02 +00:00
264 lines
8.7 KiB
Python
264 lines
8.7 KiB
Python
"""Agent run session registry for proxy action permission validation."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import copy
|
|
import typing
|
|
import time
|
|
import threading
|
|
|
|
from .context_builder import AgentResources
|
|
|
|
|
|
class AgentRunSessionStatus(typing.TypedDict):
|
|
"""Status tracking for agent run session."""
|
|
started_at: int
|
|
last_activity_at: int
|
|
|
|
|
|
class RunAuthorizationSnapshot(typing.TypedDict):
|
|
"""Frozen authorization data for one active run.
|
|
|
|
ResourceBuilder creates the authorized resource list once before runner
|
|
execution. Runtime proxy handlers must validate against this run-scoped
|
|
snapshot instead of recomputing resource policy.
|
|
"""
|
|
|
|
resources: AgentResources
|
|
permissions: dict[str, list[str]]
|
|
conversation_id: str | None
|
|
state_policy: dict[str, typing.Any]
|
|
state_context: dict[str, typing.Any]
|
|
authorized_ids: dict[str, set[str]]
|
|
|
|
|
|
class AgentRunSession(typing.TypedDict):
|
|
"""Session for an active agent runner execution.
|
|
|
|
Stored in AgentRunSessionRegistry for proxy action permission validation.
|
|
|
|
Fields:
|
|
run_id: Unique run identifier (UUID from AgentRunContext)
|
|
runner_id: Runner descriptor ID (plugin:author/name/runner)
|
|
query_id: Host entry query ID, only present for query-based adapters
|
|
plugin_identity: Plugin identifier (author/name) of the runner
|
|
authorization: Run-scoped authorization snapshot; runtime auth truth
|
|
status: Session status tracking
|
|
"""
|
|
run_id: str
|
|
runner_id: str
|
|
query_id: int | None
|
|
plugin_identity: str # author/name
|
|
authorization: RunAuthorizationSnapshot
|
|
status: AgentRunSessionStatus
|
|
|
|
|
|
class AgentRunSessionRegistry:
|
|
"""Registry for active agent run sessions.
|
|
|
|
Host-owned registry for tracking active AgentRunner executions.
|
|
Used by proxy actions in handler.py to validate resource access.
|
|
|
|
Key: run_id (UUID from AgentRunContext)
|
|
Value: AgentRunSession with authorized resources
|
|
|
|
Thread-safe via asyncio.Lock.
|
|
"""
|
|
|
|
_sessions: dict[str, AgentRunSession]
|
|
_lock: asyncio.Lock
|
|
|
|
def __init__(self):
|
|
self._sessions = {}
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def register(
|
|
self,
|
|
run_id: str,
|
|
runner_id: str,
|
|
query_id: int | None,
|
|
plugin_identity: str,
|
|
resources: AgentResources,
|
|
conversation_id: str | None = None,
|
|
permissions: dict[str, list[str]] | None = None,
|
|
state_policy: dict[str, typing.Any] | None = None,
|
|
state_context: dict[str, typing.Any] | None = None,
|
|
) -> None:
|
|
"""Register a new agent run session.
|
|
|
|
Args:
|
|
run_id: Unique run identifier
|
|
runner_id: Runner descriptor ID
|
|
query_id: Host entry query ID, only present for query-based adapters
|
|
plugin_identity: Plugin identifier (author/name)
|
|
resources: Authorized resources for this run
|
|
conversation_id: Conversation ID for history/event access
|
|
permissions: Runner permissions from descriptor (artifacts, history, events, etc.)
|
|
state_policy: State policy from binding (enable_state, state_scopes)
|
|
state_context: Context for state API (scope_keys, binding_identity, etc.)
|
|
"""
|
|
now = int(time.time())
|
|
|
|
# Normalize permissions to empty dict if None
|
|
permissions = permissions or {}
|
|
|
|
# Normalize state_policy to defaults if None
|
|
if state_policy is None:
|
|
state_policy = {'enable_state': True, 'state_scopes': ['conversation', 'actor']}
|
|
|
|
# Normalize state_context to empty dict if None
|
|
state_context = state_context or {}
|
|
|
|
resources_snapshot = copy.deepcopy(resources)
|
|
authorization: RunAuthorizationSnapshot = {
|
|
'resources': resources_snapshot,
|
|
'permissions': copy.deepcopy(permissions),
|
|
'conversation_id': conversation_id,
|
|
'state_policy': copy.deepcopy(state_policy),
|
|
'state_context': copy.deepcopy(state_context),
|
|
'authorized_ids': self._build_authorized_ids(resources_snapshot),
|
|
}
|
|
|
|
session: AgentRunSession = {
|
|
'run_id': run_id,
|
|
'runner_id': runner_id,
|
|
'query_id': query_id,
|
|
'plugin_identity': plugin_identity,
|
|
'authorization': authorization,
|
|
'status': {
|
|
'started_at': now,
|
|
'last_activity_at': now,
|
|
},
|
|
}
|
|
|
|
async with self._lock:
|
|
self._sessions[run_id] = session
|
|
|
|
def _build_authorized_ids(self, resources: AgentResources) -> dict[str, set[str]]:
|
|
"""Pre-compute authorized resource IDs for O(1) lookup."""
|
|
return {
|
|
'model': {m.get('model_id') for m in resources.get('models', [])},
|
|
'tool': {t.get('tool_name') for t in resources.get('tools', [])},
|
|
'knowledge_base': {kb.get('kb_id') for kb in resources.get('knowledge_bases', [])},
|
|
'file': {f.get('file_id') for f in resources.get('files', [])},
|
|
}
|
|
|
|
async def unregister(self, run_id: str) -> None:
|
|
"""Unregister an agent run session.
|
|
|
|
Args:
|
|
run_id: Unique run identifier
|
|
"""
|
|
async with self._lock:
|
|
if run_id in self._sessions:
|
|
del self._sessions[run_id]
|
|
|
|
async def get(self, run_id: str) -> AgentRunSession | None:
|
|
"""Get session by run_id.
|
|
|
|
Args:
|
|
run_id: Unique run identifier
|
|
|
|
Returns:
|
|
AgentRunSession if found, None otherwise
|
|
"""
|
|
async with self._lock:
|
|
return self._sessions.get(run_id)
|
|
|
|
async def update_activity(self, run_id: str) -> None:
|
|
"""Update last activity timestamp for session.
|
|
|
|
Args:
|
|
run_id: Unique run identifier
|
|
"""
|
|
async with self._lock:
|
|
if run_id in self._sessions:
|
|
self._sessions[run_id]['status']['last_activity_at'] = int(time.time())
|
|
|
|
def is_resource_allowed(
|
|
self,
|
|
session: AgentRunSession,
|
|
resource_type: str,
|
|
resource_id: str,
|
|
) -> bool:
|
|
"""Check if resource access is allowed for this session.
|
|
|
|
Uses pre-computed authorized IDs for O(1) lookup.
|
|
|
|
Args:
|
|
session: AgentRunSession to check
|
|
resource_type: Resource type ('model', 'tool', 'knowledge_base', 'storage', 'file')
|
|
resource_id: Resource identifier (model_id, tool_name, kb_id, 'plugin'/'workspace', file_key)
|
|
|
|
Returns:
|
|
True if resource is authorized, False otherwise
|
|
"""
|
|
authorization = session['authorization']
|
|
authorized_ids = authorization['authorized_ids']
|
|
resources = authorization['resources']
|
|
|
|
if resource_type in ('model', 'tool', 'knowledge_base', 'file'):
|
|
return resource_id in authorized_ids.get(resource_type, set())
|
|
|
|
if resource_type == 'storage':
|
|
storage = resources.get('storage', {})
|
|
if resource_id == 'plugin':
|
|
return storage.get('plugin_storage', False)
|
|
elif resource_id == 'workspace':
|
|
return storage.get('workspace_storage', False)
|
|
return False
|
|
|
|
return False
|
|
|
|
async def list_active_runs(self) -> list[AgentRunSession]:
|
|
"""List all active run sessions.
|
|
|
|
Returns:
|
|
List of active AgentRunSession dicts
|
|
"""
|
|
async with self._lock:
|
|
return list(self._sessions.values())
|
|
|
|
async def cleanup_stale_sessions(self, max_age_seconds: int = 3600) -> int:
|
|
"""Cleanup sessions that have been inactive for too long.
|
|
|
|
Args:
|
|
max_age_seconds: Maximum inactivity time in seconds (default 1 hour)
|
|
|
|
Returns:
|
|
Number of sessions cleaned up
|
|
"""
|
|
now = int(time.time())
|
|
cleaned = 0
|
|
|
|
async with self._lock:
|
|
stale_run_ids = []
|
|
for run_id, session in self._sessions.items():
|
|
last_activity = session['status'].get('last_activity_at', 0)
|
|
if now - last_activity > max_age_seconds:
|
|
stale_run_ids.append(run_id)
|
|
|
|
for run_id in stale_run_ids:
|
|
del self._sessions[run_id]
|
|
cleaned += 1
|
|
|
|
return cleaned
|
|
|
|
|
|
# Global registry instance (singleton)
|
|
_global_registry: AgentRunSessionRegistry | None = None
|
|
_global_registry_lock = threading.Lock()
|
|
|
|
|
|
def get_session_registry() -> AgentRunSessionRegistry:
|
|
"""Get global session registry instance (thread-safe singleton).
|
|
|
|
Returns:
|
|
AgentRunSessionRegistry singleton
|
|
"""
|
|
global _global_registry
|
|
with _global_registry_lock:
|
|
if _global_registry is None:
|
|
_global_registry = AgentRunSessionRegistry()
|
|
return _global_registry
|