Skip to main content

Agent Harness

A terminal-based ReAct agent for the University of Passau AI Engineering Lab (SoSe 2026). The Week 3 harness adds specialised sub-agents, config-driven permission tiers, and Prometheus/Grafana observability to the Week 2 base of workspace-confined filesystem tools, runtime-discovered MCP tools, and persistent long-term memory — all authority stays in deterministic local code.

The LLM proposes. The local registry validates, authorizes, executes, observes, and audits. A sub-agent is the same loop, narrower.

The features are built around one product: an operations copilot for a farm's wind turbine and solar arrays — see the problem statement. The sub-agents have real roles because there is real analysis to delegate; the require-confirmation tier gates real consequences (restart an inverter, schedule turbine curtailment); the dashboard shows real runs.

First time here? Follow Getting started. For complete engineering context, read Project engineering reference.

Requirements

  • Python 3.12
  • An InnKube inference access key
  • Docker (recommended, but optional for local development); Docker Compose for the optional Prometheus/Grafana stack
  • Network access to the configured LLM; to Open-Meteo for the plant and hourly forecast tools; and to the US National Weather Service for the NWS tools. PLANT_OFFLINE=1 runs the plant on synthetic weather with no network.

Architecture

Agent Harness Week 3 architecture

The non-root Python process runs a bounded reason → act → observe loop. Native filesystem and memory tools, the delegate tool, and MCP-discovered tools all enter the same ToolRegistry, argument-validation, permission, observation, and audit path. A sub-agent is that same loop with its own instructions, its own limits, and an allow-listed view of the registry whose tiers can only be tighter than the parent's. Every audit event fans out to a JSONL file and a Prometheus exporter; a compose-managed Prometheus scrapes it over a shared Docker network and Grafana shows it. The bundled MCP servers are stdio subprocesses: the extended weather quickstart, the unmodified third-party mcp-server-time, and the plant operations server, which reads a SunSpec inverter fleet over Modbus TCP.

The maintainable diagram source is docs/architecture/week-3.svg. Earlier diagrams are retained in docs/architecture/ so each week's architectural change stays visible.

Quick start

cp .env.example .env                  # put your InnKube key in AGENT_API_KEY
docker network create agentnet        # once; ./agent also creates it
docker compose up -d                  # Prometheus :9090, Grafana :3000 (optional)
./agent --verbose                     # builds the image, joins agentnet, enters agent mode

Then open http://localhost:3000/d/agent-harness and follow the Week 3 demo.

Week 3 capabilities

  • Specialised sub-agents on the same harness. Roles are configuration: a name, instructions, a tool allow-list, per-tool tiers that can only tighten the parent's, and their own iteration and observation limits. The parent delegates with one delegate tool; the child's answer is the parent's observation; every child event is stamped with the role, a delegation id, and the parent's turn. Delegation is one level deep by construction.
  • Permission management for built-in and MCP tools. allow, deny, and ask (require confirmation) through one path. Built-in tiers are re-tierable in config; MCP tiers stay with their server; a base deny — workspace confinement, unknown action, server deny — is never lifted by an override.
  • Observability. Metrics are a projection of the audit stream: turns, active runs, latencies, success/failure, LLM tokens, tool usage, permission decisions, memory operations, sub-agent activity, each with a scope of parent or the role. /metrics on agent.metrics_port; Prometheus and a provisioned 13-panel Grafana dashboard via compose.yaml; the JSONL audit is the per-run trace.
  • The plant. A SunSpec/Modbus TCP inverter fleet (simulated in-process, reached over TCP by a real Modbus client), weather-normalised diagnosis with fault attribution priced in euros, the day-to-day comparison that separates weather from faults, a bat-curtailment planner that applies the permit's thresholds to tonight's forecast and shows every one, and two approval-gated control actions.
  • Week 2 hardening. Tool observations are capped before entering the context; the memory store is tenant-scoped with session provenance; MCP adaptation failure is isolated per server; structured MCP results reach the model with their values; the weather server gained a global hourly forecast from Open-Meteo.
  • Week 1 and Week 2 behaviour — filesystem confinement, one-use overwrite approval, runtime MCP discovery, long-term memory across restarts — is unchanged and still tested.

Configuration

The runnable default is config/config.example.yaml. The Week 3 settings:

agent:
  metrics_port: 9464                 # Prometheus scrape endpoint; omit to disable
  instructions: |                    # appended to the built-in guidance for this deployment
    You are the operations copilot for a farm's wind turbine and solar arrays. ...
  # tool_permissions:                # re-tier any tool by public name; a base deny is final
  #   modify_file: allow

mcp:
  servers:
    - name: "plant"
      command: "python3"
      args: ["-m", "mcp_servers.plant.server"]
      env: { PLANT_SITE_FILE: "mcp_servers/plant/site.example.json" }
      default_permission: "allow"
      tool_overrides:
        restart_inverter: "ask"
        set_turbine_curtailment: "ask"
        clear_turbine_curtailment: "ask"

sub_agents:
  - name: "diagnostician"
    description: "Reads plant telemetry ... attributes underperformance to a cause with its cost."
    instructions: |
      You are the plant diagnostician ...
    tools: ["mcp__plant__list_assets", "mcp__plant__diagnose_performance",
            "mcp__plant__read_inverter", "mcp__plant__compare_days", "recall"]
    max_iterations: 6
  - name: "compliance"
    ...

AGENT_METRICS_PORT, AGENT_MEMORY_DB, and AGENT_AUDIT_LOG override their YAML counterparts. The plant server reads PLANT_SITE_FILE, PLANT_MODBUS_HOST/PORT, PLANT_SIMULATOR (0 to point at real hardware), and PLANT_OFFLINE.

Capability and permission boundary

Native tools: create_folder, create_file, navigate, search_in_files, read_file, modify_file; with memory configured, remember and recall; with roles configured, delegate. Discovered MCP tools are added as mcp__<server>__<tool> — never as bare names, never through a side channel.

Permission precedence, most specific last:

workspace confinement / unknown action ─▶ server default ─▶ server tool_overrides
        ─▶ agent.tool_permissions ─▶ sub-agent role tool_permissions (tighten only)
  • Workspace reads, searches, navigation, and non-overwriting creation are allowed inside the canonical workspace; modify_file asks, because it overwrites bytes.
  • remember is covered by the one-time memory consent; recall and delegate are allowed — the sub-agent's own tools are gated separately.
  • Plant reads and diagnosis are allowed; restart_inverter, set_turbine_curtailment, and clear_turbine_curtailment ask.
  • A sub-agent's ask reaches the same human prompt, labelled with the role. A role can make a tool stricter than the parent's tier, never looser.
  • Unknown tools, unknown roles, and unavailable approvals fail closed.

There is no delete, rename, shell, arbitrary-code, or unrestricted-HTTP tool. This is a controlled harness, not a hostile-code sandbox. See Security before mounting sensitive data or configuring a new MCP command.

Tests and quality checks

ruff check .
ruff format --check .
mypy src tests mcp_servers
pytest                       # 174 deterministic cases, incl. real Modbus TCP round trips

Real-LLM end-to-end tests are opt-in because they use credentials, network, and API budget:

RUN_E2E=1 AGENT_CONFIG=config/config.yaml pytest -m e2e tests/e2e

The Week 3 E2E scenario asks a real model to find the money-losing inverter and restart it, and asserts that the diagnosis was delegated, that the restart reached the approval callback before the device, and that both the audit trail and the Prometheus registry recorded it.

Documentation

Project layout

agent, compose.yaml              One-command launcher; Prometheus + Grafana stack
config/                          Runnable validated configuration
docker/                          Non-root Python 3.12 runtime image
observability/                   Prometheus scrape config, Grafana provisioning, dashboard
docs/                            Index, security, status, problem statement
docs/week-1/ … docs/week-3/      Per-week demo, presentation, and evidence docs
docs/architecture/               Diagrams and re-export instructions
docs/lab_instructions/           The graded course specification
mcp_servers/weather/             Extended official weather quickstart + Open-Meteo forecast
mcp_servers/plant/               SunSpec/Modbus simulator and gateway, diagnosis, curtailment
mcp_servers/common/              Open-Meteo client shared by the servers
src/enzomotive/               Loop, LLM client, config, audit, UI, memory
src/enzomotive/subagents.py   Delegation runner: one role, one narrower loop
src/enzomotive/observability.py  Prometheus sink over the audit stream
src/enzomotive/mcp/           MCP configuration, lifecycle, adapter, errors
src/enzomotive/tools/         Registry and sub-agent views, permission tiers, filesystem
tests/unit/                      Fast deterministic harness tests
mcp_servers/*/tests/             Server-side tests, incl. Modbus TCP round trips
tests/e2e/                       Opt-in real-LLM workflows

Release files for enzomotive 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enzomotive 0.1.0
File Size Uploaded
enzomotive-0.1.0.tar.gz 141.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for enzomotive 0.1.0
File Interpreter ABI Platform
enzomotive-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 210.4 kB

Release files / enzomotive-0.1.0.tar.gz

Download URL enzomotive-0.1.0.tar.gz
Size 141.8 kB
Tags Source
SHA-256 checksum
How to use checksums
d03e0ba8bef70af9a1fd61e24b7bce904f8845f81c073308efca24843d58f4e8
BLAKE2b-256 checksum
How to use checksums
9ce384f3718eaf88fce833816824b151cf1a1ea6b0d8bf743c8c93e2f826e85e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / enzomotive-0.1.0-py3-none-any.whl

Download URL enzomotive-0.1.0-py3-none-any.whl
Size 68.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
caec49e089f8d3ea1a0a6da7d18a6f73e0183d888bb3ad842e292e0035b97f71
BLAKE2b-256 checksum
How to use checksums
a8035ffbb55119a909a41f2e6acaa92e059ad463c4e4a5cfb0158b6c262cef4f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page