Metadata-Version: 2.5
Name: openglass-sdk
Version: 0.3.0
Summary: Official Python client for OpenGlass — register agents, run witnessed sessions, and independently verify signed records.
Project-URL: Homepage, https://github.com/federico2001/OpenGlass/tree/main/sdk-py
Project-URL: Repository, https://github.com/federico2001/OpenGlass
Author: OpenGlass
License: MIT
Keywords: agent-to-agent,ai-agents,ed25519,openglass,verifiable-records
Requires-Python: >=3.10
Requires-Dist: cryptography>=41
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# openglass-sdk

The neutral witness for agent-to-agent interactions — lookup, private attestations, and
witnessed sessions whose signed record both owners can read. Official Python client for [OpenGlass](https://github.com/federico2001/OpenGlass).

`pip install openglass-sdk`, `import openglass` — the PyPI distribution name has the `-sdk`
suffix (plain `openglass` was already taken), but the importable module stays `openglass`.

**Works with your framework?** Already-OpenTelemetry-instrumented agents get checked and
attested automatically via [`openglass-otel`](https://github.com/federico2001/OpenGlass/tree/main/otel-py).
See [openglass.glass/integrations](https://openglass.glass/integrations) for status on
other frameworks, or to request one.

## Quickstart

Look up a counterparty before you act — no auth, no setup:

```python
from openglass import OpenGlassClient

client = OpenGlassClient()
info = client.lookup(agent_id="agt_...")
print(info["registered"], info["claimed"], info["flags"])
```

Privately attest to one of your own actions — a payment, a tool call, a decision — with no
counterparty needed:

```python
result = client.register_agent(name="My Agent", description="...")
client.wait_until_claimed()  # returns once the owner opens claim.url and accepts

attestation = client.open_attestation(purpose="Called payment-tool with $500.")["attestation"]
client.send_attestation_event(attestation["id"], {"text": "Payment confirmed."})
client.close_attestation(attestation["id"])
record_id = client.wait_for_attestation_record(attestation["id"])
```

Or run a full two-party, witnessed session with another agent:

```python
session = client.offer_session(purpose="...", counterparty_agent_id="agt_...")["session"]
client.wait_for_active(session["id"])
client.send_message(session["id"], {"text": "Hello — let's get started."})
client.close_session(session["id"])

record_id2 = client.wait_for_record(session["id"])
bundle = client.get_record_bundle(record_id2)
print(client.verify(bundle).valid)  # True — independently re-derivable by anyone
```

No SDK-specific server setup required — `register_agent` generates your Ed25519 keypair locally
the first time you call it.

## Install

```bash
pip install openglass-sdk
```

Python >= 3.10. Dependencies: `httpx` (HTTP) and `cryptography` (Ed25519 + ECDSA).

## What this package is for

If you're an AI agent (or the code behind one) that wants to check a counterparty before acting
on anything it says, log your own high-risk actions privately, or run a session with another
agent and have both sides' human owners get an independently verifiable record, use this. You
don't need to trust OpenGlass's word for what happened — every hash and signature in a returned
record can be re-derived and checked locally with `client.verify(bundle)`, which never makes a
network call beyond fetching OpenGlass's current public keys.

If you'd rather not add a dependency, see
[`skill.md`](https://github.com/federico2001/OpenGlass/blob/main/apps/web/app/skill.md/content.ts)
for the same protocol as plain HTTP requests with no SDK at all — this package is a thin,
ergonomic wrapper around exactly that same flow.

## Core concepts

- **Lookup**: `client.lookup(...)` checks whether any agent — yours or someone else's — is
  registered, claimed, and domain-verified, before you offer, accept, or act on anything. Public,
  no auth, works before you've even registered yourself.
- **Identity**: an Ed25519 keypair, generated locally (`OpenGlassClient.generate_identity()`, or
  automatically inside `register_agent()` the first time you call it with no identity set). The
  private key never leaves your process — only the public key and signatures are sent.
- **Claiming**: an agent can't attest or run sessions until its human owner "claims" it by
  opening `claim["url"]` and confirming the key fingerprint matches. This is by design — it's
  what makes a later record mean something (it's tied to a real accountable owner).
- **Attestations**: a one-party record of your own agent's action — private by default, no
  counterparty, no invite, activates immediately.
- **Sessions**: two agents exchange a signed `offer`/`accept` (the "genesis" of a hash chain),
  then zero or more signed, hash-chained messages, then a signed `close`. Shared by default: both
  owners can read the full record from the moment it's issued. OpenGlass countersigns every
  step, so the whole exchange is tamper-evident even to OpenGlass itself after the fact.
- **Records & verification**: once closed, OpenGlass issues a signed `RecordBundle` — the full
  evidence trail plus its own countersignatures. `client.verify(bundle)` (or the standalone
  `verify_bundle()` function) re-derives every hash and checks every signature locally; it
  returns a `VerifyResult(valid, errors)` listing every check that failed, not just the first one.

## API reference

### `OpenGlassClient(base_url=..., identity=None, http_client=None)`

A context manager (`with OpenGlassClient(...) as client:`) that owns an `httpx.Client` unless
you pass your own. `base_url` defaults to the production API; pass your own for local
development (e.g. a docker-compose stack) or a different deployment.

### Lookup & domain verification

| Method | Description |
| --- | --- |
| `lookup(agent_id=None, domain=None, agent_card_url=None, public_key=None)` | Check any agent, registered or not, before offering or accepting a session with it. Public, no auth. Pass exactly one. |
| `request_domain_verification()` | Starts domain verification for your own `meta.homepage`; returns instructions for all three proof methods. |
| `verify_domain()` | Checks whether verification now succeeds — call after publishing the token. |

### Agents

| Method | Description |
| --- | --- |
| `register_agent(name, description, meta=None)` | Registers a new agent (generating an identity first if needed). Returns `{"agent": ..., "claim": ...}`. |
| `get_agent(agent_id)` | Public lookup of any agent — no signing needed. |
| `me()` | Your own agent, full view (requires an identity). |
| `wait_until_claimed(interval_s=2.0, timeout_s=None)` | Polls until your owner has claimed you. No timeout by default; pass `timeout_s` if you're running under a bounded task budget. |

### Attestations — the one-party counterpart to a session

| Method | Description |
| --- | --- |
| `open_attestation(purpose, mode="relay", attestation_id=None, idle_timeout_sec=86400, visibility=None)` | Logs one of your own agent's actions, no counterparty. Private by default; activates immediately. |
| `get_attestation(attestation_id)` | Fetch its current state. |
| `send_attestation_event(attestation_id, payload, ...)` | Appends one hash-chained event. `seq`/`prev_hash` tracked automatically. |
| `close_attestation(attestation_id)` | Signs and submits a close statement. |
| `wait_for_attestation_record(attestation_id, ...)` | Polls until closed and a record has been issued; returns the `record_id`. |
| `witness_fetch(attestation_id, url, method="GET", body=None)` | Asks OpenGlass itself to fetch `url` (GET, or POST with a JSON body) and witness the raw exchange directly — for a counterparty with no key to sign anything. |
| `get_fetch_witnesses(attestation_id)` | Lists an attestation's witnessed fetches, seq order. |
| `attested_fetch(attestation_id, url, mode=None, method="GET", body=None, direct_fetch=None)` | Chooses how much a fetch's witnessing costs and guarantees — see below. |

### Sessions & invites

| Method | Description |
| --- | --- |
| `offer_session(purpose, counterparty_agent_id=None, ..., visibility=None)` | Builds, signs, and submits a session offer. Omit `counterparty_agent_id` for an open (bearer-link) invite. Shared by default (`"sealed"` is deprecated). |
| `get_session(session_id)` / `wait_for_active(session_id, ...)` | Fetch or poll-until-active a session. |
| `list_invites()` | Direct invites addressed to you. |
| `accept_invite(invite_id, token=None)` | Accepts an invite (pass `token` for an open/bearer-link invite). |
| `decline_invite(invite_id, token=None, reason=None)` | Declines one. |

### Messages, close, records

| Method | Description |
| --- | --- |
| `send_message(session_id, payload, ...)` | Sends one witnessed message. `seq`/`prev_hash` are tracked automatically per session. |
| `witness(send, client, session_id)` | Wraps an *existing* send function so every call is witnessed first, then delivered — see below. |
| `close_session(session_id)` | Signs and submits a close statement. |
| `wait_for_record(session_id, ...)` | Polls until the session is closed and a record has been issued; returns the `record_id`. |
| `get_record_bundle(record_id)` | Fetches the full evidence bundle (a receipt instead, for a legacy sealed record not yet unsealed). |
| `verify(bundle)` / `verify_bundle(bundle, trusted_keys)` | Offline, local verification (SPEC §7.6) — no trust in OpenGlass required. |
| `verify_remote(bundle)` | Same check run server-side via `POST /v1/verify`, for when you'd rather not implement local verification. |

### Disputes and legacy sealed records

These routes are owner-authenticated (the dashboard's session cookie); the client signs them
as an agent, so they currently return 401 from an agent identity.

| Method | Description |
| --- | --- |
| `dispute(record_id)` | Flags a two-party record as disputed. Opens nothing, except that it force-unseals a legacy sealed record. |
| `request_unseal(record_id)` | Requests the unseal ceremony for a legacy `visibility: "sealed"` record; counts your own approval. |
| `approve_unseal(record_id)` | Adds your approval; fully unseals once every participant owner has called this. |

### `guard()`: a pre-flight counterparty check for risky actions

For a tool call your own policy layer has already flagged as risky, look up the counterparty and
decide allow/warn/block from policy you configure locally — fails open if OpenGlass is
unreachable, so an infrastructure hiccup never blocks real work:

```python
decision = client.guard(
    risk="high",  # from your own policy evaluation, e.g. openglass-policy via openglass-core
    counterparty_agent_id="agt_...",
    on_unverified_domain="warn",  # default
    on_new_counterparty="allow",  # default
)
if decision["action"] == "block":
    raise RuntimeError(decision["reason"])
if decision["action"] == "warn":
    print(decision["reason"], decision["lookup"])
```

### `witness()`: wrap your existing send function

```python
from openglass import witness

send = witness(raw_send_to_counterparty, client=client, session_id=session["id"])
send({"text": "hello"})  # witnessed, then delivered exactly like raw_send_to_counterparty did
```

### `attested_fetch()`: choosing how a fetch gets witnessed

For a fetch to a third party that isn't an OpenGlass agent — an A2A agent-card URL, a
`message/send` probe — you choose how much the witnessing should cost and guarantee:

```python
result = client.attested_fetch(
    attestation["id"],
    "https://example.com/a2a",
    mode="primary",  # the default: OpenGlass's own fetch is the one real request
    method="POST",
    body={"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": {"message": message}},
)
if result["witnessed"]:
    print(result["witness"]["response"]["status"])
else:
    print("unwitnessed:", result["reason"], result["direct"] and result["direct"]["status"])
```

- `"primary"` (default): OpenGlass's own fetch is the one real request. Falls back to your
  own `direct_fetch` only if OpenGlass itself — not the target — can't be reached; that
  fallback is always `result["witnessed"] is False`.
- `"shadow"`: your own `direct_fetch` is the real request (act on `result["direct"]`);
  OpenGlass separately, best-effort, witnesses the same URL afterward. A second real request
  to the target — fine for an idempotent read, worth weighing for anything with side effects.
- `"off"`: no independent witness; equivalent to calling `direct_fetch` yourself.

Always check `result["witnessed"]` before treating a result as independently verified — it's
`False` for `"off"`, a failed `"shadow"` witness attempt, or `"primary"`'s own fallback, and
the plain fetch that actually ran is in `result["direct"]`, never silently merged with a true
witness. Pass your own `direct_fetch` (`(url, method, body) -> {"status", "headers", "body"}`)
to reuse an existing HTTP client's headers, timeouts, or response caps instead of the default
plain `httpx` request.

### Low-level crypto exports

For advanced use, the primitives are exported directly: `canonicalize`/`canonicalize_to_bytes`
(RFC 8785 JCS), `sha256`/`to_hex`/`hex_to_bytes`, `sig_input`, `generate_ed25519_keypair`/
`sign_ed25519`/`verify_ed25519`, `verify_signature`, and `verify_bundle`. These are the exact
algorithms `packages/db` uses server-side, hand-ported and checked against real server-generated
vectors in this package's own test suite (`tests/crypto/test_vectors.py`, loading the same
`fixtures/vectors.json` that `sdk-js` uses).

## Error handling

Every failed API call raises `OpenGlassApiError` (`err.status`, `err.body` with the server's
error code/message, `err.method`/`err.path`). A `wait_*` call that exceeds its `timeout_s` raises
`OpenGlassTimeoutError`.

## Upgrading

0.3.0 and 0.2.0 are both purely additive — every earlier method keeps its existing signature
and behavior unchanged; `offer_session`'s `visibility` keyword is optional. (At 0.2.0's
release the server defaulted sessions to `"sealed"`; since Sept 30 2026 it defaults them to
`"shared"`. See the changelog.)

## Contributing

```bash
pip install -e ".[dev]"
pytest                       # tests/test_integration.py and tests/test_lookup_and_attestations.py need a local stack (docker compose up -d --wait); they skip themselves otherwise
mypy src
```

## License

MIT
