Debug and observe
Find out what a session did and why a call was denied, with the timeline, the audit ledger, sandbox logs, events and OpenTelemetry GenAI traces.
When an agent misbehaves, you need two views: what the agent process did (its logs) and what the gateway decided (the audit ledger). MAQPNA keeps them apart on purpose: the logs are yours, the ledger is append-only and hash-chained. This page shows where each view lives, locally and in a cluster, and a step-by-step workflow for the most common question: why was this call denied?
flowchart TD
Q["A call was denied<br/>or behaved oddly"] --> E["1 Read error.data:<br/>policy, rule, reason"]
E --> R["2 Find the audit record<br/>timeline / audit tail"]
R --> P{"reason?"}
P -->|"policy_denied, default_action,<br/>no_applicable_policy"| PT["3a maqpna policy test --explain"]
P -->|revoked| RV["3b maqpna revocations list"]
P -->|budget_exceeded| BU["3c maqpna budgets list --exceeded"]
P -->|"dlp:*"| DL["3d record ext.dlp*, DLP profile"]
P -->|"tool_definition_changed"| PN["3e maqpna mcp tools --drift-only"]
P -->|"taint in rule"| TA["3f maqpna taint get"]
P -->|"missing_scope, token_*"| TK["3g maqpna token inspect"]
Where to look#
| Question | Local (maqpna dev) |
Cluster |
|---|---|---|
| What did the session do, step by step? | maqpna dev timeline --last or --session S |
maqpna session describe S -n NS, the console's session page |
| Every decision, with policy and rule | maqpna audit tail --session S |
maqpna audit tail --gateway URL --session S -n NS -f |
| What did the agent process print? | your terminal (it runs in the foreground) | maqpna logs S -n NS -f |
| Kubernetes events (sandbox, pods, tokens) | — | maqpna events S -n NS, with --audit to merge the gateway's records |
| CPU, memory, tokens and cost per session | maqpna session cost S |
maqpna top sessions -n NS --sort-by cost |
| Gateway and broker logs | .maqpna/logs/ |
kubectl logs -n maqpna-system deploy/maqpna-gateway |
| One request across agent, gateway and upstream | traceparent + maqpna audit tail --trace ID |
OpenTelemetry traces in your backend |
Load the local admin token first for the admin-API commands: eval "$(maqpna dev env)".
The timeline#
The timeline is the ledger-ordered view of one session: tool calls, model calls, approvals and the result your agent reported.
maqpna dev timeline --last
TIME KIND SERVER/TOOL DECISION DETAIL
17:31:58 approval echo/delete_resource require_approval apr_31bc56b1ef96a592333824db approved
17:31:58 tool_call echo/delete_resource allow approval:approved by reviewer@localhost (apr_31bc56b1ef96a592333824db)
17:31:58 tool_call echo/delete_resource deny approval_invalid: approval: already used
17:31:58 model_call llm/stub/chat.completions allow scope:models:stub; model:stub; usage:prompt=8,completion=44
17:31:58 model_call llm/stub/models allow scope:models:stub; model:stub
17:31:58 model_call llm/stub/messages allow scope:models:stub; unsupported_by_provider: model provider openai cannot serve this request: native Anthropic Messages requests need a route with provider anthropic or bedrock
17:31:58 result maqpna-session/report_result observe session_result:Succeeded
dev-curl2: 4 allow, 1 deny, cost $0.0000 (maqpna dev timeline --last)
-o json prints every field. MAQPNA Desk's Local (dev) window shows the same timeline live (maqpna desk). In a cluster, maqpna session describe merges the AgentSession, its sandbox and pods with the gateway's view: cost, taint and pending approvals.
The audit ledger#
Each record is one decision. audit tail reads it through the gateway's admin API:
maqpna audit tail --session dev-curl2
2026-10-18T21:31:58.265Z #4 dev/dev-curl2 coder echo/delete_resource require_approval default-agent-policy/destructive-needs-human approval_pending:apr_31bc56b1ef96a592333824db
2026-10-18T21:31:58.341Z #5 dev/dev-curl2 coder echo/delete_resource allow default-agent-policy/destructive-needs-human approval:approved by reviewer@localhost (apr_31bc56b1ef96a592333824db) approver=reviewer@localhost
2026-10-18T21:31:58.358Z #6 dev/dev-curl2 coder echo/delete_resource deny default-agent-policy/destructive-needs-human approval_invalid: approval: already used
2026-10-18T21:31:58.382Z #7 dev/dev-curl2 coder llm/stub/chat.completions allow - scope:models:stub; model:stub; usage:prompt=8,completion=44
2026-10-18T21:31:58.403Z #8 dev/dev-curl2 coder llm/stub/models allow - scope:models:stub; model:stub
2026-10-18T21:31:58.424Z #9 dev/dev-curl2 coder llm/stub/messages allow - scope:models:stub; unsupported_by_provider: model provider openai cannot serve this request: native Anthropic Messages requests need a route with provider anthropic or bedrock
2026-10-18T21:31:58.445Z #10 dev/dev-curl2 coder maqpna-session/report_result observe session/result session_result:Succeeded
-o json shows the whole record, including ext, where the details of each check live. A DLP denial, for example (shortened: the time, latency, cost and hash-chain fields are left out):
{
"seq": 14, "session": "dev-dlp2", "namespace": "dev", "agent": "coder", "user": "you@localhost",
"server": "echo", "tool": "echo",
"argsSha256": "3a6fa4e5f2a367f992b13fc53f6a94cb4c45a922acc588f3efbea4c4621430bf",
"decision": "deny", "rule": "dlp/dev", "reason": "dlp:aws_access_key",
"ext": {"dlp": "aws_access_key:1", "dlpAction": "deny", "dlpBlocked": "request:aws_access_key", "dlpProfile": "dev"}
}
The ledger stores a hash of the arguments, never the arguments or prompts. Filters: --session, -n, --agent, --user, --trace, --since 1h, --tail 50, and -f to follow. Prove nothing was changed with maqpna audit verify .maqpna/audit.jsonl (or --gateway URL in a cluster). Record fields are described in Audit.
Why was this call denied?#
Step 1: Read the error your code got#
error.data names the policy, the rule and the reason. Log all three; they are what the steps below need.
error -32001: denied by policy baseline-guardrails rule never-touch-system-namespaces: system namespaces are off-limits to agents
data: {"detail":"system namespaces are off-limits to agents","domain":"maqpna.com","policy":"baseline-guardrails","reason":"policy_denied","rule":"never-touch-system-namespaces"}
Step 2: Find the record#
maqpna audit tail --session dev-aa0511c1 -o json # local
maqpna audit tail --gateway "$GW" --session fix-4821 -n team-a -o json # cluster
The record's rule is <policy>/<rule> and its ext carries the detail of the check that decided.
Step 3: Ask why, per reason#
| Reason | Command | What it tells you |
|---|---|---|
policy_denied, default_action |
maqpna policy test --policy policies/ --ns NS --agent A --server S --tool T --args '{...}' --explain |
Each rule, whether it matched, and why not (tool "x" not in tools). |
| the same, against what the gateway has loaded | maqpna policy test --gateway "$GW" --ns NS --agent A --server S --tool T |
The decision with the live policies and the session's taints. |
no_applicable_policy |
maqpna policy list -n NS, maqpna agent describe A -n NS |
Whether any policy selects this agent in this namespace. |
revoked |
maqpna revocations list --gateway "$GW" |
The revocation, who created it, why and until when. |
budget_exceeded:<scope>/<window> |
maqpna budgets list --exceeded, maqpna costs summary --scope agent --window day |
Which budget is spent and by whom. |
dlp:<detector> |
maqpna audit tail ... -o json (ext.dlp*) |
The detector, the profile and whether the request or the response matched. |
tool_definition_changed, tool_unpinned |
maqpna mcp tools NS/SERVER --drift-only |
Which tool drifted from its pin. |
a rule with sessionTaints |
maqpna taint get SESSION -n NS |
The session's taint labels and where they came from. |
missing_scope:*, token_* |
maqpna token inspect "$TOKEN" |
The token's scopes, expiry, agent and user. |
approval_denied, approval_expired |
maqpna approvals get ID |
Who decided, when, and the note. |
Step 4: Fix it in the right place#
- The agent did something it should not: change the code or the prompt, and let it handle the denial (see Handle decisions).
- The policy is wrong: change it, add a case to your
PolicyTestsuite, and replay the ledger withmaqpna replaybefore you ship it. - A revocation or a spent budget: those are decisions by people; talk to whoever made them.
Logs and events in a cluster#
maqpna logs fix-4821 -n team-a -f --since 10m --timestamps
maqpna logs ag/triage -n team-a # the agent's newest running session
maqpna events fix-4821 -n team-a # session, sandbox, pods, snapshots
maqpna events -n team-a --audit --since 30m --gateway "$GW"
maqpna logs finds the sandbox pods by the maqpna.com/session label; -c agent limits it to your container. Token mints and refreshes show as events TokenMinted and TokenRefreshed.
Traces with OpenTelemetry GenAI#
Pass W3C trace context from your agent and the gateway continues your trace: it creates spans that follow the OpenTelemetry GenAI conventions (chat {model}, tools/call {tool} with gen_ai.operation.name=execute_tool, invoke_agent {agent}), forwards traceparent to the model, MCP server or called agent, and records the trace ID in the audit record.
client = MaqpnaClient.from_env(traceparent=lambda: current_traceparent()) # or env TRACEPARENT
client.tools("github").call_tool("get_issue", {"number": 4821}, traceparent="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
const client = MaqpnaClient.fromEnv(process.env, { traceparent: () => currentTraceparent() });
client, err := maqpna.NewFromEnv(ctx, maqpna.WithTraceSource(func(ctx context.Context) (string, string) {
return traceparentFromSpan(ctx) // from your OpenTelemetry span
}))
curl -sS "$MAQPNA_TOOL_ECHO_URL" -H "Authorization: Bearer $MAQPNA_TOKEN" -H 'Content-Type: application/json' \
-H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_time","arguments":{}}}'
Then find the decision for that trace:
maqpna audit tail --trace 4bf92f3577b34da6a3ce929d0e0e4736
2026-10-18T21:55:17.263Z #28 dev/dev-trace coder echo/get_time allow baseline-guardrails default_action trace=4bf92f3577b34da6a3ce929d0e0e4736
The gateway exports spans when tracing is enabled (gateway.tracing.enabled and endpoint in the chart). Prompts and arguments are not exported unless the platform team opts in to DLP-redacted content capture. See Trace agents with OpenTelemetry GenAI and OpenTelemetry.