Skip to content
Merged
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
4 changes: 2 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ in the same process without one overriding the other.
identical otherwise — only the name and import path changed.
2. **`client.me()` no longer exists on `AgentClient`.** Identity moved
to `UserClient.me()` and the response shape changed too
(`UserPrincipal` with `user_id` / `email` / `name` /
(`UserPrincipal` with `user_id` / `username` / `email` /
`principal_type` / `key_prefix`, replacing `MeResponse` with
`agent_id` / `agent_name`). Listing your agents now goes through
`UserClient.agents.list()` instead.
Expand Down Expand Up @@ -141,7 +141,7 @@ from xagent_sdk import UserClient

with UserClient() as user:
me = user.me()
print(f"user_id={me.user_id} email={me.email} name={me.name}")
print(f"user_id={me.user_id} username={me.username} email={me.email}")
```

Each call hits the backend; cache the value locally if you need it
Expand Down
12 changes: 7 additions & 5 deletions python/src/xagent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ class UserPrincipal:
is bound to.

``principal_type`` is the stable enum string ``"user"`` for the
surface ``UserClient`` covers. ``key_prefix`` is the public-safe
6-char handle (``xag_personal_<prefix>_...``) and is safe to log;
the secret half of the key is never returned by this endpoint.
surface ``UserClient`` covers. ``username`` is the user's login name.
``email`` is optional and ``None`` when the account has no email set.
``key_prefix`` is the public-safe 6-char handle
(``xag_personal_<prefix>_...``) and is safe to log; the secret half of
the key is never returned by this endpoint.
"""

principal_type: str
user_id: int
email: str
name: str
username: str
email: str | None
key_prefix: str


Expand Down
8 changes: 4 additions & 4 deletions python/src/xagent_sdk/user_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ def me(self) -> UserPrincipal:
"""``GET /v1/me`` -- identity probe for the personal key.

Zero side-effect. Returns the user principal the personal key
belongs to (``principal_type`` / ``user_id`` / ``email`` /
``name`` / ``key_prefix``). Use once at startup to log which
user is connected; cache the result locally if you only need it
once -- the SDK does not cache so a revoked key surfaces as
belongs to (``principal_type`` / ``user_id`` / ``username`` /
``email`` (nullable) / ``key_prefix``). Use once at startup to log
which user is connected; cache the result locally if you only need
it once -- the SDK does not cache so a revoked key surfaces as
``InvalidAPIKey`` immediately rather than silently using a
stale principal.
"""
Expand Down
5 changes: 3 additions & 2 deletions python/tests/e2e/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def test_user_me(user_client: UserClient) -> None:
assert isinstance(me, UserPrincipal)
assert me.principal_type == "user"
assert me.user_id > 0
assert me.email
assert me.name
assert me.username
# email is optional (null for accounts with none set)
assert me.email is None or isinstance(me.email, str)
assert me.key_prefix


Expand Down
26 changes: 25 additions & 1 deletion python/tests/unit/test_user_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,34 @@ def test_returns_user_principal(self) -> None:
assert isinstance(me, UserPrincipal)
assert me.principal_type == "user"
assert me.user_id == 123
assert me.username == "alex"
assert me.email == "user@example.com"
assert me.name == "Alex"
assert me.key_prefix == "abc123"

def test_me_email_may_be_null(self) -> None:
# The backend returns email=null for accounts with no email set;
# UserPrincipal.email is optional, so this must parse cleanly.
def handler(req: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"principal_type": "user",
"user_id": 1,
"username": "administrator",
"email": None,
"key_prefix": "abc123",
},
)

with UserClient(
personal_key="xag_personal_p_s",
base_url="https://test.example",
transport=httpx.MockTransport(handler),
) as c:
me = c.me()
assert me.username == "administrator"
assert me.email is None

def test_401_raises_invalid_api_key(self) -> None:
with (
UserClient(
Expand Down
2 changes: 1 addition & 1 deletion shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ implicit and documented below.

| File | Endpoint | Notes |
|---|---|---|
| `me_user.json` | `GET /v1/me` | User principal: `principal_type / user_id / email / name / key_prefix` |
| `me_user.json` | `GET /v1/me` | User principal: `principal_type / user_id / username / email (nullable) / key_prefix` |
| `templates_list.json` | `GET /v1/templates` | Bare JSON array; each entry keys its id under `id` (SDKs surface it as `template_id`) plus `name`, optional `description` |
| `templates_detail.json` | `GET /v1/templates/{id}` | Single object keyed by `id`; carries the merge-target `agent_config` dict |
| `agents_list.json` | `GET /v1/agents` | Bare JSON array; each entry keys its id under `id` (SDKs surface it as `agent_id`); covers `active`, `draft`, `paused` status values |
Expand Down
2 changes: 1 addition & 1 deletion shared/fixtures/v1/responses/me_user.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"principal_type": "user",
"user_id": 123,
"username": "alex",
"email": "user@example.com",
"name": "Alex",
"key_prefix": "abc123"
}
Loading