MAQPNADocs

Run your own agent under maqpna dev run

Point a Python, TypeScript or Go agent at the local MAQPNA gateway with the SDK, handle denials and approvals, and report the session result.

flowchart LR
    A["maqpna dev up"] --> B["maqpna dev run -- CMD"]
    B -->|"MAQPNA_GATEWAY_URL<br/>MAQPNA_TOKEN<br/>MAQPNA_TOOL_*_URL"| C["your agent<br/>(SDK)"]
    C -->|"tools/call"| D["MAQPNA gateway"]
    D --> E["MCP servers"]
    C -->|"report_result"| D
    D --> F["timeline and audit ledger"]

Goal#

Run an agent you wrote under a local MAQPNA so that every tool call it makes goes through the gateway, exactly as it would in a sandbox in a cluster. The same code then runs unchanged in a cluster, because the operator gives each sandbox the same environment variables that maqpna dev run sets.

Run your own Python agent under maqpna dev run.cast

Prerequisites#

  • A local MAQPNA from Your first governed agent: maqpna dev up in your working directory.
  • A checkout of the MAQPNA repository. The SDKs are not published to PyPI, npm or a Go proxy yet, so you install them from sdk/.
  • For Python: Python 3.10 or later. For TypeScript: Node.js 20.3 or later and npm. For Go: Go 1.24 or later.

What maqpna dev run gives your agent#

maqpna dev run [flags] -- CMD [ARG]... mints a session token from the local identity broker and runs CMD with these variables. The exit status is your command's.

Variable Value
MAQPNA_GATEWAY_URL http://127.0.0.1:8080
MAQPNA_TOKEN A session token for agent coder, namespace dev, on behalf of you@localhost. In a cluster the operator mounts it as a file instead (MAQPNA_TOKEN_FILE, default /var/run/maqpna/token).
MAQPNA_SESSION, MAQPNA_AGENT The session and agent names.
MAQPNA_TOOL_<NAME>_URL {gateway}/mcp/<name> for each MCP server, for example MAQPNA_TOOL_ECHO_URL.
OPENAI_BASE_URL, OPENAI_API_KEY, MAQPNA_MODEL_NAME Only with maqpna dev up --stub-llm: the gateway's model route stub and the session token as the API key, so OpenAI-SDK agents work unchanged.

Flags of maqpna dev run change the identity in the token: --agent, --namespace, --user, --session, --scope (repeatable; default tools:<each MCP server> and models:*) and --ttl (default 1h). Policies match on these values, so use them to test a policy for a specific agent or namespace.

Steps: Python#

1. Install the SDK#

python3 -m venv .venv && . .venv/bin/activate
pip install ./maqpna/sdk/python          # path to sdk/python in your checkout

2. Write the agent#

Save this as agent.py:

from maqpna import MaqpnaClient, PolicyDenied, Result

client = MaqpnaClient.from_env()          # reads MAQPNA_GATEWAY_URL, MAQPNA_TOKEN, ...
echo = client.tools("echo")               # the MCP server "echo", through the gateway

print("tools:", [t.name for t in echo.list_tools()])
print("echo:", echo.call("echo", text="hello from my agent").text)

try:
    echo.call("delete_resource", id="db-1", namespace="kube-system")
except PolicyDenied as e:
    print("denied:", e.rule, "/", e.reason)

client.report_result(Result.SUCCEEDED, "echoed; the kube-system delete was denied")
  • list_tools() returns only the tools that policy lets this session see.
  • A denied call raises PolicyDenied with the rule and reason. Catch it and let your agent change its plan; the tool never ran.
  • report_result records how the run ended. It appears in the timeline and, in a cluster, as the session's result.

3. Run it#

maqpna dev run -- python agent.py
maqpna dev: session dev-dc659728 (agent coder, namespace dev, on behalf of you@localhost)
tools: ['echo', 'get_time', 'delete_resource']
echo: hello from my agent
denied: never-touch-system-namespaces / policy_denied
maqpna dev: dev-dc659728: 1 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)

4. See what it did#

maqpna dev timeline --last
TIME      KIND       SERVER/TOOL                   DECISION  DETAIL
00:11:43  tool_call  echo/echo                     allow     default_action
00:11:43  tool_call  echo/delete_resource          deny      system namespaces are off-limits to agents
00:11:43  result     maqpna-session/report_result  observe   session_result:Succeeded
dev-dc659728: 1 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)

5. Watch it live#

MAQPNA Desk's Local (dev) window starts and stops the local MAQPNA and follows the timeline of the last maqpna dev run session, or any other, as it happens. Run maqpna desk:

MAQPNA Desk Local (dev) window: the local MAQPNA is running, and the timeline of session fix-issue-4821 lists model calls and tool calls with their decisions

Light theme.

Steps: TypeScript#

Build the SDK in your checkout and install the packed tarball into your agent's project:

cd maqpna/sdk/typescript && npm ci && npm run build && npm pack    # writes maqpna-sdk-0.1.0.tgz
cd ~/my-agent && npm install ../maqpna/sdk/typescript/maqpna-sdk-0.1.0.tgz

agent.mjs:

import { MaqpnaClient, PolicyDenied } from "@maqpna/sdk";

const client = MaqpnaClient.fromEnv();
const echo = client.tools("echo");

console.log("echo ->", (await echo.call("echo", { text: "hello from TypeScript" })).text);
try {
  await echo.call("delete_resource", { id: "db-1", namespace: "kube-system" });
} catch (e) {
  if (e instanceof PolicyDenied) console.log("denied:", e.rule, e.reason);
  else throw e;
}
await client.reportResult("Succeeded", "echo ok; kube-system delete denied");
maqpna dev run -- node agent.mjs

The output has the same shape as the Python run: one allowed call, one denial with rule never-touch-system-namespaces and reason policy_denied. The full API is in the TypeScript SDK page.

Steps: Go#

The Go SDK uses only the standard library. It is its own module in sdk/go; until it is published, point your module at the checkout with a replace directive (see Go SDK). The example in the checkout runs as is:

maqpna dev run -- go -C sdk/go run ./examples/agent
maqpna dev: session dev-eee5a78c (agent coder, namespace dev, on behalf of you@localhost)
session dev-eee5a78c, tool servers [echo]
echo -> hello from the Go SDK
delete_resource in kube-system -> denied by policy: denied by policy baseline-guardrails rule never-touch-system-namespaces: system namespaces are off-limits to agents rule=never-touch-system-namespaces reason=policy_denied
reported Succeeded (annotated=false)
maqpna dev: dev-eee5a78c: 1 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)

The core calls are maqpna.NewFromEnv(ctx), client.CallTool(ctx, "echo", "echo", args) and client.ReportResult(ctx, maqpna.Succeeded, summary). Check denials with errors.As(err, &denied) where denied is a *maqpna.PolicyDenied.

Steps: any other language#

The gateway speaks MCP Streamable HTTP and an OpenAI-compatible model API, so any client works. Send the session token as a bearer token. This shell agent (examples/stub-llm/agent.sh in the checkout) uses only curl and jq:

maqpna dev up --stub-llm
maqpna dev run -- examples/stub-llm/agent.sh
maqpna dev: session fix-issue-4821 (agent coder, namespace dev, on behalf of you@localhost)
tool echo -> "hello from stub-llm"
tool delete_resource -> "denied by policy baseline-guardrails rule never-touch-system-namespaces: system namespaces are off-limits to agents"
model: Done: echoed a greeting; the delete in kube-system was refused by policy.
maqpna dev: fix-issue-4821: 4 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)

The request it sends for a tool is:

curl -sS "$MAQPNA_TOOL_ECHO_URL" -H "Authorization: Bearer $MAQPNA_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}'

Handle approvals in your code#

A call that a rule holds for approval either blocks until someone decides (sync mode, the default) or returns at once with JSON-RPC -32002 and an approval ID (async mode). In Python:

from maqpna import MaqpnaClient, PolicyDenied, wait_for_approval

client = MaqpnaClient.from_env()
try:
    result = wait_for_approval(client.tools("echo"), "delete_resource",
                               {"id": "db-1", "namespace": "dev"}, poll_interval=2, timeout=120)
    print("approved:", result.text)
except PolicyDenied as e:
    print("denied:", e.reason)          # approval_denied or approval_expired

Approve it from a second terminal with maqpna approvals approve ID --approver reviewer@localhost, from MAQPNA Desk or from the console.

Verify#

  • maqpna dev run printed a summary line such as 1 allow, 1 deny.
  • maqpna dev timeline --last shows your calls and a result row when you called report_result.
  • maqpna audit verify .maqpna/audit.jsonl prints OK.

Troubleshooting#

Symptom Cause Fix
ConfigError (Python) or a thrown ConfigError (TypeScript) about MAQPNA_GATEWAY_URL The agent ran outside maqpna dev run. Run it as maqpna dev run -- CMD, or export the variables yourself.
ConfigError from client.model_endpoint() maqpna dev run does not set MAQPNA_MODEL_ENDPOINT. Pass the route name, client.model_endpoint("stub"), or use OPENAI_BASE_URL (set with --stub-llm).
Unauthorized / JSON-RPC -32003 The session token expired (--ttl, default 1 hour) or was revoked. Start a new maqpna dev run.
TransportError while a call waits for approval The client's 120-second request timeout is shorter than the approval timeout. Raise the timeout or use async mode.
Your tool is missing from list_tools() Policy hides tools the session may not call, and tool pinning hides drifted tools. Check with maqpna policy test and maqpna mcp tools dev/<server> (see MCP servers and tool pinning).
pip install maqpna or npm install @maqpna/sdk fails The SDKs are not published yet. Install from the checkout as shown above.

Next steps#