Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to `intuno-sdk` are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] — 2026-04-23

### Added

- **Service delegation.** `IntunoClient` and `AsyncIntunoClient` accept
an optional `act_as_user_id` kwarg. When set, the client sends
`X-Service-Key: <api_key>` + `X-On-Behalf-Of: <uuid>` instead of the
normal user auth headers. The backend attributes every call to the
delegated user.

Intended for internal services (e.g. `wisdom-agents` hosting
multi-tenant entities) that need to make network / registry / broker
calls on behalf of specific users without holding those users' API
keys. Not for end-user SDK code.

```python
from uuid import UUID
from intuno_sdk import AsyncIntunoClient

client = AsyncIntunoClient(
api_key=settings.AGENTS_SERVICE_API_KEY, # the service secret
act_as_user_id=UUID("..."), # the entity's owner
)
```

## [0.4.0] — 2026-04-22

### Added
Expand Down Expand Up @@ -102,6 +127,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
(`list_conversations`, `get_messages`), and LangChain / OpenAI
integration helpers.

[0.5.0]: https://github.com/IntunoAI/intuno-sdk/releases/tag/v0.5.0
[0.4.0]: https://github.com/IntunoAI/intuno-sdk/releases/tag/v0.4.0
[0.3.0]: https://github.com/IntunoAI/intuno-sdk/releases/tag/v0.3.0
[0.2.2]: https://github.com/IntunoAI/intuno-sdk/releases/tag/v0.2.2
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "intuno-sdk"
version = "0.4.0"
version = "0.5.0"
description = "The official Python SDK for the Intuno Agent Network."
authors = ["Alquify Inc. <hello@alquify.com>"]
license = "Apache-2.0"
Expand Down
40 changes: 35 additions & 5 deletions src/intuno_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from uuid import UUID as UUIDType

import httpx
from pydantic import ValidationError
Expand Down Expand Up @@ -29,12 +30,25 @@
)


def _build_auth_headers(api_key: str) -> dict:
"""Build auth headers, supporting both API keys and JWT bearer tokens."""
def _build_auth_headers(
api_key: str,
act_as_user_id: Optional[Union[str, "UUIDType"]] = None,
) -> dict:
"""Build auth headers.

- Plain user key / JWT: sends ``X-API-Key`` or ``Authorization: Bearer``.
- Service delegation (``act_as_user_id`` set): sends ``X-Service-Key``
+ ``X-On-Behalf-Of`` instead. The ``api_key`` argument is the
service secret in this mode.
"""
headers = {
"Content-Type": "application/json",
"User-Agent": f"Intuno-SDK/{SDK_VERSION}",
}
if act_as_user_id is not None:
headers["X-Service-Key"] = api_key
headers["X-On-Behalf-Of"] = str(act_as_user_id)
return headers
# JWT tokens start with 'eyJ' (base64-encoded JSON header)
if api_key.startswith("eyJ"):
headers["Authorization"] = f"Bearer {api_key}"
Expand All @@ -46,23 +60,34 @@ def _build_auth_headers(api_key: str) -> dict:
class IntunoClient:
"""
The main synchronous client for interacting with the Intuno Agent Network.

Service delegation
------------------
Pass ``act_as_user_id`` to operate on behalf of a specific user. The
``api_key`` you provide is then used as a *service secret* — sent as
``X-Service-Key`` alongside ``X-On-Behalf-Of`` — and the backend
attributes every call to the delegated user. Only internal services
(wisdom-agents today) should use this mode.
"""

def __init__(
self,
api_key: str,
base_url: str = DEFAULT_BASE_URL,
timeout: float = 30.0,
*,
act_as_user_id: Optional[Union[str, UUIDType]] = None,
):
if not api_key:
raise APIKeyMissingError()

self.api_key = api_key
self.base_url = base_url
self.act_as_user_id = act_as_user_id
self._http_client = httpx.Client(
base_url=self.base_url,
timeout=timeout,
headers=_build_auth_headers(api_key),
headers=_build_auth_headers(api_key, act_as_user_id=act_as_user_id),
)

def discover(self, query: str, limit: int = 10) -> List[Agent]:
Expand Down Expand Up @@ -1038,23 +1063,28 @@ def __exit__(self, exc_type, exc_val, exc_tb):
class AsyncIntunoClient:
"""
The main asynchronous client for interacting with the Intuno Agent Network.

Service delegation: see ``IntunoClient`` — same ``act_as_user_id`` kwarg.
"""

def __init__(
self,
api_key: str,
base_url: str = DEFAULT_BASE_URL,
timeout: float = 30.0,
*,
act_as_user_id: Optional[Union[str, UUIDType]] = None,
):
if not api_key:
raise APIKeyMissingError()

self.api_key = api_key
self.base_url = base_url
self.act_as_user_id = act_as_user_id
self._http_client = httpx.AsyncClient(
base_url=self.base_url,
timeout=timeout,
headers=_build_auth_headers(api_key),
headers=_build_auth_headers(api_key, act_as_user_id=act_as_user_id),
)

async def discover(self, query: str, limit: int = 10) -> List[Agent]:
Expand Down
2 changes: 1 addition & 1 deletion src/intuno_sdk/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
DEFAULT_BASE_URL = "https://api.intuno.net"
SDK_VERSION = "0.4.0"
SDK_VERSION = "0.5.0"
51 changes: 51 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,57 @@ def test_init_requires_api_key():
AsyncIntunoClient(api_key="")


# --- Service delegation (act_as_user_id) ---


def _header_keys(client):
return {k.lower() for k in client._http_client.headers.keys()}


def test_normal_mode_sends_x_api_key():
c = IntunoClient(api_key="wsk_user_key")
keys = _header_keys(c)
assert "x-api-key" in keys
assert "x-service-key" not in keys
assert "x-on-behalf-of" not in keys


def test_delegation_sends_service_key_and_on_behalf_of():
from uuid import UUID

user_id = UUID("11111111-1111-4111-8111-111111111111")
c = IntunoClient(api_key="service_secret", act_as_user_id=user_id)
keys = _header_keys(c)
assert "x-service-key" in keys
assert "x-on-behalf-of" in keys
# Normal auth header is NOT sent in service mode
assert "x-api-key" not in keys
assert "authorization" not in keys
# UUID stringified
assert c._http_client.headers["X-On-Behalf-Of"] == str(user_id)
# Stored attribute for introspection
assert c.act_as_user_id == user_id


def test_delegation_accepts_string_user_id():
c = IntunoClient(
api_key="service_secret",
act_as_user_id="22222222-2222-4222-8222-222222222222",
)
assert c._http_client.headers["X-On-Behalf-Of"] == "22222222-2222-4222-8222-222222222222"


def test_async_delegation_sends_service_key_and_on_behalf_of():
from uuid import UUID

user_id = UUID("33333333-3333-4333-8333-333333333333")
c = AsyncIntunoClient(api_key="service_secret", act_as_user_id=user_id)
keys = _header_keys(c)
assert "x-service-key" in keys
assert "x-on-behalf-of" in keys
assert "x-api-key" not in keys


# --- Synchronous Client Tests ---


Expand Down