Skip to content

Commit 9853831

Browse files
authored
feat(toolbox-llamaindex): Add client headers to Toolbox (#265)
* feat(toolbox-llamaindex): Add client headers to Toolbox * lint * rename package
1 parent 887b719 commit 9853831

File tree

5 files changed

+163
-12
lines changed

5 files changed

+163
-12
lines changed

packages/toolbox-llamaindex/README.md

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ applications, enabling advanced orchestration and interaction with GenAI models.
1818
- [Use with LlamaIndex](#use-with-llamaindex)
1919
- [Maintain state](#maintain-state)
2020
- [Manual usage](#manual-usage)
21+
- [Client to Server Authentication](#client-to-server-authentication)
22+
- [When is Client-to-Server Authentication Needed?](#when-is-client-to-server-authentication-needed)
23+
- [How it works](#how-it-works)
24+
- [Configuration](#configuration)
25+
- [Authenticating with Google Cloud Servers](#authenticating-with-google-cloud-servers)
26+
- [Step by Step Guide for Cloud Run](#step-by-step-guide-for-cloud-run)
2127
- [Authenticating Tools](#authenticating-tools)
2228
- [Supported Authentication Mechanisms](#supported-authentication-mechanisms)
2329
- [Configure Tools](#configure-tools)
@@ -33,14 +39,6 @@ applications, enabling advanced orchestration and interaction with GenAI models.
3339

3440
<!-- /TOC -->
3541

36-
## Installation
37-
38-
```bash
39-
pip install toolbox-llamaindex
40-
```
41-
42-
## Quickstart
43-
4442
Here's a minimal example to get you started using
4543
[LlamaIndex](https://docs.llamaindex.ai/en/stable/#getting-started):
4644

@@ -168,6 +166,86 @@ result = tools[0].call(name="Alice", age=30)
168166
This is useful for testing tools or when you need precise control over tool
169167
execution outside of an agent framework.
170168

169+
## Client to Server Authentication
170+
171+
This section describes how to authenticate the ToolboxClient itself when
172+
connecting to a Toolbox server instance that requires authentication. This is
173+
crucial for securing your Toolbox server endpoint, especially when deployed on
174+
platforms like Cloud Run, GKE, or any environment where unauthenticated access is restricted.
175+
176+
This client-to-server authentication ensures that the Toolbox server can verify the identity of the client making the request before any tool is loaded or called. It is different from [Authenticating Tools](#authenticating-tools), which deals with providing credentials for specific tools within an already connected Toolbox session.
177+
178+
### When is Client-to-Server Authentication Needed?
179+
180+
You'll need this type of authentication if your Toolbox server is configured to deny unauthenticated requests. For example:
181+
182+
- Your Toolbox server is deployed on Cloud Run and configured to "Require authentication."
183+
- Your server is behind an Identity-Aware Proxy (IAP) or a similar authentication layer.
184+
- You have custom authentication middleware on your self-hosted Toolbox server.
185+
186+
Without proper client authentication in these scenarios, attempts to connect or
187+
make calls (like `load_tool`) will likely fail with `Unauthorized` errors.
188+
189+
### How it works
190+
191+
The `ToolboxClient` (and `ToolboxSyncClient`) allows you to specify functions (or coroutines for the async client) that dynamically generate HTTP headers for every request sent to the Toolbox server. The most common use case is to add an Authorization header with a bearer token (e.g., a Google ID token).
192+
193+
These header-generating functions are called just before each request, ensuring
194+
that fresh credentials or header values can be used.
195+
196+
### Configuration
197+
198+
You can configure these dynamic headers in two ways:
199+
200+
1. **During Client Initialization**
201+
202+
```python
203+
from toolbox_llamaindex import ToolboxClient
204+
205+
client = ToolboxClient("toolbox-url", headers={"header1": header1_getter, "header2": header2_getter, ...})
206+
```
207+
208+
1. **After Client Initialization**
209+
210+
```python
211+
from toolbox_llamaindex import ToolboxClient
212+
213+
client = ToolboxClient("toolbox-url")
214+
client.add_headers({"header1": header1_getter, "header2": header2_getter, ...})
215+
```
216+
217+
### Authenticating with Google Cloud Servers
218+
219+
For Toolbox servers hosted on Google Cloud (e.g., Cloud Run) and requiring
220+
`Google ID token` authentication, the helper module
221+
[auth_methods](src/toolbox_core/auth_methods.py) provides utility functions.
222+
223+
### Step by Step Guide for Cloud Run
224+
225+
1. **Configure Permissions**: [Grant](https://cloud.google.com/run/docs/securing/managing-access#service-add-principals) the `roles/run.invoker` IAM role on the Cloud
226+
Run service to the principal. This could be your `user account email` or a
227+
`service account`.
228+
2. **Configure Credentials**
229+
- Local Development: Set up
230+
[ADC](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment).
231+
- Google Cloud Environments: When running within Google Cloud (e.g., Compute
232+
Engine, GKE, another Cloud Run service, Cloud Functions), ADC is typically
233+
configured automatically, using the environment's default service account.
234+
3. **Connect to the Toolbox Server**
235+
236+
```python
237+
from toolbox_core import auth_methods
238+
239+
auth_token_provider = auth_methods.aget_google_id_token # can also use sync method
240+
client = ToolboxClient(
241+
URL,
242+
client_headers={"Authorization": auth_token_provider},
243+
)
244+
tools = await client.load_toolset()
245+
246+
# Now, you can use the client as usual.
247+
```
248+
171249
## Authenticating Tools
172250

173251
> [!WARNING]

packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Any, Callable, Optional, Union
15+
from typing import Any, Awaitable, Callable, Mapping, Optional, Union
1616
from warnings import warn
1717

1818
from aiohttp import ClientSession
@@ -30,6 +30,9 @@ def __init__(
3030
self,
3131
url: str,
3232
session: ClientSession,
33+
client_headers: Optional[
34+
Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]]
35+
] = None,
3336
):
3437
"""
3538
Initializes the AsyncToolboxClient for the Toolbox service at the given URL.
@@ -38,7 +41,9 @@ def __init__(
3841
url: The base URL of the Toolbox service.
3942
session: An HTTP client session.
4043
"""
41-
self.__core_client = ToolboxCoreClient(url=url, session=session)
44+
self.__core_client = ToolboxCoreClient(
45+
url=url, session=session, client_headers=client_headers
46+
)
4247

4348
async def aload_tool(
4449
self,
@@ -185,3 +190,16 @@ def load_toolset(
185190
strict: bool = False,
186191
) -> list[AsyncToolboxTool]:
187192
raise NotImplementedError("Synchronous methods not supported by async client.")
193+
194+
def add_headers(
195+
self,
196+
headers: Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]],
197+
) -> None:
198+
"""
199+
Add headers to be included in each request sent through this client.
200+
Args:
201+
headers: Headers to include in each request sent through this client.
202+
Raises:
203+
ValueError: If any of the headers are already registered in the client.
204+
"""
205+
self.__core_client.add_headers(headers)

packages/toolbox-llamaindex/src/toolbox_llamaindex/client.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
from asyncio import to_thread
16-
from typing import Any, Callable, Optional, Union
16+
from typing import Any, Awaitable, Callable, Mapping, Optional, Union
1717
from warnings import warn
1818

1919
from toolbox_core.sync_client import ToolboxSyncClient as ToolboxCoreSyncClient
@@ -27,14 +27,19 @@ class ToolboxClient:
2727
def __init__(
2828
self,
2929
url: str,
30+
client_headers: Optional[
31+
Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]]
32+
] = None,
3033
) -> None:
3134
"""
3235
Initializes the ToolboxClient for the Toolbox service at the given URL.
3336
3437
Args:
3538
url: The base URL of the Toolbox service.
3639
"""
37-
self.__core_client = ToolboxCoreSyncClient(url=url)
40+
self.__core_client = ToolboxCoreSyncClient(
41+
url=url, client_headers=client_headers
42+
)
3843

3944
async def aload_tool(
4045
self,
@@ -287,3 +292,16 @@ def load_toolset(
287292
for core_sync_tool in core_sync_tools:
288293
tools.append(ToolboxTool(core_tool=core_sync_tool))
289294
return tools
295+
296+
def add_headers(
297+
self,
298+
headers: Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]],
299+
) -> None:
300+
"""
301+
Add headers to be included in each request sent through this client.
302+
Args:
303+
headers: Headers to include in each request sent through this client.
304+
Raises:
305+
ValueError: If any of the headers are already registered in the client.
306+
"""
307+
self.__core_client.add_headers(headers)

packages/toolbox-llamaindex/tests/test_async_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,22 @@ async def test_load_toolset_not_implemented(self, mock_client):
339339
assert "Synchronous methods not supported by async client." in str(
340340
excinfo.value
341341
)
342+
343+
@patch("toolbox_llamaindex.async_client.ToolboxCoreClient")
344+
async def test_init_with_client_headers(
345+
self, mock_core_client_constructor, mock_session
346+
):
347+
"""Tests that client_headers are passed to the core client during initialization."""
348+
headers = {"X-Test-Header": "value"}
349+
AsyncToolboxClient(URL, session=mock_session, client_headers=headers)
350+
mock_core_client_constructor.assert_called_once_with(
351+
url=URL, session=mock_session, client_headers=headers
352+
)
353+
354+
async def test_add_headers(self, mock_client):
355+
"""Tests that add_headers calls the core client's add_headers."""
356+
headers = {"X-Another-Header": lambda: "dynamic_value"}
357+
mock_client.add_headers(headers)
358+
mock_client._AsyncToolboxClient__core_client.add_headers.assert_called_once_with(
359+
headers
360+
)

packages/toolbox-llamaindex/tests/test_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,21 @@ async def test_aload_toolset_with_args(
422422
bound_params=bound_params,
423423
strict=True,
424424
)
425+
426+
@patch("toolbox_llamaindex.client.ToolboxCoreSyncClient")
427+
def test_init_with_client_headers(self, mock_core_client_constructor):
428+
"""Tests that client_headers are passed to the core client during initialization."""
429+
headers = {"X-Test-Header": "value"}
430+
ToolboxClient(URL, client_headers=headers)
431+
mock_core_client_constructor.assert_called_once_with(
432+
url=URL, client_headers=headers
433+
)
434+
435+
@patch("toolbox_llamaindex.client.ToolboxCoreSyncClient")
436+
def test_add_headers(self, mock_core_client_constructor):
437+
"""Tests that add_headers calls the core client's add_headers."""
438+
mock_core_instance = mock_core_client_constructor.return_value
439+
client = ToolboxClient(URL)
440+
headers = {"X-Another-Header": "dynamic_value"}
441+
client.add_headers(headers)
442+
mock_core_instance.add_headers.assert_called_once_with(headers)

0 commit comments

Comments
 (0)