Metadata-Version: 2.5
Name: truthlock-io
Version: 0.1.0
Summary: Client for the TruthLock verification API: submit, wait, decide, and verify signed webhooks.
Project-URL: Homepage, https://truthlock.io
Project-URL: Documentation, https://truthlock.io/developers
Author-email: Iron Rod Systems LLC <contact@truthlock.io>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: attestation,llm,truthlock,verification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

# truthlock-io

Python client and command line tool for [TruthLock](https://truthlock.io): standing
verification for AI responses.

```
pip install truthlock-io
```

The package is `truthlock-io` and the module is `truthlock_io`. The similarly named
`truthlock` on PyPI belongs to a different company and does not talk to this service.

## Verify a response

```python
from truthlock_io import TruthLock

tl = TruthLock(api_key="tl_...", organization="field-service")

record = tl.verify_and_wait(
    question="How do I clear fault E-41 on the press?",
    response=answer_text,
)

if tl.may(record, "display"):
    show(answer_text)
elif tl.may(record, "display_with_caveat"):
    show(answer_text, caveats=record.caveats())
else:
    hold(record.review_ids())
```

The key and organization may also come from `TRUTHLOCK_API_KEY` and
`TRUTHLOCK_ORGANIZATION`. `TRUTHLOCK_URL` points the client at a dedicated stack;
the default is `https://app.truthlock.io`.

Verification takes seconds to a minute or two, so it is asynchronous. `verify` returns an
id at once. `wait` polls with a growing interval and raises `VerificationTimeout` if it
gives up; the verification carries on, and `get` finds it later.

| Call | What it does |
|---|---|
| `verify(response, question=, external_id=, operation=, webhook_url=, meta=)` | Submit. Returns `Accepted(id, status, status_url)`. |
| `get(id)` / `wait(id)` / `verify_and_wait(...)` / `recent(limit)` | The `Record`: `.grade`, `.claims`, `.caveats()`, `.review_ids()`, `.raw`. |
| `check(record, operation)` | The reliance `Decision`: `allow`, `review` or `deny`, per claim and overall. |
| `may(record, operation)` | `True` only for `allow`. |
| `respond(question, grounded=False)` | An answer from the organization's responder agent. |
| `attestations(id)` / `export(id, path)` | Whether the record is attested and stands, with its signed `chain`; or the whole bundle as a zip. |
| `webhook_deliveries(id)` | What was sent for the record, and what your receiver answered. |

Errors are typed: `AuthenticationError` (401, 403), `PaymentRequired` (402: the plan's
allowance is spent, or an invoice is past due), `RateLimited` (429, with `.retry_after`),
`NotFound`, and `ApiError` for the rest.

## Webhooks

Give an API key a webhook under **Advanced → API keys → Webhook**, or:

```
curl -X PUT https://app.truthlock.io/v1/api-keys/$KEY_ID/webhook \
  -H "X-API-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/truthlock"}'
```

The response carries the signing secret, `whsec_...`, once. When a verification submitted
with that key finishes, TruthLock posts a `verification.completed` or
`verification.failed` event, signed:

```
TruthLock-Signature: t=1700000000,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
```

Verify against the raw bytes, before parsing them:

```python
from truthlock_io import parse_webhook, WebhookSignatureError


@app.post("/hooks/truthlock")
async def hook(request):
    try:
        event = parse_webhook(
            await request.body(),
            request.headers.get("TruthLock-Signature"),
            WEBHOOK_SECRET,
        )
    except WebhookSignatureError:
        return Response(status_code=400)
    if event.completed:
        record = tl.get(event.verification_id)
    return Response(status_code=204)
```

`parse_webhook` rejects an altered body, another key's secret, and anything older than five
minutes, which is what stops a captured delivery being replayed. Answer with any 2xx
quickly and do the work afterwards: anything else is retried, eight times over about a day.
A delivery may arrive more than once; `event.id` is the same each time.

## Command line

```
$ export TRUTHLOCK_API_KEY=tl_...
$ truthlock verify --org field-service --file answer.txt --json | jq .overall.grade
"CONTRADICTED"
$ truthlock may 3f2a9b1c-... publish
review claims 1
$ truthlock attest 3f2a9b1c-... --export bundle.zip
```

| Command | |
|---|---|
| `verify --file F \| --text T [--question Q] [--no-wait] [--webhook-url U]` | Submit and wait. `-` reads standard input. |
| `get ID`, `wait ID` | Fetch, or wait for, a record. |
| `may ID OPERATION` | Prints the decision. Exit 0 only for `allow`. |
| `attest ID [--export ZIP]` | The attestation chain, or the bundle. |
| `respond QUESTION [--grounded]` | Ask the responder agent. |
| `deliveries ID` | Webhook deliveries for a record. |
| `webhook-verify --body F --signature H --secret S` | Check a captured delivery. |

Every command takes `--json`. Exit codes: 0 done or allowed, 1 a finding that says no
(failed run, decision other than allow, bad signature), 2 an error.
