Read-back recipes
Attest reads back with the credentials the agent already holds — in-process, never through the cloud.
at = Attest(readers={"gmail": gmail_service_or_token, "slack": web_client_or_token, "hubspot": client_or_token},
http_get=lambda url, params=None: session.get(url, params=params).json())
| system | write | read-back | compares |
|---|---|---|---|
| gmail | send / reply | messages.get |
SENT label, every intended recipient in To/Cc, subject, reply thread |
| gmail | create draft | drafts.get |
exists, recipients, subject |
| gmail | update labels | messages.get |
added ⊂ labels, removed ∩ labels = ∅ |
| slack | send | conversations.history / .replies |
ts, text, thread_ts |
| slack | create channel | conversations.info |
exists, name, is_private |
| hubspot | create / update any object | GET crm/v3/objects/{type}/{id} |
id, every intended property |
| calendar | create / update event | events.get |
confirmed, summary, start / end, attendees |
| drive | file / share | files.get / permissions.list |
name, parents, not trashed / email present, role |
| docs | create / append | documents.get |
title, appended text present |
| sheets | create / write values | spreadsheets.get / values.get |
title, every written row present |
| notion | page / database | pages.retrieve / databases.retrieve |
properties (title, status, select, text…), parent |
| linear | issue / project / comment | GraphQL | title, priority, state, assignee, team, body |
| outlook | send / event | sent-items search / me/events/{id} |
recipients, subject / start, attendees |
| teams | send | channel / chat message | text |
| any REST API | create / update | convention GET <url>/<id> |
id, every intended field the record carries |
| any REST API with a spec | create / update | OpenApiDriver(spec, http_get) |
spec-derived read path; never guesses |
| any MCP server | create / update | tool pair create_X ⇒ get_X |
id, overlapping fields |
Readers accept the vendor SDK object (duck-typed, no vendor import), a bearer token (stdlib HTTP), or any
fetch(path, params) callable. Every reader also accepts a fetch(path, params) callable, so any HTTP client (or a test double) works.
Live verification against real accounts
tests/live proves the ladder against real Gmail, Slack, HubSpot, Notion and Linear — including the case that
matters most: an action the API accepted whose record contradicts the intent must come back unverified.
export ATTEST_LIVE_GMAIL_TOKEN=… ATTEST_LIVE_SLACK_TOKEN=… ATTEST_LIVE_SLACK_CHANNEL=C…
pytest tests/live -q --live-report # only what you have credentials for; prints the ledger it produced
Each system has a positive test (record matches ⇒ verified) and a contradiction test (claim ≠ record ⇒
unverified). Tokens stay in your shell: the suite reads back with the same token it writes with.