diff --git a/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md b/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md index d9ca602a..78b3f075 100644 --- a/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md +++ b/docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md @@ -234,7 +234,21 @@ LangBot 应提供事实源能力: AgentRunner 可以读取这些能力,但不能被迫使用 LangBot 作为唯一记忆系统。 -### 4.8 External harness resource projection +### 4.8 Prompt / Instruction Package(占位) + +旧 Pipeline 入口目前可以把 preprocessing 后的有效 prompt 放进 adapter metadata, +这是为了保持旧入口行为,不是长期协议。目标形态应是 Host 保存或生成一个 +run-scoped instruction package,runner 通过 Host API 拉取: + +- Host 负责记录静态绑定 prompt、host hook / user plugin 产生的 instruction + fragment、来源和审计信息。 +- `ctx.context.available_apis.prompt_get` 只表示拉取能力是否可用。 +- Runner 拉取 instruction package 后,仍由 runner 自己决定如何与 history、RAG、 + tool 结果、memory 和当前输入组装最终模型 prompt。 +- Host 不实现通用 agentic prompt assembler,也不把 Pipeline adapter prompt 作为 + 长期业务输入契约。 + +### 4.9 External harness resource projection Claude Code、Codex、Kimi Code 等外部 harness runner 可能不会直接调用 LangBot 的 model/tool loop,而是把 LangBot 事件和授权资源投影到自己的 harness 中执行。Host 侧仍要保持统一边界: diff --git a/docs/agent-runner-pluginization/PROTOCOL_V1.md b/docs/agent-runner-pluginization/PROTOCOL_V1.md index 8c60d3dd..d5437554 100644 --- a/docs/agent-runner-pluginization/PROTOCOL_V1.md +++ b/docs/agent-runner-pluginization/PROTOCOL_V1.md @@ -670,6 +670,8 @@ Pipeline adapter 负责: - 从 Pipeline config 构造临时 AgentBinding。 - 从当前 runner binding config 构造 `ctx.config`。 - 保留必要的 legacy adapter metadata,但不定义历史窗口、prompt 组装或 agentic context 策略。 +- 后续若需要传递 preprocessing / hook 后的有效指令,应通过 Host prompt/instruction + package pull API 暴露能力位和引用,而不是继续把 prompt 推入 `ctx.adapter.extra`。 - 将 Query-only 字段放入 `adapter`。 Runner 不应长期依赖 `adapter`。新 runner 应只依赖 event-first context 和 Host APIs。 diff --git a/src/langbot/pkg/agent/runner/config_migration.py b/src/langbot/pkg/agent/runner/config_migration.py index 0dac8cf0..3d007644 100644 --- a/src/langbot/pkg/agent/runner/config_migration.py +++ b/src/langbot/pkg/agent/runner/config_migration.py @@ -1,4 +1,5 @@ """Configuration migration for agent runner IDs.""" + from __future__ import annotations import typing @@ -113,10 +114,33 @@ class ConfigMigration: if old_runner_name: old_config = ai_config.get(old_runner_name, {}) if old_config: - return old_config + return ConfigMigration.normalize_runner_config_for_migration(runner_id, old_config) return {} + @staticmethod + def normalize_runner_config_for_migration( + runner_id: str, + runner_config: dict[str, typing.Any], + ) -> dict[str, typing.Any]: + """Normalize released legacy runner config before storing binding config. + + Runtime code should not carry aliases. This helper is intentionally used + only by config migration so AgentRunner implementations can consume the + current manifest-defined field names. + """ + normalized = dict(runner_config) + + if runner_id == OLD_RUNNER_TO_PLUGIN_RUNNER_ID['local-agent']: + legacy_kb = normalized.pop('knowledge-base', None) + if 'knowledge-bases' not in normalized: + if isinstance(legacy_kb, str) and legacy_kb and legacy_kb not in {'__none__', '__none'}: + normalized['knowledge-bases'] = [legacy_kb] + elif legacy_kb is not None: + normalized['knowledge-bases'] = [] + + return normalized + @staticmethod def get_old_runner_name(runner_id: str) -> str | None: """Get old runner name from mapped runner ID. @@ -188,6 +212,7 @@ class ConfigMigration: if not resolved_config: resolved_config = ConfigMigration.resolve_legacy_runner_config(pipeline_config, runner_id) if resolved_config: + resolved_config = ConfigMigration.normalize_runner_config_for_migration(runner_id, resolved_config) runner_configs[runner_id] = resolved_config # Remove old runner config block for old_name, mapped_id in OLD_RUNNER_TO_PLUGIN_RUNNER_ID.items(): diff --git a/src/langbot/pkg/agent/runner/context_builder.py b/src/langbot/pkg/agent/runner/context_builder.py index a6be6940..d6fb4179 100644 --- a/src/langbot/pkg/agent/runner/context_builder.py +++ b/src/langbot/pkg/agent/runner/context_builder.py @@ -1,4 +1,5 @@ """Agent run context builder for provisioning AgentRunContext envelopes.""" + from __future__ import annotations import uuid @@ -19,6 +20,7 @@ DEFAULT_RUNNER_TIMEOUT_SECONDS = 300 class AgentTrigger(typing.TypedDict): """Agent trigger information.""" + type: str source: str # 'pipeline' or 'event_router' timestamp: int | None @@ -26,6 +28,7 @@ class AgentTrigger(typing.TypedDict): class ConversationContext(typing.TypedDict): """Conversation context.""" + conversation_id: str | None thread_id: str | None launcher_type: str | None @@ -39,6 +42,7 @@ class ConversationContext(typing.TypedDict): class AgentInput(typing.TypedDict): """Agent input.""" + text: str | None contents: list[dict[str, typing.Any]] message_chain: dict[str, typing.Any] | None @@ -47,6 +51,7 @@ class AgentInput(typing.TypedDict): class AgentRunState(typing.TypedDict): """Agent run state with 4 scopes.""" + conversation: dict[str, typing.Any] actor: dict[str, typing.Any] subject: dict[str, typing.Any] @@ -58,6 +63,7 @@ class AgentRunState(typing.TypedDict): class ModelResource(typing.TypedDict): """Model resource payload.""" + model_id: str model_type: str | None provider: str | None @@ -65,6 +71,7 @@ class ModelResource(typing.TypedDict): class ToolResource(typing.TypedDict): """Tool resource payload.""" + tool_name: str tool_type: str | None description: str | None @@ -72,6 +79,7 @@ class ToolResource(typing.TypedDict): class KnowledgeBaseResource(typing.TypedDict): """Knowledge base resource payload.""" + kb_id: str kb_name: str | None kb_type: str | None @@ -79,6 +87,7 @@ class KnowledgeBaseResource(typing.TypedDict): class FileResource(typing.TypedDict): """File resource payload.""" + file_id: str file_name: str | None mime_type: str | None @@ -87,12 +96,14 @@ class FileResource(typing.TypedDict): class StorageResource(typing.TypedDict): """Storage resource payload.""" + plugin_storage: bool workspace_storage: bool class AgentResources(typing.TypedDict): """Agent resources payload.""" + models: list[ModelResource] tools: list[ToolResource] knowledge_bases: list[KnowledgeBaseResource] @@ -103,6 +114,7 @@ class AgentResources(typing.TypedDict): class AgentRuntimeContext(typing.TypedDict): """Agent runtime context.""" + langbot_version: str | None sdk_protocol_version: str query_id: int | None @@ -119,6 +131,7 @@ class AgentRunContextPayload(typing.TypedDict): Note: The 'config' field contains the binding config from ai.runner_config[runner_id], which is Pipeline's configuration for this specific runner binding (not plugin instance config). """ + run_id: str trigger: AgentTrigger conversation: ConversationContext | None @@ -237,7 +250,9 @@ class AgentRunContextBuilder: 'text': event.input.text, 'contents': [c.model_dump(mode='json') if hasattr(c, 'model_dump') else c for c in event.input.contents], 'message_chain': event.input.message_chain, - 'attachments': [a.model_dump(mode='json') if hasattr(a, 'model_dump') else a for a in event.input.attachments], + 'attachments': [ + a.model_dump(mode='json') if hasattr(a, 'model_dump') else a for a in event.input.attachments + ], } # Build context access (no history inlined by default for Protocol v1) @@ -245,9 +260,7 @@ class AgentRunContextBuilder: context_access = await self._build_context_access(event, descriptor, binding) # Build state snapshot from persistent state store (event-first Protocol v1) - persistent_state_store = get_persistent_state_store( - self.ap.persistence_mgr.get_db_engine() - ) + persistent_state_store = get_persistent_state_store(self.ap.persistence_mgr.get_db_engine()) state: AgentRunState = await persistent_state_store.build_snapshot_from_event(event, binding, descriptor) # Build runtime context @@ -261,6 +274,10 @@ class AgentRunContextBuilder: 'bot_id': event.bot_id, 'workspace_id': event.workspace_id, 'streaming_supported': event.delivery.supports_streaming, + 'model_context_window_tokens': None, + # TODO(model-info): populate model_context_window_tokens after + # LiteLLM/model metadata lands. Runners fall back to their + # binding config until Host can provide the real window. }, } @@ -375,6 +392,7 @@ class AgentRunContextBuilder: if conversation_id: try: from .transcript_store import TranscriptStore + store = TranscriptStore(self.ap.persistence_mgr.get_db_engine()) latest_cursor = await store.get_latest_cursor(conversation_id) @@ -406,5 +424,6 @@ class AgentRunContextBuilder: 'artifact_read': artifact_read_enabled, 'state': state_enabled, 'storage': True, + 'prompt_get': False, }, } diff --git a/src/langbot/pkg/persistence/alembic/versions/0004_migrate_runner_config.py b/src/langbot/pkg/persistence/alembic/versions/0004_migrate_runner_config.py index 504145d1..5f49a77c 100644 --- a/src/langbot/pkg/persistence/alembic/versions/0004_migrate_runner_config.py +++ b/src/langbot/pkg/persistence/alembic/versions/0004_migrate_runner_config.py @@ -31,6 +31,21 @@ def is_plugin_runner_id(runner_id: str) -> bool: return runner_id.startswith('plugin:') +def normalize_runner_config_for_migration(runner_id: str, runner_config: dict) -> dict: + """Normalize released legacy runner fields before storing binding config.""" + normalized = dict(runner_config) + + if runner_id == OLD_RUNNER_TO_PLUGIN_RUNNER_ID['local-agent']: + legacy_kb = normalized.pop('knowledge-base', None) + if 'knowledge-bases' not in normalized: + if isinstance(legacy_kb, str) and legacy_kb and legacy_kb not in {'__none__', '__none'}: + normalized['knowledge-bases'] = [legacy_kb] + elif legacy_kb is not None: + normalized['knowledge-bases'] = [] + + return normalized + + def migrate_pipeline_config(config: dict) -> dict: """Migrate pipeline config to new format.""" new_config = dict(config) @@ -44,7 +59,13 @@ def migrate_pipeline_config(config: dict) -> dict: # Check for new format first runner_id = runner_config.get('id') if runner_id and is_plugin_runner_id(runner_id): - # Already in new format, no need to migrate + if runner_id in runner_configs: + runner_configs[runner_id] = normalize_runner_config_for_migration( + runner_id, + runner_configs[runner_id], + ) + ai_config['runner_config'] = runner_configs + new_config['ai'] = ai_config return new_config # Check for old format @@ -67,14 +88,14 @@ def migrate_pipeline_config(config: dict) -> dict: if old_runner_name in ai_config: old_runner_config = ai_config[old_runner_name] if old_runner_config: - runner_configs[runner_id] = old_runner_config + runner_configs[runner_id] = normalize_runner_config_for_migration(runner_id, old_runner_config) # Remove old config block after migration del ai_config[old_runner_name] # Also check if runner_id has config under other old name formats for old_name, mapped_id in OLD_RUNNER_TO_PLUGIN_RUNNER_ID.items(): if mapped_id == runner_id and old_name in ai_config: - runner_configs[runner_id] = ai_config[old_name] + runner_configs[runner_id] = normalize_runner_config_for_migration(runner_id, ai_config[old_name]) # Remove old config block after migration del ai_config[old_name] @@ -111,7 +132,7 @@ def upgrade() -> None: if json.dumps(config, sort_keys=True) != json.dumps(migrated_config, sort_keys=True): conn.execute( sa.text('UPDATE pipelines SET config = :config WHERE uuid = :uuid'), - {'config': json.dumps(migrated_config), 'uuid': pipeline_uuid} + {'config': json.dumps(migrated_config), 'uuid': pipeline_uuid}, ) except Exception: # Skip invalid configs @@ -121,4 +142,4 @@ def upgrade() -> None: def downgrade() -> None: """Downgrade is not supported for data migration.""" # No downgrade - keep configs in new format - pass \ No newline at end of file + pass diff --git a/tests/unit_tests/agent/test_config_migration.py b/tests/unit_tests/agent/test_config_migration.py index 07be608b..c1e3b560 100644 --- a/tests/unit_tests/agent/test_config_migration.py +++ b/tests/unit_tests/agent/test_config_migration.py @@ -1,4 +1,5 @@ """Tests for agent runner config migration.""" + from __future__ import annotations @@ -155,6 +156,7 @@ class TestResolveRunnerConfig: 'local-agent': { 'model': 'uuid-123', 'max_round': 10, + 'knowledge-base': 'kb-123', }, }, } @@ -163,7 +165,8 @@ class TestResolveRunnerConfig: pipeline_config, 'plugin:langbot/local-agent/default', ) - assert config == {'model': 'uuid-123', 'max_round': 10} + assert config == {'model': 'uuid-123', 'max_round': 10, 'knowledge-bases': ['kb-123']} + assert 'knowledge-base' not in config def test_resolve_no_config(self): """Resolve runner config when not found.""" diff --git a/tests/unit_tests/agent/test_config_migration_full.py b/tests/unit_tests/agent/test_config_migration_full.py index 15bdef93..a55af073 100644 --- a/tests/unit_tests/agent/test_config_migration_full.py +++ b/tests/unit_tests/agent/test_config_migration_full.py @@ -21,6 +21,7 @@ class TestMigratePipelineConfig: 'local-agent': { 'model': {'primary': 'model-uuid', 'fallbacks': []}, 'max-round': 10, + 'knowledge-base': 'kb-uuid', 'prompt': [{'role': 'system', 'content': 'Hello'}], }, }, @@ -35,6 +36,8 @@ class TestMigratePipelineConfig: # Config should be in runner_config assert 'plugin:langbot/local-agent/default' in migrated['ai']['runner_config'] assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['max-round'] == 10 + assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['knowledge-bases'] == ['kb-uuid'] + assert 'knowledge-base' not in migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] # Expire-time preserved assert migrated['ai']['runner']['expire-time'] == 0 @@ -85,6 +88,26 @@ class TestMigratePipelineConfig: assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default' assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['max-round'] == 10 + def test_new_format_local_agent_config_normalizes_legacy_kb_key(self): + """Migration should normalize legacy KB aliases before runtime.""" + config = { + 'ai': { + 'runner': { + 'id': 'plugin:langbot/local-agent/default', + }, + 'runner_config': { + 'plugin:langbot/local-agent/default': { + 'knowledge-base': 'kb-legacy', + }, + }, + }, + } + + migrated = ConfigMigration.migrate_pipeline_config(config) + runner_config = migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] + + assert runner_config == {'knowledge-bases': ['kb-legacy']} + def test_migrate_all_old_runners(self): """All old runner names should be migrated.""" old_runners = [ diff --git a/tests/unit_tests/agent/test_handler_auth.py b/tests/unit_tests/agent/test_handler_auth.py index 26c560f7..2deacce5 100644 --- a/tests/unit_tests/agent/test_handler_auth.py +++ b/tests/unit_tests/agent/test_handler_auth.py @@ -10,6 +10,7 @@ Authorization paths: 1. AgentRunner calls: has run_id, validates against session_registry 2. Regular plugin calls: no run_id, unscoped plugin action path """ + from __future__ import annotations import pytest @@ -26,6 +27,7 @@ from .conftest import make_resources class MockModel: """Mock LLM model for testing.""" + def __init__(self, uuid: str): self.uuid = uuid self.provider = MagicMock() @@ -34,6 +36,7 @@ class MockModel: class MockEmbeddingModel: """Mock embedding model for testing.""" + def __init__(self, uuid: str): self.uuid = uuid self.provider = MagicMock() @@ -41,6 +44,7 @@ class MockEmbeddingModel: class MockKnowledgeBase: """Mock knowledge base for testing.""" + def __init__(self, uuid: str, name: str = 'KB'): self.knowledge_base_entity = MagicMock() self.knowledge_base_entity.description = f'{name} description' @@ -57,6 +61,7 @@ class MockKnowledgeBase: class MockQuery: """Mock query for testing.""" + def __init__(self, query_id: int = 1): self.query_id = query_id self.session = MagicMock() @@ -81,6 +86,7 @@ class MockQuery: class MockApplication: """Mock Application for testing.""" + def __init__(self): self.logger = MagicMock() self.logger.debug = MagicMock() @@ -125,6 +131,7 @@ class FakeAgentRunnerRegistry: class MockConnection: """Mock connection for testing.""" + pass @@ -155,6 +162,7 @@ class TestPipelineKnowledgeBaseScope: class MockDisconnectCallback: """Mock disconnect callback for testing.""" + async def __call__(self): return True @@ -489,25 +497,25 @@ class TestHandlerAuthorizationErrorMessages: def test_model_not_authorized_error_message(self): """Error message should mention model not authorized.""" - expected_msg = "Model model_999 is not authorized for this agent run" + expected_msg = 'Model model_999 is not authorized for this agent run' assert 'not authorized' in expected_msg assert 'model_999' in expected_msg def test_tool_not_authorized_error_message(self): """Error message should mention tool not authorized.""" - expected_msg = "Tool image_gen is not authorized for this agent run" + expected_msg = 'Tool image_gen is not authorized for this agent run' assert 'not authorized' in expected_msg assert 'image_gen' in expected_msg def test_kb_not_authorized_error_message(self): """Error message should mention kb not authorized.""" - expected_msg = "Knowledge base kb_999 is not authorized for this agent run" + expected_msg = 'Knowledge base kb_999 is not authorized for this agent run' assert 'not authorized' in expected_msg assert 'kb_999' in expected_msg def test_session_not_found_error_message(self): """Error message should mention session not found.""" - expected_msg = "Run session run_xyz not found or expired" + expected_msg = 'Run session run_xyz not found or expired' assert 'not found' in expected_msg assert 'run_xyz' in expected_msg @@ -585,8 +593,8 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix: # Should resolve to plugin:langbot/local-agent/default assert 'local-agent' in runner_id - def test_retrieve_kb_fix_backward_compat_knowledge_base(self): - """Fix should handle backward compat for old 'knowledge-base' field.""" + def test_retrieve_kb_legacy_single_key_is_migration_only(self): + """Old singular knowledge-base config is normalized before runtime.""" from langbot.pkg.agent.runner.config_migration import ConfigMigration pipeline_config = { @@ -602,17 +610,11 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix: }, } - runner_id = ConfigMigration.resolve_runner_id(pipeline_config) - runner_config = ConfigMigration.resolve_runner_config(pipeline_config, runner_id) + migrated = ConfigMigration.migrate_pipeline_config(pipeline_config) + runner_id = ConfigMigration.resolve_runner_id(migrated) + runner_config = ConfigMigration.resolve_runner_config(migrated, runner_id) - # Handler.py checks both knowledge-bases and knowledge-base - allowed_kbs = runner_config.get('knowledge-bases', []) - if not allowed_kbs: - old_kb = runner_config.get('knowledge-base', '') - if old_kb and old_kb != '__none__': - allowed_kbs = [old_kb] - - assert 'kb_single' in allowed_kbs + assert runner_config == {'knowledge-bases': ['kb_single']} class TestHandlerActionAuthorization: @@ -1059,10 +1061,12 @@ class TestResourceTypeValidation: async def test_model_resource_validation(self): """Model resource: correct model_id validation.""" registry = AgentRunSessionRegistry() - resources = make_resources(models=[ - {'model_id': 'model_001'}, - {'model_id': 'model_002'}, - ]) + resources = make_resources( + models=[ + {'model_id': 'model_001'}, + {'model_id': 'model_002'}, + ] + ) await registry.register( run_id='run_model_validation', @@ -1087,10 +1091,12 @@ class TestResourceTypeValidation: async def test_tool_resource_validation(self): """Tool resource: correct tool_name validation.""" registry = AgentRunSessionRegistry() - resources = make_resources(tools=[ - {'tool_name': 'web_search'}, - {'tool_name': 'image_gen'}, - ]) + resources = make_resources( + tools=[ + {'tool_name': 'web_search'}, + {'tool_name': 'image_gen'}, + ] + ) await registry.register( run_id='run_tool_validation', @@ -1115,10 +1121,12 @@ class TestResourceTypeValidation: async def test_knowledge_base_resource_validation(self): """Knowledge base resource: correct kb_id validation.""" registry = AgentRunSessionRegistry() - resources = make_resources(knowledge_bases=[ - {'kb_id': 'kb_001'}, - {'kb_id': 'kb_002'}, - ]) + resources = make_resources( + knowledge_bases=[ + {'kb_id': 'kb_001'}, + {'kb_id': 'kb_002'}, + ] + ) await registry.register( run_id='run_kb_validation', @@ -1262,6 +1270,7 @@ class TestValidateRunAuthorizationHelper: """_validate_run_authorization returns session when resource is authorized.""" # Use global session registry (same as _validate_run_authorization) from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1280,12 +1289,7 @@ class TestValidateRunAuthorizationHelper: mock_ap = MagicMock() mock_ap.logger = MagicMock() - session, error = await _validate_run_authorization( - 'run_validate_test_helper', - 'model', - 'model_001', - mock_ap - ) + session, error = await _validate_run_authorization('run_validate_test_helper', 'model', 'model_001', mock_ap) # Should return session, no error assert session is not None @@ -1303,12 +1307,7 @@ class TestValidateRunAuthorizationHelper: mock_ap.logger = MagicMock() mock_ap.logger.warning = MagicMock() - session, error = await _validate_run_authorization( - 'run_nonexistent_helper', - 'model', - 'model_001', - mock_ap - ) + session, error = await _validate_run_authorization('run_nonexistent_helper', 'model', 'model_001', mock_ap) # Should return no session, error response assert session is None @@ -1321,6 +1320,7 @@ class TestValidateRunAuthorizationHelper: """_validate_run_authorization returns error when resource not allowed.""" # Use global session registry from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1342,7 +1342,7 @@ class TestValidateRunAuthorizationHelper: 'run_unauthorized_helper', 'model', 'model_999', # Not in resources - mock_ap + mock_ap, ) # Should return no session, error response @@ -1358,6 +1358,7 @@ class TestValidateRunAuthorizationHelper: """_validate_run_authorization works for tool resource type.""" # Use global session registry from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(tools=[{'tool_name': 'web_search'}]) @@ -1374,12 +1375,7 @@ class TestValidateRunAuthorizationHelper: mock_ap = MagicMock() mock_ap.logger = MagicMock() - session, error = await _validate_run_authorization( - 'run_tool_test_helper', - 'tool', - 'web_search', - mock_ap - ) + session, error = await _validate_run_authorization('run_tool_test_helper', 'tool', 'web_search', mock_ap) assert session is not None assert error is None @@ -1391,6 +1387,7 @@ class TestValidateRunAuthorizationHelper: """_validate_run_authorization works for knowledge_base resource type.""" # Use global session registry from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(knowledge_bases=[{'kb_id': 'kb_001'}]) @@ -1407,12 +1404,7 @@ class TestValidateRunAuthorizationHelper: mock_ap = MagicMock() mock_ap.logger = MagicMock() - session, error = await _validate_run_authorization( - 'run_kb_test_helper', - 'knowledge_base', - 'kb_001', - mock_ap - ) + session, error = await _validate_run_authorization('run_kb_test_helper', 'knowledge_base', 'kb_001', mock_ap) assert session is not None assert error is None @@ -1545,6 +1537,7 @@ class TestFilesResourcePermission: async def test_files_resource_type_now_implemented(self): """'files' resource type is now implemented in is_resource_allowed.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(files=[{'file_id': 'file_001'}]) @@ -1577,6 +1570,7 @@ class TestRealActionHandlerSimulation: """Simulate INVOKE_LLM action handler authorization flow.""" # Use global session registry from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1595,12 +1589,7 @@ class TestRealActionHandlerSimulation: mock_ap.logger = MagicMock() # Step 1: Validate authorization - session, error = await _validate_run_authorization( - 'run_invoke_llm_flow_sim', - 'model', - 'model_001', - mock_ap - ) + session, error = await _validate_run_authorization('run_invoke_llm_flow_sim', 'model', 'model_001', mock_ap) # Should pass authorization assert session is not None @@ -1615,6 +1604,7 @@ class TestRealActionHandlerSimulation: """Simulate INVOKE_LLM handler rejecting unauthorized model.""" # Use global session registry from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1633,12 +1623,7 @@ class TestRealActionHandlerSimulation: mock_ap.logger.warning = MagicMock() # Try to access unauthorized model - session, error = await _validate_run_authorization( - 'run_reject_model_sim', - 'model', - 'model_999', - mock_ap - ) + session, error = await _validate_run_authorization('run_reject_model_sim', 'model', 'model_999', mock_ap) # Should reject assert session is None @@ -1659,10 +1644,7 @@ class TestRealActionHandlerSimulation: # Try to validate with non-existent run_id session, error = await _validate_run_authorization( - 'run_nonexistent_session_flow', - 'model', - 'model_001', - mock_ap + 'run_nonexistent_session_flow', 'model', 'model_001', mock_ap ) # Should return error @@ -1683,6 +1665,7 @@ class TestStoragePermissionValidation: async def test_plugin_storage_allowed_when_permitted(self): """_validate_run_authorization allows 'plugin' storage when permitted.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(storage={'plugin_storage': True, 'workspace_storage': False}) @@ -1699,12 +1682,7 @@ class TestStoragePermissionValidation: mock_ap = MagicMock() mock_ap.logger = MagicMock() - session, error = await _validate_run_authorization( - 'run_plugin_storage_auth', - 'storage', - 'plugin', - mock_ap - ) + session, error = await _validate_run_authorization('run_plugin_storage_auth', 'storage', 'plugin', mock_ap) assert session is not None assert error is None @@ -1715,6 +1693,7 @@ class TestStoragePermissionValidation: async def test_plugin_storage_denied_when_not_permitted(self): """_validate_run_authorization denies 'plugin' storage when not permitted.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(storage={'plugin_storage': False, 'workspace_storage': False}) @@ -1732,12 +1711,7 @@ class TestStoragePermissionValidation: mock_ap.logger = MagicMock() mock_ap.logger.warning = MagicMock() - session, error = await _validate_run_authorization( - 'run_plugin_storage_denied', - 'storage', - 'plugin', - mock_ap - ) + session, error = await _validate_run_authorization('run_plugin_storage_denied', 'storage', 'plugin', mock_ap) assert session is None assert error is not None @@ -1749,6 +1723,7 @@ class TestStoragePermissionValidation: async def test_workspace_storage_allowed_when_permitted(self): """_validate_run_authorization allows 'workspace' storage when permitted.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(storage={'plugin_storage': False, 'workspace_storage': True}) @@ -1766,10 +1741,7 @@ class TestStoragePermissionValidation: mock_ap.logger = MagicMock() session, error = await _validate_run_authorization( - 'run_workspace_storage_auth', - 'storage', - 'workspace', - mock_ap + 'run_workspace_storage_auth', 'storage', 'workspace', mock_ap ) assert session is not None @@ -1781,6 +1753,7 @@ class TestStoragePermissionValidation: async def test_workspace_storage_denied_when_not_permitted(self): """_validate_run_authorization denies 'workspace' storage when not permitted.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(storage={'plugin_storage': False, 'workspace_storage': False}) @@ -1799,10 +1772,7 @@ class TestStoragePermissionValidation: mock_ap.logger.warning = MagicMock() session, error = await _validate_run_authorization( - 'run_workspace_storage_denied', - 'storage', - 'workspace', - mock_ap + 'run_workspace_storage_denied', 'storage', 'workspace', mock_ap ) assert session is None @@ -1823,6 +1793,7 @@ class TestFilePermissionValidation: async def test_file_allowed_when_in_resources(self): """_validate_run_authorization allows file when in resources.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(files=[{'file_id': 'file_001'}]) @@ -1839,12 +1810,7 @@ class TestFilePermissionValidation: mock_ap = MagicMock() mock_ap.logger = MagicMock() - session, error = await _validate_run_authorization( - 'run_file_auth', - 'file', - 'file_001', - mock_ap - ) + session, error = await _validate_run_authorization('run_file_auth', 'file', 'file_001', mock_ap) assert session is not None assert error is None @@ -1855,6 +1821,7 @@ class TestFilePermissionValidation: async def test_file_denied_when_not_in_resources(self): """_validate_run_authorization denies file when not in resources.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(files=[{'file_id': 'file_001'}]) @@ -1876,7 +1843,7 @@ class TestFilePermissionValidation: 'run_file_denied', 'file', 'file_999', # Not in resources - mock_ap + mock_ap, ) assert session is None @@ -1898,6 +1865,7 @@ class TestCallerPluginIdentityValidation: async def test_same_plugin_identity_allowed(self): """_validate_run_authorization allows when caller matches session.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1931,6 +1899,7 @@ class TestCallerPluginIdentityValidation: async def test_different_plugin_identity_denied(self): """_validate_run_authorization denies when caller differs from session.""" from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}]) @@ -1967,6 +1936,7 @@ class TestCallerPluginIdentityValidation: """_validate_run_authorization allows when caller_plugin_identity not provided.""" # Unscoped plugin path: if caller_plugin_identity is None, skip identity check from langbot.pkg.agent.runner.session_registry import get_session_registry + registry = get_session_registry() resources = make_resources(models=[{'model_id': 'model_001'}])