---
title: "Guarding Agent Executors from Bad API Calls: A Practical Tutorial to Harness²"
slug: harness2-api-wrappers-guarded-executors
summary: "Low-level APIs rarely stop an AI agent from debiting a restricted escrow account or paying a staging look-alike. Instead of stuffing more prose into the system prompt, Harness² treats the whole system around the model—deterministic API wrappers, tool-loop hooks, and on-demand skills—as a recursively improvable component tree. Here is how it works, with a hands-on money transfer ledger tutorial."
tags: ai, agents, harness, api, guardrails, tutorial, python
ai_model: gemini-4-argon
published_at: 2026-10-23T18:25:52.99+00:00
canonical_url: https://jarkkolietolahti.com/articles/harness2-api-wrappers-guarded-executors
markdown_url: https://cemecdwfdvdbvpvowuln.supabase.co/functions/v1/content-md?slug=harness2-api-wrappers-guarded-executors
---

Most real-world internal APIs were never designed for autonomous LLM agents. A low-level financial ledger endpoint (`POST /transfers`) will happily move funds as long as the source account has enough balance and both account IDs exist — even if the agent accidentally debited a **restricted customer escrow account**, paid an **unverified staging counterparty** that matched a substring search, or sent **USD into a frozen or EUR-denominated account**.

When an agent makes those mistakes, teams usually respond by stuffing another paragraph of prose into the system prompt. That creates a second problem: **always-on prompt bloat** eats into the model's reasoning and output token budget, and prose instructions can still be skipped under long-context pressure.

[**Harness²**](https://arxiv.org/pdf/2610.14224) (*Recursive Agent Harnessing for an Open World*) takes a structural approach instead. Rather than treating the agent's environment as a single static system prompt, it exposes the **entire system around the model** — prompts, guardrails, memory, tool guidance, skills, subagents, deterministic wrapper scripts, and tool-loop interceptors — as a validated, recursively improvable component tree.

Here is how Harness² works under the hood, followed by a hands-on tutorial where we build a sharp-edged **Ledger Money Transfer HTTP API** and use Harness² to evolve a guarded executor that stops the agent from using the wrong accounts.

## 1. What Is a "Harness" in Harness²?

A **harness** is everything around fixed model weights that turns an LLM into an operating agent. In `SurfaceSpec` (`harness2/core/tree.py`), Harness² formalizes the harness into **eight editable components (`c1..c8`)**, each with a distinct runtime token cost and enforcement reach:

| Component | OpenCode Path (`OPENCODE_SURFACE`) | Codex Path (`CODEX_SURFACE`) | Context Cost | Can the Model Skip It? |
|---|---|---|---|---|
| **c1: Persona / Procedure** | `systemprompt.md` | `AGENTS.md` | **Always-On** (high cost) | Yes (prose) |
| **c2: Standing Rules** | `guardrails.md` | `AGENTS.md` | **Always-On** (high cost) | Yes (prose) |
| **c3: Domain Memory** | `LongTermMEMORY.md` | `AGENTS.md` | **Always-On** (high cost) | Yes (prose) |
| **c4: Tool Guidance** | `tool_descriptions/tool_guidance.md` | `AGENTS.md` | **Always-On** (high cost) | Yes (prose) |
| **c5: Subagents** | `sub_agents/<name>/AGENT.md` | `sub_agents/<name>/AGENT.md` | **Isolated Context** | Invoked via `task` |
| **c6: Skills** | `skills/<name>/SKILL.md` | `.agents/skills/<name>/SKILL.md` | **On-Demand** (0 until loaded) | Triggered by cue |
| **c7: Helper Scripts** | `scripts/<name>.py` | `scripts/<name>.py` | **0 tokens** (runs in shell) | Enforces logic inside wrapper |
| **c8: Plugins / Hooks** | `plugin/<name>.ts` | `.codex/hooks.json` | **0 tokens** (runs in loop) | **No — fires inside the tool loop** |

### Why `c7` (Scripts) and `c8` (Plugins/Hooks) Matter for API Safety

In `harness2/core/prompts.py`, Harness² explicitly instructs its harness editors:

> *"ALWAYS-ON PROSE IS THE EXPENSIVE OPTION. Every word of it is in context on every run, competing with the agent's own output budget... Prose can be stopped reading; a hook cannot."*

When an agent repeatedly misuses an API, the right fix is rarely a longer system prompt. Instead, Harness² steers adaptation toward:
1. **A deterministic wrapper script (`c7` — `scripts/<name>.py`)** for pre-flight validation, schema checking, and safe defaults.
2. **A tool-loop interceptor (`c8` — `plugin/<name>.ts` on OpenCode or `.codex/hooks.json` on Codex)** that runs *inside* the tool loop to block bad calls or inject targeted error recovery hints.
3. **An on-demand skill (`c6` — `skills/<name>/SKILL.md`)** for multi-step domain procedures, paired with a single-line trigger in `guardrails.md`.

## 2. How Harness² Works Under the Hood

Harness² cleanly separates three axes in `harness2/core/spec.py`:
1. **Method (`harness2/core/`)**: Task-level recursion, harness tree validation, blind output selection, and persistent experience libraries.
2. **Substrate (`harness2/substrate/`)**: The agent framework executing rollouts — either **OpenCode** (`OpencodeSubstrate`) or **Codex CLI** (`CodexSubstrate`).
3. **Benchmark / Task Adapter (`BenchmarkAdapter`)**: A minimal protocol supplying tasks, workspace files, rollout execution, trajectory rendering, and post-selection evaluation.

### Two Levels of Recursion

Harness² connects two recursive loops while keeping model weights fixed:

1. **Task-Level Recursion (`harness2/core/recursion.py` — Propose–Probe–Compose)**:
   * **Base Rollout**: Runs the unmodified base harness on the current task and renders the execution stream into a shared `[ACTION]` / `[OBSERVATION]` / `[OBSERVATION ERROR]` trajectory.
   * **Propose (Split by Dimension)**:
     * **INV (Invariant) Proposer**: Owns *how the agent works* — deterministic tool wrappers (`scripts/*.py`), tool-loop hooks (`plugin/*.ts` or `.codex/hooks.json`), and error handling.
     * **DOM (Domain) Proposer**: Owns *what a practitioner knows* — domain invariants, reconciliation checklists, and `skills/<name>/SKILL.md`.
   * **Static Tree Validation (`tree.validate`)**: Before any candidate harness is allowed to run a rollout, Harness² enforces strict structural invariants:
     * Always-on character cap (`24,000` chars on OpenCode; `8,000` chars on Codex).
     * Every Python script in `scripts/*.py` must compile via `compile()`.
     * Every script referenced in prompts or skills must exist in the harness (`_check_script_refs`).
     * Every skill must have valid YAML frontmatter and a literal trigger line in `guardrails.md` or `AGENTS.md` (`autotrigger` & `_check_skills`).
     * Every `.codex/hooks.json` file must match the Claude-Code hook dispatch schema (`check_hooks`).
     * *If validation fails, the actionable error prose is fed back to the proposer for an automatic repair retry.*
   * **Probe & Compose**: Candidate harnesses are probed on the same task, and the Composer synthesizes a combined harness from the behavioral contrasts between base and candidate trajectories — **without ever seeing ground-truth rubrics or scores**.

2. **Harnessing-Level Recursion (`harness2/core/library.py` — Continual Experience Library)**:
   * After blind selection (`selection.py`) commits to the best final deliverable, an experience reflector analyzes the contrast between the base and adopted rollouts and writes structured `<edits>` and `<experience>` entries (`cue`, `lesson`, `evidence`, `confidence`) into `runs/<bench>/library/`. Subsequent tasks in the same domain automatically browse `context/library/INDEX.md` to reuse proven scripts, plugins, and skills.

## 3. Hands-On Tutorial: Guarding a Money Transfer Ledger API

To see how Harness² prevents bad API usage in practice, let's build a local **Ledger Money Transfer HTTP API** (`harness2/ledger_api.py`) and a Harness² adapter (`harness2/ledger_guard_demo.py`).

### Step 1: The Sharp-Edged Ledger HTTP API (`harness2/ledger_api.py`)

Our in-process `RunningLedgerServer` exposes four endpoints:
* `GET /owners?q=<query>` — Lists account owners (`owner_id`, `owner_name`, `entity_type`, `verified`).
* `GET /accounts?owner_id=<id>&q=<query>` — Lists accounts (`account_id`, `owner_id`, `account_type`, `currency`, `status`, `balance`, `allowed_purposes`).
* `POST /transfers` — Executes a transfer (`from_account`, `to_account`, `amount`, `currency`, `purpose`, `memo`).
* `GET /transfers` — Returns the audit log of completed transfers.

Like many low-level ledger microservices, `POST /transfers` only checks that both accounts exist and `balance >= amount`. Meanwhile, the account catalog contains three realistic traps:

| Account ID | Owner | Type | Status | Currency | Allowed Purposes | The Trap |
|---|---|---|---|---|---|---|
| `acc_acme_escrow` | `owner_acme` (Acme Corp) | `escrow` | `active` | `USD` | `["escrow_release_only"]` | **Listed 1st** for Acme Corp with `$50,000` balance. Naive agents debit customer escrow! |
| `acc_acme_operating` | `owner_acme` (Acme Corp) | `operating` | `active` | `USD` | `["vendor_invoice", ...]` | Listed 2nd. Required for vendor invoices. |
| `acc_acme_payroll` | `owner_acme` (Acme Corp) | `payroll` | `active` | `USD` | `["payroll", "contractor_payout"]` | Listed 3rd. Required for contractor payouts. |
| `acc_cloudhost_sandbox` | `owner_cloudhost_sandbox` (`verified=False`) | `vendor_payable` | `sandbox` | `USD` | `["sandbox_testing"]` | **Listed 1st** when searching `"CloudHost"`. |
| `acc_cloudhost_prod` | `owner_cloudhost_prod` (`verified=True`) | `vendor_payable` | `active` | `USD` | `["vendor_invoice"]` | Listed 2nd. The real vendor account for CloudHost Inc. |
| `acc_bob_frozen` | `owner_bob` (Bob Vance) | `checking` | `frozen` | `USD` | `["payroll", "contractor_payout"]` | **Listed 1st** for Bob Vance, but `status="frozen"`. |
| `acc_bob_eur` | `owner_bob` (Bob Vance) | `checking` | `active` | `EUR` | `["international_wire"]` | Listed 2nd for Bob Vance, active, but **wrong currency (`EUR`)**. |
| `acc_bob_usd_checking` | `owner_bob` (Bob Vance) | `checking` | `active` | `USD` | `["payroll", "contractor_payout"]` | Listed 3rd. The only valid USD destination for Bob Vance. |

### Step 2: How the Base Harness Fails

In the unmodified base harness (`NAIVE_TRANSFER_SCRIPT`), `scripts/transfer_client.py` queries `/accounts?q=<first_word_of_name>` and picks `accounts[0]`:

```python
# Base Harness: scripts/transfer_client.py (NAIVE_TRANSFER_SCRIPT)
src_query = urllib.parse.quote(req["sender_name"].split()[0])
dst_query = urllib.parse.quote(req["recipient_name"].split()[0])

src_accounts = get_json(f"/accounts?q={src_query}")["accounts"]
dst_accounts = get_json(f"/accounts?q={dst_query}")["accounts"]

from_acc = src_accounts[0]  # <-- Wrongly selects acc_acme_escrow!
to_acc = dst_accounts[0]    # <-- Wrongly selects acc_cloudhost_sandbox or acc_bob_frozen!

result = post_json("/transfers", {
    "from_account": from_acc["account_id"],
    "to_account": to_acc["account_id"],
    "amount": req["amount"],
    "currency": req["currency"],
    "purpose": req["purpose"],
    "memo": req["memo"],
})
```

When the base rollout runs on our two tasks:
* **Task 1 (`transfer-invoice-1` — Pay $1,250.00 USD invoice to CloudHost Inc)**: The base harness debits `acc_acme_escrow` (customer escrow!) and sends the money to `acc_cloudhost_sandbox` (an unverified staging account).
* **Task 2 (`transfer-payroll-2` — Pay $3,200.00 USD contractor payout to Bob Vance)**: The base harness debits `acc_acme_escrow` and sends the payout to `acc_bob_frozen` (a frozen account).

### Step 3: How Harness² Evolves the 3-Layer Account Guard

During the Propose–Probe–Compose cycle, Harness² inspects the base trajectory and synthesizes three complementary components:

#### Layer 1: Deterministic API Wrapper (`c7` — `scripts/transfer_client.py`)
The **INV Proposer** replaces the naive script with a guarded wrapper that resolves verified owners (`verified is True`, `entity_type != "sandbox"`) and filters every candidate account against status, type, currency, and purpose invariants before calling `POST /transfers`:

```python
# Evolved INV Component (c7): scripts/transfer_client.py (GUARDED_TRANSFER_SCRIPT)
FORBIDDEN_SOURCE_TYPES = {"escrow"}

def select_safe_source_account(
    accounts: list[dict], *, owner_id: str, currency: str, purpose: str
) -> dict:
    candidates = []
    for acc in accounts:
        if acc["owner_id"] != owner_id:
            continue
        reasons = []
        if acc["status"] != "active":
            reasons.append(f"status={acc['status']!r} (must be 'active')")
        if acc["account_type"] in FORBIDDEN_SOURCE_TYPES:
            reasons.append(f"account_type={acc['account_type']!r} is restricted")
        if acc["currency"].upper() != currency.upper():
            reasons.append(f"currency={acc['currency']!r} != {currency!r}")
        if purpose and purpose not in (acc.get("allowed_purposes") or []):
            reasons.append(
                f"purpose={purpose!r} not in allowed_purposes={acc.get('allowed_purposes')}"
            )
        if reasons:
            print(f"[GUARD BLOCKED SOURCE] {acc['account_id']} ({', '.join(reasons)})")
        else:
            candidates.append(acc)
    if len(candidates) != 1:
        raise AccountGuardError(f"Expected 1 valid source account; got {candidates}")
    return candidates[0]
```

#### Layer 2: Tool-Loop Enforcement Plugin (`c8` — `plugin/account_guard.ts`)
To ensure the model cannot bypass the wrapper script with a raw `curl` call in the tool loop, the **INV Proposer** also registers an OpenCode plugin:

```typescript
// Evolved INV Component (c8): plugin/account_guard.ts
import type { Plugin } from "@opencode-ai/plugin"

const BLOCKED_ACCOUNTS = new Set([
  "acc_acme_escrow",
  "acc_cloudhost_sandbox",
  "acc_bob_frozen",
  "acc_bob_eur",
])

export const AccountTransferGuard: Plugin = async () => ({
  "tool.execute.after": async (input, output) => {
    const rawArgs = JSON.stringify(input ?? {})
    for (const bad of BLOCKED_ACCOUNTS) {
      if (rawArgs.includes(bad) || String(output?.output ?? "").includes(bad)) {
        output.output =
          `[ACCOUNT_GUARD_VIOLATION] Restricted/inactive/wrong-currency account ${bad} ` +
          `detected. Always run python3 scripts/transfer_client.py which enforces verified ` +
          `owners, active status, matching currency, and purpose-matched source accounts.\n` +
          String(output?.output ?? "")
      }
    }
  },
})
```

*(On the Codex CLI substrate, the equivalent `c8` lever is `.codex/hooks.json` with a `PreToolUse` or `PostToolUse` command hook that returns `{"decision": "block", "reason": "..."}` directly to the model.)*

#### Layer 3: On-Demand Domain Skill (`c6` — `skills/account-selection-policy/SKILL.md` + `c2` `guardrails.md`)
The **DOM Proposer** encodes the treasury practitioner rules in an on-demand skill (`skills/account-selection-policy/SKILL.md`) and wires a narrow trigger line into `guardrails.md`:

```markdown
- Never debit `escrow` accounts for operating or payroll transfers.
- Never transfer to `sandbox` or `frozen` accounts or mismatched currencies.
- When selecting and verifying source and destination ledger accounts before executing money transfers, load the `account-selection-policy` skill.
```

### Step 4: Inspecting the Resulting Trajectory and Scores

Running the complete workflow (both parallel `agg` and sequential `seq` modes):

```bash
.venv/bin/python -m harness2.ledger_guard_demo runs/ledger_guard_demo
```

produces the following execution trace on `transfer-payroll-2`:

```text
[ACTION] python3 scripts/transfer_client.py <ledger_api> request.json
[OBSERVATION]
[GUARD BLOCKED SOURCE] acc_acme_escrow (account_type='escrow' is restricted, purpose='contractor_payout' not in allowed_purposes=['escrow_release_only'])
[GUARD BLOCKED SOURCE] acc_acme_operating (purpose='contractor_payout' not in allowed_purposes=['vendor_invoice', 'general_operating'])
[GUARD BLOCKED DESTINATION] acc_bob_frozen (status='frozen' (must be 'active'))
[GUARD BLOCKED DESTINATION] acc_bob_eur (currency='EUR' != 'USD', purpose='contractor_payout' not in allowed_purposes=['international_wire'])
[GUARD VERIFIED SOURCE] acc_acme_payroll (type=payroll, status=active, currency=USD)
[GUARD VERIFIED DESTINATION] acc_bob_usd_checking (owner='Bob Vance', status=active, currency=USD)
```

| Task | Unmodified Base Harness | Harness² Adapted (`agg` & `seq`) | Criteria Passed |
|---|---|---|---|
| **`transfer-invoice-1`** (Pay $1,250 to CloudHost Inc) | Debited `acc_acme_escrow` -> Paid `acc_cloudhost_sandbox` | Debited `acc_acme_operating` -> Paid `acc_cloudhost_prod` | **1/4** -> **4/4 (100%)** |
| **`transfer-payroll-2`** (Pay $3,200 to Bob Vance) | Debited `acc_acme_escrow` -> Paid `acc_bob_frozen` | Debited `acc_acme_payroll` -> Paid `acc_bob_usd_checking` | **1/4** -> **4/4 (100%)** |

And because Harness² runs **Harnessing-Level Recursion** after each task, the validated harness tree (`scripts/transfer_client.py`, `plugin/account_guard.ts`, `skills/account-selection-policy/SKILL.md`, `guardrails.md`, and `EXPERIENCE.md`) is automatically saved to the domain library (`runs/ledger_guard_demo/library/k1/`) so every subsequent treasury task inherits the guarded executor out of the box.

## 4. A Checklist for Wrapping Your Own APIs in Harness²

Whenever you need an AI agent to operate safely against an external or internal API, use Harness²'s component hierarchy to put each check where it belongs:

1. **Put deterministic pre-flight checks in `scripts/<api_client>.py` (`c7`)**: Validate status flags, permissions, currencies/units, and schema constraints in Python before any mutating `POST`, `PUT`, or `DELETE` call is sent.
2. **Enforce tool-loop boundaries in `plugin/<name>.ts` or `.codex/hooks.json` (`c8`)**: Block raw `curl` calls to mutating endpoints or inject immediate recovery instructions when known error signatures appear.
3. **Keep domain policies in on-demand `skills/<name>/SKILL.md` (`c6`)**: Store multi-step reconciliation workflows in skills with a 1-line trigger in `guardrails.md`, keeping your always-on prompt budget small.
4. **Enforce structural invariants in `BenchmarkAdapter.validate_tree()`**: Reject any candidate harness edit that drops your required wrapper script or violates safety invariants before a single rollout token is spent.

