Metadata-Version: 2.4
Name: metamynd-client
Version: 0.2.0
Summary: MetaMynd/AgentSafe authorize-gate client for Python — the supported reference implementation of MAGP §8.3
Author: MetaMynd
License-Expression: MIT
Project-URL: Homepage, https://metamynd.ai/developers/python
Project-URL: Documentation, https://metamynd.ai/developers/spec
Project-URL: Source, https://github.com/Metamynd/agentsafe-guard
Project-URL: Issues, https://github.com/Metamynd/agentsafe-guard/issues
Keywords: metamynd,agentsafe,governance,ai-agents,magp,authorization
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography
Dynamic: license-file

# metamynd-client

The MetaMynd/AgentSafe authorize-gate client for Python — the supported reference
implementation. MAGP is language-agnostic by design: the gate verifies an Ed25519
signature over a canonical string, and nothing about that is JavaScript-specific. This
is the proof, and the shortest path for a Python team that doesn't want to read the
[Node guard](https://www.npmjs.com/package/@metamynd/agentsafe-guard) to find out what
the protocol actually expects.

```bash
pip install metamynd-client
```

> **Upgrading from 0.1.0?** Do. 0.1.0 signs a *seven*-field message; the gate requires
> *eight* (`resource` was added), so every request from 0.1.0 is refused with
> `CLIENT_PROTOCOL_VERSION_UNSUPPORTED`. This release signs the eight-field message and is
> tested against the gate's own vectors and the real guard (see [Tests](#tests)).

```python
from metamynd_client import MetaMyndClient

client = MetaMyndClient.from_env()  # reads METAMYND_API / AGENT_DID / AGENT_KEY

verdict = client.authorize(
    "flight-purchase", 150,
    merchant="skyward-air",
    context={"tool": "book-flight", "riskLevel": "low"},
)

if verdict.permitted:
    ...  # run the action
elif verdict.decision == "escalate":
    # ESCALATE is a HOLD, not a denial. Wait for a human; act ONLY on may_proceed.
    state = client.wait_for_escalation(verdict.escalation_id, timeout=600)
    if state.may_proceed:
        ...
else:
    raise RuntimeError(f"refused: {verdict.reason_code}")
```

### Send an honest `riskLevel`

A rule that judges risk **escalates a request that carries none** (or an unrecognised one),
instead of allowing it: an agent that omits its risk is indistinguishable from one hiding it.
`guard_tool` never invents a risk for you, and the `"low"` in the examples is a **placeholder,
not an assessment**. A real integration states an honest one — or, better, doesn't depend on
the agent: the mandate's owner can set a `riskTier` no claim can lower (MAGP §6.3).

## Guard a tool

```python
from metamynd_client import MetaMyndClient, guard_tool, GovernanceBlocked

client = MetaMyndClient.from_env()

def book_flight(amount, merchant):
    ...  # your real implementation

governed_book_flight = guard_tool(
    client, "flight-purchase", book_flight,
    lambda amount, merchant: {
        "amount": amount, "merchant": merchant,
        "context": {"tool": "book-flight", "riskLevel": "low"},  # state an honest risk
    },
)

try:
    governed_book_flight(amount=150, merchant="skyward-air")
except GovernanceBlocked as refused:
    print(f"refused: {refused.verdict.reason_code}")
    # refused.verdict.escalation_id, for a hold: client.wait_for_escalation(...)
```

`map_args` returns `amount`, `currency`, `merchant`, `resource` and `context`. **Async tools are
supported**: hand `guard_tool` an `async def` and you get an `async def` back (the gate call runs in
a worker thread, so it never stalls your event loop). The original signature is preserved, so a
framework that builds the tool schema from it — the OpenAI Agents SDK, PydanticAI — sees exactly
the unguarded function. If the awaiting task is cancelled (a timeout) while the gate is answering, and
the gate permits, the hold that call created is voided rather than left reserving the budget.
Async generator tools are refused when you wrap them: one authorization covers one action.

An escalation can also end as `modified` — a reviewer changed the action instead of approving it as
asked. That stops the wait too, is never `may_proceed`, and `status.next_escalation_id` is the
follow-up hold for the modified action.

## Behind a gateway or MCP server

A service protected by [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway)
or an MCP server using `agentsafe-mcp-guard` doesn't take the agent's word: it re-verifies the
signed request against the agent's own policy. The client hands it over:

```python
import requests
from metamynd_client import governance_headers, guard_tool

def book_via_gateway(amount, merchant):
    # inside a guarded tool, governance_headers() is the x-magp-request header for THIS call
    return requests.post("https://gateway.example/book",
                         json={"amount": amount, "merchant": merchant},
                         headers=governance_headers()).json()

book = guard_tool(client, "flight-purchase", book_via_gateway, map_args)
```

Outside a guarded tool: `verdict.signed.headers()`. It is not a parameter of your function, so the
tool's signature is unchanged. Without the header the service refuses (`MISSING_GOVERNANCE`) — the
safe failure. The gateway also refuses arguments that differ from what was signed
(`PAYLOAD_NOT_BOUND`), so the amount you authorized is the amount that executes.

**After a human approves a held action**, the request you first signed is stale (a service refuses
one more than a few minutes old). Sign a fresh one carrying the approved authorization:

```python
signed = client.sign_request("flight-purchase", 150, merchant="skyward-air",
                             context={"riskLevel": "low"},
                             authorization_id=state.authorization_id)
requests.post(url, json=body, headers=signed.headers())
```

## Settle, release, look up

```python
client.capture(verdict.authorization_id, 150, booking_ref="PNR1")   # commit at the FULL amount
client.void(verdict.authorization_id, reason="not needed")           # release an UNCLAIMED hold
out = client.outcome(verdict.authorization_id)                       # did it happen? may I retry?
```

**Who settles.** The *service* that executed the action settles or releases the hold it claimed;
an agent normally doesn't. Once a service has claimed a hold, only that service can settle it
below the authorized amount or release it — the gate refuses the agent's attempt (`ok=False`,
with the reason), on purpose: otherwise an agent could wait for a purchase to happen and then take
its budget back. An agent can `capture` at the full amount, and `void` a hold nobody has claimed.

**What was authorized, what was settled, and how far to trust it.** `Outcome` reports `authorized_amount`
(kept after settlement — `None` means unknown, not zero), `settled_amount`, and `settlement_evidence`:
`unattested` (the full authorization, counted for a caller that is not the claiming service),
`unclaimed_lowered` (released below the authorization before anyone claimed it),
`counterparty_attested` (the claiming service said so), `independently_confirmed` (the payment facilitator
reported the same amount), `operator_resolved`, or `reconciled_at_authorized`. Where a facilitator is
configured, a service settling *below* the authorization must be confirmed by it — an unreachable or
silent facilitator refuses the lower figure rather than believing it (the service can still settle in full).

**Retry only when it is safe.** `Outcome` carries two flags: `nothing_executed` (nothing has run
*so far*) and `retry_safe` (nothing can run *later* either — only `expired` and `not_executed`).
Retry a purchase **only** on `retry_safe`. A `not_started` hold is not yet retry-safe (a service
queued behind a slow gateway can still claim it — `void` it first); `unknown` and `in_flight` are
ambiguous and are reconciled, never retried blindly.

## Examples

Six runnable examples in
[`docs/integration/examples`](https://github.com/Metamynd/agentsafe-guard) — each runs offline
against a test gate, and CI runs them: `langgraph_agent.py`, `openai_agents_agent.py`,
`crewai_agent.py`, `langchain_agent.py`, `pydantic_ai_agent.py`, and `plain_python_agent.py` (no
framework: the whole lifecycle — authorize, hold, gateway handoff, capture, void, outcome — in one
file). Frameworks are imported lazily, so an example still runs the governance without its
framework installed.

## Scope

This is the entry price, not a full SDK: it signs and submits authorize requests, returns the
verdict, wraps a tool (`guard_tool`, sync or async), hands the signed request to a gateway, follows
up an `escalate`, and settles or looks up a hold. Local bundle evaluation (deciding at the edge,
no network) and evidence inclusion-proof fetch exist in the protocol and in the
[Node guard](https://www.npmjs.com/package/@metamynd/agentsafe-guard) and are deliberately not
reimplemented here — see [the full comparison](https://metamynd.ai/developers/python).

## Tests

```bash
python -m metamynd_client --selftest        # offline: the details that cost a day each,
                                            # plus the whole client against an in-process gate stub
```

The repository's CI additionally checks the client against the **shared protocol vectors**
(`docs/protocol/authorize-vectors.json`, generated from the gate's own message builder and read by both
the gate's tests and this client's — so the two cannot drift apart again), runs every example, and has
the **real** `agentsafe-mcp-guard` and `agentsafe-http-gateway` verify what the client signs.

## Links

- [Full guide](https://metamynd.ai/developers/python)
- [Protocol spec](https://metamynd.ai/developers/spec) — the signed message is §8.3
- [Source](https://github.com/Metamynd/agentsafe-guard)

MIT licensed.
