mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-20 19:36:08 +00:00
fix: harden agent runner runtime boundaries
This commit is contained in:
@@ -16,6 +16,7 @@ from ...entity.persistence.artifact import AgentArtifact
|
||||
from ...entity.persistence.bstorage import BinaryStorage
|
||||
|
||||
_FILE_ARTIFACT_METADATA_KEY = '_langbot_file_artifact'
|
||||
_ARTIFACT_THREAD_METADATA_KEY = '_langbot_thread_id'
|
||||
|
||||
|
||||
class ArtifactStore:
|
||||
@@ -56,6 +57,7 @@ class ArtifactStore:
|
||||
runner_id: str | None = None,
|
||||
bot_id: str | None = None,
|
||||
workspace_id: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
expires_at: datetime.datetime | None = None,
|
||||
metadata: dict[str, typing.Any] | None = None,
|
||||
) -> str:
|
||||
@@ -92,6 +94,7 @@ class ArtifactStore:
|
||||
runner_id=runner_id,
|
||||
bot_id=bot_id,
|
||||
workspace_id=workspace_id,
|
||||
thread_id=thread_id,
|
||||
expires_at=expires_at,
|
||||
metadata=public_metadata,
|
||||
content=None,
|
||||
@@ -113,6 +116,7 @@ class ArtifactStore:
|
||||
runner_id: str | None = None,
|
||||
bot_id: str | None = None,
|
||||
workspace_id: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
expires_at: datetime.datetime | None = None,
|
||||
metadata: dict[str, typing.Any] | None = None,
|
||||
content: bytes | None = None,
|
||||
@@ -137,6 +141,7 @@ class ArtifactStore:
|
||||
runner_id: Runner ID that created this
|
||||
bot_id: Bot UUID
|
||||
workspace_id: Workspace ID
|
||||
thread_id: Thread ID stored as Host-only metadata
|
||||
expires_at: Expiration time
|
||||
metadata: Additional metadata
|
||||
content: Optional content to store in BinaryStorage
|
||||
@@ -147,6 +152,10 @@ class ArtifactStore:
|
||||
if artifact_id is None:
|
||||
artifact_id = str(uuid.uuid4())
|
||||
|
||||
metadata_payload = dict(metadata or {})
|
||||
if thread_id is not None:
|
||||
metadata_payload[_ARTIFACT_THREAD_METADATA_KEY] = thread_id
|
||||
|
||||
# If content provided, store in BinaryStorage
|
||||
if content is not None and storage_key is None:
|
||||
storage_key = f"artifact:{artifact_id}"
|
||||
@@ -184,7 +193,7 @@ class ArtifactStore:
|
||||
workspace_id=workspace_id,
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
expires_at=expires_at,
|
||||
metadata_json=json.dumps(metadata) if metadata else None,
|
||||
metadata_json=json.dumps(metadata_payload) if metadata_payload else None,
|
||||
)
|
||||
session.add(artifact)
|
||||
await session.commit()
|
||||
@@ -216,6 +225,22 @@ class ArtifactStore:
|
||||
return None
|
||||
return self._row_to_public_dict(row)
|
||||
|
||||
async def get_authorization_metadata(
|
||||
self,
|
||||
artifact_id: str,
|
||||
) -> dict[str, typing.Any] | None:
|
||||
"""Get artifact metadata with Host-only scope fields for authorization."""
|
||||
row = await self._get_internal_record(artifact_id)
|
||||
if row is None:
|
||||
return None
|
||||
metadata = self._row_to_public_dict(row)
|
||||
metadata.update({
|
||||
'bot_id': row.bot_id,
|
||||
'workspace_id': row.workspace_id,
|
||||
'thread_id': self._load_metadata(row.metadata_json).get(_ARTIFACT_THREAD_METADATA_KEY),
|
||||
})
|
||||
return metadata
|
||||
|
||||
async def _get_internal_record(
|
||||
self,
|
||||
artifact_id: str,
|
||||
@@ -455,6 +480,7 @@ class ArtifactStore:
|
||||
def _public_metadata(metadata_json: str | None) -> dict[str, typing.Any]:
|
||||
metadata = ArtifactStore._load_metadata(metadata_json)
|
||||
metadata.pop(_FILE_ARTIFACT_METADATA_KEY, None)
|
||||
metadata.pop(_ARTIFACT_THREAD_METADATA_KEY, None)
|
||||
return metadata
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -6,6 +6,10 @@ import typing
|
||||
from .descriptor import AgentRunnerDescriptor
|
||||
|
||||
|
||||
FORM_ITEM_TYPE_ALIASES = {
|
||||
'select-llm-model': 'llm-model-selector',
|
||||
'select-knowledge-bases': 'knowledge-base-multi-selector',
|
||||
}
|
||||
LLM_MODEL_SELECTOR_TYPES = {'model-fallback-selector', 'llm-model-selector'}
|
||||
KB_SELECTOR_TYPES = {'knowledge-base-multi-selector'}
|
||||
PROMPT_EDITOR_TYPES = {'prompt-editor'}
|
||||
@@ -13,6 +17,13 @@ FILE_SELECTOR_TYPES = {'file', 'array[file]'}
|
||||
NONE_SENTINELS = {'', '__none__', '__none'}
|
||||
|
||||
|
||||
def normalize_schema_item_type(item_type: typing.Any) -> typing.Any:
|
||||
"""Normalize legacy/frontend DynamicForm aliases to protocol field types."""
|
||||
if not isinstance(item_type, str):
|
||||
return item_type
|
||||
return FORM_ITEM_TYPE_ALIASES.get(item_type, item_type)
|
||||
|
||||
|
||||
def iter_schema_items(
|
||||
descriptor: AgentRunnerDescriptor | None,
|
||||
field_types: set[str],
|
||||
@@ -23,7 +34,7 @@ def iter_schema_items(
|
||||
for item in descriptor.config_schema or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get('type') in field_types:
|
||||
if normalize_schema_item_type(item.get('type')) in field_types:
|
||||
yield item
|
||||
|
||||
|
||||
@@ -81,7 +92,8 @@ def extract_model_selection(
|
||||
continue
|
||||
|
||||
value = runner_config.get(field_name, item.get('default'))
|
||||
if item.get('type') == 'model-fallback-selector':
|
||||
item_type = normalize_schema_item_type(item.get('type'))
|
||||
if item_type == 'model-fallback-selector':
|
||||
if isinstance(value, str):
|
||||
primary_uuid = value
|
||||
elif isinstance(value, dict):
|
||||
@@ -91,7 +103,7 @@ def extract_model_selection(
|
||||
fallback_uuids = [fallback for fallback in fallbacks if isinstance(fallback, str)]
|
||||
break
|
||||
|
||||
if item.get('type') == 'llm-model-selector' and isinstance(value, str):
|
||||
if item_type == 'llm-model-selector' and isinstance(value, str):
|
||||
primary_uuid = value
|
||||
break
|
||||
|
||||
@@ -145,7 +157,8 @@ def extract_config_file_resources(
|
||||
if not field_name:
|
||||
continue
|
||||
value = runner_config.get(field_name, item.get('default'))
|
||||
if item.get('type') == 'file':
|
||||
item_type = normalize_schema_item_type(item.get('type'))
|
||||
if item_type == 'file':
|
||||
append_file(value)
|
||||
elif isinstance(value, list):
|
||||
for entry in value:
|
||||
@@ -167,7 +180,7 @@ def iter_config_model_refs(
|
||||
continue
|
||||
|
||||
field_name = item.get('name')
|
||||
field_type = item.get('type')
|
||||
field_type = normalize_schema_item_type(item.get('type'))
|
||||
if not field_name or field_name not in runner_config:
|
||||
continue
|
||||
|
||||
@@ -200,7 +213,7 @@ def set_empty_llm_model_selection(
|
||||
"""Set the first empty schema-defined LLM selector to model_uuid."""
|
||||
for item in iter_schema_items(descriptor, LLM_MODEL_SELECTOR_TYPES):
|
||||
field_name = item.get('name')
|
||||
field_type = item.get('type')
|
||||
field_type = normalize_schema_item_type(item.get('type'))
|
||||
if not field_name:
|
||||
continue
|
||||
|
||||
|
||||
@@ -283,6 +283,9 @@ class AgentRunOrchestrator:
|
||||
target_run_id = await self._session_registry.find_steering_target(
|
||||
conversation_id=event.conversation_id,
|
||||
runner_id=descriptor.id,
|
||||
bot_id=event.bot_id,
|
||||
workspace_id=event.workspace_id,
|
||||
thread_id=event.thread_id,
|
||||
)
|
||||
if target_run_id is None:
|
||||
return False
|
||||
|
||||
@@ -230,6 +230,7 @@ class AgentRunJournal:
|
||||
runner_id=runner_id,
|
||||
bot_id=event.bot_id,
|
||||
workspace_id=event.workspace_id,
|
||||
thread_id=event.thread_id,
|
||||
metadata=metadata,
|
||||
content=content,
|
||||
)
|
||||
@@ -371,6 +372,7 @@ class AgentRunJournal:
|
||||
runner_id=runner_id,
|
||||
bot_id=event.bot_id,
|
||||
workspace_id=event.workspace_id,
|
||||
thread_id=event.thread_id,
|
||||
metadata=metadata,
|
||||
content=content,
|
||||
)
|
||||
|
||||
@@ -254,6 +254,9 @@ class AgentRunSessionRegistry:
|
||||
*,
|
||||
conversation_id: str,
|
||||
runner_id: str,
|
||||
bot_id: str | None = None,
|
||||
workspace_id: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
) -> str | None:
|
||||
"""Find the oldest active run that can accept steering for a conversation."""
|
||||
async with self._lock:
|
||||
@@ -264,6 +267,12 @@ class AgentRunSessionRegistry:
|
||||
continue
|
||||
if authorization.get('conversation_id') != conversation_id:
|
||||
continue
|
||||
if authorization.get('bot_id') != bot_id:
|
||||
continue
|
||||
if authorization.get('workspace_id') != workspace_id:
|
||||
continue
|
||||
if authorization.get('thread_id') != thread_id:
|
||||
continue
|
||||
if not authorization.get('available_apis', {}).get('steering_pull', False):
|
||||
continue
|
||||
candidates.append((session['status'].get('started_at', 0), run_id))
|
||||
|
||||
@@ -254,6 +254,10 @@ class TranscriptStore:
|
||||
self,
|
||||
conversation_id: str,
|
||||
limit: int = HARD_LIMIT,
|
||||
bot_id: str | None = None,
|
||||
workspace_id: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
strict_thread: bool = False,
|
||||
) -> list[provider_message.Message]:
|
||||
"""Project Transcript rows into the legacy provider Message view.
|
||||
|
||||
@@ -265,6 +269,10 @@ class TranscriptStore:
|
||||
conversation_id=conversation_id,
|
||||
limit=limit,
|
||||
direction="backward",
|
||||
bot_id=bot_id,
|
||||
workspace_id=workspace_id,
|
||||
thread_id=thread_id,
|
||||
strict_thread=strict_thread,
|
||||
)
|
||||
|
||||
messages: list[provider_message.Message] = []
|
||||
|
||||
Reference in New Issue
Block a user