Anatomy of an agent project
The code, image and manifests that make up a governed agent, how the MAQPNA resources relate, how to scaffold them with maqpna init, and a complete minimal set that validates.
A governed agent is two things: your code, packaged as a container image, and a few Kubernetes resources that tell MAQPNA how to run it: which trust tier, which MCP servers, which policy and which budget. This page shows what goes in each file, how the resources point at each other, and a minimal set you can copy and check with maqpna validate.
The resources#
| Kind | Plain name | Scope | You write it when | What it says |
|---|---|---|---|---|
Agent |
agent | namespace | Always | Image, trust tier, model, MCP servers (tools), scopes, budget, timeouts. |
ToolPolicy |
policy | namespace | Always (no policy means every call is denied) | Which agents it applies to, a default action and an ordered list of rules. |
MCPServer |
MCP server | namespace | The agent uses a tool server | URL, transport, trust, data class, and the credential the gateway injects. |
TrustTier |
trust tier | cluster | Usually written by the platform team | Isolation (gvisor, microVM, confidential VM), runtime class, egress, jurisdiction. |
BudgetPolicy |
budget | cluster | You want namespace or user spend limits | Per-day and per-month limits and what happens when they run out. |
AgentSession |
session | namespace | You start a run declaratively (or use maqpna session start) |
The agent, the user it acts for, scopes, TTL and the task. |
A2APeer |
A2A peer | namespace | The agent calls a remote agent | The peer URL, pinned card keys and allowed callers. |
Per-agent spend limits are not a separate kind: they go in Agent.spec.budget.
How they relate#
erDiagram
TrustTier ||--o{ Agent : "spec.tier"
Agent ||--o{ AgentSession : "spec.agentRef"
Agent }o--o{ MCPServer : "spec.tools[].serverRef"
ToolPolicy }o--o{ Agent : "spec.agents (globs)"
Agent }o--o| ToolPolicy : "spec.policyRef (checked)"
BudgetPolicy }o--o{ Agent : "namespaces, users"
A2APeer }o--o{ Agent : "spec.allowedCallers"
AgentSession ||--|| Sandbox : "operator creates"
Three rules explain most surprises:
- Policies select agents, not the other way round. A
ToolPolicyapplies to every agent in its namespace whose name matchesspec.agents(empty means all agents in the namespace).Agent.spec.policyRefonly lets the operator check that the named policy exists (conditionPolicyFound). Every applicable policy is evaluated and the strictest decision wins. - Scopes come from
tools. Withoutspec.scopes, the agent's sessions gettools:<name>for each entry inspec.tools. A session may ask for fewer scopes, never more. A model route scope (models:<namespace>.<agent>) is added automatically whenspec.model.endpointis set. - The tool name is the URL path.
tools: [{name: github}]gives the sandboxMAQPNA_TOOL_GITHUB_URL={gateway}/mcp/github, and policy rules match it asservers: [github].
Step 1: Lay out the project#
A layout that keeps code, manifests, policies and tests apart works with every command on these pages:
triage-agent/
agent.py # or agent.mjs, main.go
requirements.txt
Dockerfile
manifests/
agent.yaml # Agent
mcpserver.yaml # MCPServer
budget.yaml # BudgetPolicy (optional)
tier.yaml # TrustTier (only if your team owns tiers)
policies/
triage-policy.yaml # ToolPolicy, one or more per file
policy-tests/
maqpna-test.yaml # PolicyTest suite for maqpna policy test
local/
policies.json # policy bundle for maqpna dev up --policy (optional)
.gitignore # include .maqpna/
Keep policies in their own directory. maqpna policy lint and maqpna policy test --policy DIR read only ToolPolicy resources and report every other kind in the directory as an error.
Step 2: Scaffold the manifests#
maqpna init prints a starter manifest from a built-in sample; there is no code scaffolding command. List the samples:
maqpna init --list
KIND SAMPLE
A2APeer a2apeer-partner
Agent agent-browser
Agent agent-code-interpreter
Agent agent-coder
...
MCPServer mcpserver-github
ToolPolicy toolpolicy-coder
ToolPolicy toolpolicy-v2-guardrails
TrustTier tier-0-gvisor
Write one per file, then edit it:
maqpna init agent triage --namespace team-a > manifests/agent.yaml
maqpna init mcpserver github --namespace team-a --from-sample mcpserver-github > manifests/mcpserver.yaml
maqpna init toolpolicy triage-policy --namespace team-a --from-sample toolpolicy-coder > policies/triage-policy.yaml
To see what a field means without a cluster, use maqpna explain; the schemas are the CRDs the CLI was built with:
maqpna explain agent.spec.tools
maqpna explain toolpolicy.spec.rules --recursive
For code, start from an example in the source tree (examples-agents/ for LangGraph, the OpenAI Agents SDK, the Claude Agent SDK and a plain SDK agent) or a framework template.
Step 3: Write a minimal set#
This set is one agent, triage, that reads GitHub issues through an MCP server, comments only with approval, and has a spend limit. It passes maqpna validate --strict as written.
manifests/agent.yaml:
apiVersion: maqpna.com/v1alpha1
kind: Agent
metadata:
name: triage
namespace: team-a
spec:
image: registry.example.eu/agents/triage:0.1.0
tier: tier-0-gvisor
model:
provider: vllm
endpoint: http://vllm.models.svc:8000/v1
name: qwen3-coder-30b
maxTokensPerRequest: 4096
tools:
- name: github
serverRef: {name: github}
policyRef: triage-policy
budget:
perSessionUSD: "2"
perDayUSD: "50"
maxSessionTTL: 2h
manifests/mcpserver.yaml:
apiVersion: maqpna.com/v1alpha1
kind: MCPServer
metadata:
name: github
namespace: team-a
spec:
url: https://mcp-github.tools.example.eu/mcp
transport: streamable-http
trust: trusted
auth:
type: bearerSecret
secretRef:
name: github-mcp
key: token
policies/triage-policy.yaml:
apiVersion: maqpna.com/v1alpha1
kind: ToolPolicy
metadata:
name: triage-policy
namespace: team-a
spec:
agents: ["triage"]
defaultAction: deny
rules:
- name: github-read
servers: [github]
tools: ["get_*", "list_*", "search_*"]
action: allow
- name: comment-needs-approval
servers: [github]
tools: ["add_issue_comment"]
action: require_approval
reason: Public comments need a human.
manifests/tier.yaml (skip it if the tier exists in your cluster):
apiVersion: maqpna.com/v1alpha1
kind: TrustTier
metadata:
name: tier-0-gvisor
spec:
runtimeClassName: gvisor
isolation: gvisor
egress: gateway-only
manifests/budget.yaml (cluster-scoped, usually owned by the platform team):
apiVersion: maqpna.com/v1alpha1
kind: BudgetPolicy
metadata:
name: team-a-budget
spec:
namespaces:
team-a: {perDayUSD: "200", perMonthUSD: "3000"}
onExceed: deny
What each choice does:
model.endpointis the real model URL. Only the gateway talks to it; the sandbox getsMAQPNA_MODEL_ENDPOINT={gateway}/llm/team-a.triage/v1.- The token for
github-mcpstays in a Secret the gateway reads. The sandbox never sees it. defaultAction: denymeans a tool the rules do not name is denied, for exampledelete_repository.
Step 4: Validate#
maqpna validate -f manifests/ -f policies/ --strict
5 file(s), 5 object(s): 0 error(s), 0 warning(s)
maqpna validate checks every document against the CRD schemas and CEL rules, compiles every ToolPolicy the way the operator renders it, and resolves spec.tier, spec.policyRef and tools[].serverRef within the files. A reference to an object that is not in the files is informational (it must exist in the cluster), or a warning with --strict. A typo in a field is an error:
SEVERITY FILE OBJECT FIELD CODE MESSAGE
ERROR bad/agent.yaml:1 Agent/team-a/triage spec.tierr unknown-field unknown field (the API server drops it)
Exit status is 0 when valid and 3 on errors, so it works in CI as is.
Step 5: Mirror the policy for the local stack#
The local gateway reads a policy bundle in JSON, not ToolPolicy YAML. To run your agent locally under your own rules, write the same rules as a bundle and start the stack with it. The local maqpna dev run defaults are agent coder in namespace dev, so either use those names in the bundle or pass --agent triage --namespace team-a to maqpna dev run.
{
"policies": [
{
"name": "triage-policy",
"namespace": "team-a",
"agents": ["triage"],
"defaultAction": "deny",
"rules": [
{"name": "github-read", "servers": ["github"], "tools": ["get_*", "list_*", "search_*"], "action": "allow"},
{"name": "comment-needs-approval", "servers": ["github"], "tools": ["add_issue_comment"],
"action": "require_approval", "reason": "Public comments need a human."}
]
}
]
}
maqpna dev up --policy local/policies.json --upstream github=http://127.0.0.1:9000/mcp
maqpna dev run --agent triage --namespace team-a -- python agent.py
The gateway re-reads .maqpna/policies.json every 2 seconds, so you can edit it while the stack runs. maqpna policy test --policy local/policies.json ... accepts the bundle too, so you can check that both files decide the same way.
Next steps#
- Connect to the gateway
- Write policies, for every rule field
- Test agents and policies
- CRD reference: Agent, ToolPolicy, MCPServer