Metadata-Version: 2.4
Name: hallmark-identity
Version: 0.1.0
Summary: Open, IDP-agnostic agent-identity toolkit: agent + on-behalf-of tokens via RFC 8693.
Project-URL: Homepage, https://github.com/smithingdev/hallmark-identity-python
Project-URL: Repository, https://github.com/smithingdev/hallmark-identity-python
Project-URL: Issues, https://github.com/smithingdev/hallmark-identity-python/issues
Author: smithingdev
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# hallmark-identity (Python)

Python port of [Hallmark](https://github.com/smithingdev/hallmark-identity-node) —
open, IDP-agnostic identity for AI agents via OAuth 2.0 Token Exchange (RFC 8693).

Sync API, zero runtime dependencies, Python ≥3.10.

## Install

```bash
pip install hallmark-identity   # (PyPI publish pending)
```

## Quick start

```python
from hallmark_identity import create_identity, oidc

identity = create_identity(
    idp=oidc(issuer="https://your-idp/", client_id="agent", client_secret="…"),
)

# "Who am I?" — the agent's own machine identity (client-credentials grant)
mine = identity.agent().token(audience="https://api.internal")

# "Who am I acting for?" — on behalf of a user (you bring the user's token)
on_behalf = identity.on_behalf_of(user_token).token(
    audience="https://api.github.com", scopes=["repo"],
)

import urllib.request
req = urllib.request.Request(
    "https://api.github.com/user/repos",
    headers={"authorization": f"Bearer {on_behalf.raw}"},
)
```

## Supported IDPs

Any RFC 8693-capable OIDC provider via `oidc()`, plus a `keycloak()` convenience adapter:

```python
from hallmark_identity import oidc, keycloak

oidc(issuer="https://login.microsoftonline.com/<tenant>/v2.0", client_id=client_id, client_secret=client_secret)
keycloak(base_url="https://kc.example", realm="agents", client_id=client_id, client_secret=client_secret)
```

## API

| Export | Purpose |
| --- | --- |
| `create_identity(idp, store=None, refresh_skew_seconds=30, http_request=None)` | Factory. |
| `identity.agent().token(audience=None, scopes=None)` | The agent's own identity. |
| `identity.on_behalf_of(subject_token, type="access_token").token(...)` | On-behalf-of a user. |
| `oidc(...)` / `keycloak(...)` | IDP adapters. |
| `memory_store()` | Default in-memory `TokenStore`; implement `get`/`set` to plug in Redis, etc. |
| `parse_token`, `is_expired`, `will_expire_within` | Token-claim helpers. |
| `HallmarkError`, `GrantError`, `TokenExchangeUnsupportedError` | Typed errors. |

A returned `Token` exposes `.raw`, `.sub`, `.act`, `.aud`, `.scope`, `.exp` — `.raw` is the string sent as a bearer token.

## Development

```bash
uv sync                              # install deps
uv run pytest                        # unit tests
uv run pytest -c pytest-integration.ini   # opt-in, requires a running Keycloak — see tests/integration/README.md
uv run mypy src                      # typecheck
uv build                             # build sdist + wheel
```

## License

MIT
