diff --git a/src/langbot/pkg/api/application/execution.py b/src/langbot/pkg/api/application/execution.py index 787392633..144d6a25c 100644 --- a/src/langbot/pkg/api/application/execution.py +++ b/src/langbot/pkg/api/application/execution.py @@ -684,8 +684,12 @@ class ExecutionAPI: if not isinstance(context, ExecutionAPIContext) or not isinstance(context.binding, InstallationBinding): _fail('invalid_authentication', 'A trusted installation binding is required', 401) if context.run_id is not None and (not isinstance(context.run_id, str) or not context.run_id.strip()): + if context.from_handler and operation.scope == 'run': + _fail('permission_denied', 'run_id is required', 400) _fail('invalid_authentication', 'The trusted run ID is invalid', 401) if context.run_id is None and operation.scope == 'run': + if context.from_handler: + _fail('permission_denied', 'run_id is required', 400) _fail('permission_denied', 'This operation requires a trusted run context', 403) if context.run_id is not None and operation.scope == 'plugin': _fail('permission_denied', 'A run cannot use legacy plugin-wide operations', 403) diff --git a/tests/integration/plugin/test_execution_api_routing.py b/tests/integration/plugin/test_execution_api_routing.py index c644695df..c7375e20b 100644 --- a/tests/integration/plugin/test_execution_api_routing.py +++ b/tests/integration/plugin/test_execution_api_routing.py @@ -506,3 +506,11 @@ async def test_sdk_0_7_19_call_tool_parameters_and_session_compatibility(test_ho ) assert resp.code == 0 assert resp.data == {'result': 'tool-result'} + + +@pytest.mark.asyncio +async def test_run_scoped_action_missing_run_id_returns_run_id_is_required(test_host): + """When a run-scoped action is called from handler without run_id, error message is 'run_id is required'.""" + resp = await test_host.handler.actions['state_get']({'scope': 'conversation', 'key': 'test_key'}) + assert resp.code != 0 + assert 'run_id is required' in resp.message