Quickstart — 5 minutes, no cloud
pip install attestlayer
1. Wrap one function
import attest
@attest.action(system="gmail", verb="send", target="to")
def send_email(to, subject, body):
return gmail.users().messages().send(userId="me", body=msg).execute()
send_email("arun@newco.com", "Follow-up", "…")
The first external send pauses: the console asks y / n / e. Approve it.
2. Look at the ledger
attest ledger
attest verify # hash chain intact
#1 2026-09-15 10:02:11 gmail.send → arun@newco.com ask approved acknowledged 5b1c…
acknowledged means the API returned an id and nothing was read back. Give Attest the client the agent
already holds and it reads the message back and compares recipients, subject and the SENT label:
at = attest.Attest(readers={"gmail": gmail}) # or an OAuth token string
#2 … gmail.send → arun@newco.com ask approved verified …
3. Set one policy
attest.yaml:
policies:
- match: { verb: [send, share], target: external }
decision: ask
- match: { verb: [delete, pay] }
decision: ask
- match: { system: hubspot, verb: update }
decision: act
4. Don't label anything
@attest.action # send_email ⇒ unknown / send
def send_email(to, body): ...
@attest.action(method="POST", url="https://api.someweirdcrm.io/v2/leads") # ⇒ someweirdcrm / create
def create_lead(name, email): ...
An app Attest has never seen is still recorded, decided, gated, and — with http_get= — read back by
convention (GET /v2/leads/{id}) and verified. Run examples/unknown_app.py to see all three rungs.
5. Whole agent, one line
=== "LangGraph"
from attest.adapters import langgraph as attest_lg
tools = attest_lg.wrap_tools(tools, at, mapping={"send_email": {"system": "gmail", "verb": "send", "target": "to"}})
from attest.adapters import openai_agents as attest_oa
tools = attest_oa.wrap_tools(tools, at, mapping=mapping)
{ "mcpServers": { "gmail": { "command": "attest-mcp",
"args": ["--upstream", "npx -y @modelcontextprotocol/server-gmail", "--server", "gmail"] } } }
Nothing about how you built the agent changed.