Aleo for Agents is offline at present

blog·2026-08-17·10 min read

Build a Guardrailed Aleo Coding Agent with AleoFlow MCP

Introduction

Giving an AI agent a compiler is useful. Giving the same agent an unrestricted transaction signer is reckless.

AleoFlow 0.2.0 draws a sensible boundary between those jobs. Its local MCP server exposes project scaffolding, compilation, tests, auditing, queries, local execution, and transaction dry runs. The three tools that can deploy, execute, or send with --broadcast are absent unless the server starts with ALEOFLOW_MCP_ALLOW_BROADCAST=true.

Absent matters. A warning in a tool description asks the model to behave. An omitted tool denies the capability.

Private keys are also excluded from every MCP tool schema. AleoFlow leaves key loading to the underlying Leo process through PRIVATE_KEY or a project .env file. That prevents a model from stuffing a key into a tool call, where it could land in an MCP trace or conversation log.

The boundary is narrower than a full sandbox. If your coding agent also has an unrestricted shell and can read .env, it can still steal whatever is there. Use a fresh, unfunded development account. Do not point a general coding session at a funded production key and expect MCP gating to save you. Lions appreciate fences, but we still check the gate.

What we're building

We will create guarded_payments.aleo, a small payment-note program with two entry points:

  • preview_payment creates an encrypted Payment record without touching public state.
  • create_payment creates the same record and increments a public payment counter in a final { } block.

The receiver, amount, and reference use Leo's default private visibility. Only the aggregate count reaches public storage. An observer can see that another payment was created, but not its recipient or value from this program's state.

AleoFlow's payment template gives us the project shell. We will replace its sample program so every line is visible here and compile it against Leo 4.4 syntax: fn, Final, inline final { }, storage, and a mandatory constructor.

The coding agent will receive these MCP capabilities:

ToolTypical schema fieldsAuthority
aleoflow_initname, template, optional workspaceWrites local project files
aleoflow_buildoptional path, optional JSON output settingCompiles locally
aleoflow_testoptional path, optional JSON output settingRuns local tests
aleoflow_auditpathReads and checks Leo source
aleoflow_runfunction name, inputs, optional path, network, and endpointRuns locally without sending a transaction
aleoflow_execute_dry_runfunction name, inputs, optional path, network, and endpointBuilds and simulates an execution without --broadcast
aleoflow_deploy_dry_runpath, optional network and endpointPrepares a deployment without sending it
aleoflow_send_dry_runrecipient, amount, optional network and endpointSimulates a credits transfer

None accepts private_key or a generic bag of CLI flags. That last detail blocks an agent from smuggling --broadcast through a supposedly safe tool.

AleoFlow does not edit source files itself. Your coding agent uses its normal filesystem tools for that, then calls AleoFlow for compiler-backed feedback.

Prerequisites

Install Leo 4.4 and confirm the version before generating anything.

bash
leo --version

Install the exact AleoFlow release used here.

bash
cargo install aleoflow --version 0.2.0
aleoflow --version
aleoflow doctor

You also need an MCP client. The examples use Claude Code, though any client capable of launching a local stdio server can use the same binary.

Keep a development account separate from every funded account. If a dry-run command needs signing context, create an unfunded key locally:

bash
aleoFlow account new --write

If your installed binary is lowercase-only, use the canonical command instead:

bash
aleoflow account new --write

The resulting .env must remain ignored by Git. Check that before inviting an agent into the directory.

bash
git check-ignore .env

Step-by-step

1. Scaffold the project

Create the payment project from AleoFlow's built-in template.

bash
aleoflow init guarded-payments --template payment
cd guarded-payments

AleoFlow preserves the hyphenated folder name and sanitizes the on-chain program identifier to guarded_payments.aleo, since Aleo identifiers cannot contain hyphens.

Replace program.json with the complete manifest below.

json
{
  "program": "guarded_payments.aleo",
  "version": "0.1.0",
  "description": "Private payment records with a public aggregate counter",
  "license": "MIT",
  "leo": "4.4.0",
  "dependencies": null,
  "dev_dependencies": null
}

Pinning the Leo version catches local compiler drift. A different 4.x minor release may parse or lower code differently, so a version warning deserves attention rather than a shrug.

2. Write the payment program

Replace src/main.leo with this complete program.

leo
program guarded_payments.aleo {
    @noupgrade
    constructor() {}

    record Payment {
        owner: address,
        amount: u64,
        reference: field,
    }

    storage payment_count: u64;

    fn preview_payment(
        receiver: address,
        amount: u64,
        reference: field,
    ) -> Payment {
        assert(amount > 0u64);

        return Payment {
            owner: receiver,
            amount: amount,
            reference: reference,
        };
    }

    fn create_payment(
        receiver: address,
        amount: u64,
        reference: field,
    ) -> (Payment, Final) {
        assert(amount > 0u64);

        let payment: Payment = Payment {
            owner: receiver,
            amount: amount,
            reference: reference,
        };

        return (
            payment,
            final {
                let current: u64 = payment_count.unwrap_or(0u64);
                payment_count = current + 1u64;
            },
        );
    }
}

The @noupgrade constructor fixes the program at edition zero. That is my preferred policy for a tutorial-sized payment primitive because nobody needs an admin key quietly replacing its rules later. A real product may choose @admin or @checksum, but the upgrade authority then becomes part of its security model.

Payment is a record, so Leo encrypts each output for its owner. Record declarations require owner: address; omitting it is a compiler error. The other fields have no public qualifier and are private by default.

payment_count is singleton public storage. A mapping would work, but inventing a dummy key for one counter is needless ceremony. unwrap_or(0u64) handles the undeclared initial value on the first call.

preview_payment is useful for quick local runs. create_payment returns both the private record and a Final handle. Its final block runs on-chain during a real execution and is the only place where the storage assignment is legal.

Notice what the final block does not capture: receiver, amount, or reference. Passing private values into finalization would expose them to public execution. The public counter leaks activity, which is a real tradeoff, but it does not leak the payment payload.

3. Compile before adding AI

Run AleoFlow's wrapper first, then invoke Leo directly so you know the underlying project also works without MCP.

bash
aleoflow build --path .
leo build

Both commands should produce compiled Aleo instructions under build/. AleoFlow delegates compilation to Leo rather than maintaining a second compiler.

Run the private, proof-half preview with a real Aleo address. The address below is the published local development address, not a production identity.

bash
aleoFlow run preview_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field \
  --path .

Use the canonical lowercase spelling if required by your shell:

bash
aleoflow run preview_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field \
  --path .

Here is the direct Leo equivalent required for a tool-independent check:

bash
leo run preview_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field

leo run evaluates the proof half locally. Use an execution dry run when you want the final block included in the simulation:

bash
aleoflow execute create_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field \
  --path .

There is deliberately no --broadcast flag.

4. Add current Leo tests

Create tests/test_guarded_payments.leo.

leo
import guarded_payments.aleo;

@test
fn test_preview_payment() {
    let payment: guarded_payments.aleo::Payment =
        guarded_payments.aleo::preview_payment(
            aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px,
            125u64,
            9field,
        );

    assert_eq(payment.amount, 125u64);
    assert_eq(payment.reference, 9field);
}

@test
@should_fail
fn test_zero_payment_fails() {
    let payment: guarded_payments.aleo::Payment =
        guarded_payments.aleo::preview_payment(
            aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px,
            0u64,
            9field,
        );

    assert_eq(payment.amount, 0u64);
}

Leo 4.4 tests are top-level @test fn declarations. Imported entry points use the program.aleo::function locator, and imported record types use the same double-colon form. Older test transition examples belong to another compiler generation.

Run the fast test path and then the proving path.

bash
aleoflow test --path .
leo test
leo test --prove

The failing test is intentional. Zero-value records would be valid at the type level, so the assertion in preview_payment supplies the business rule.

5. Audit the source

Run AleoFlow's linter after the compiler tests.

bash
aleoflow audit .

AleoFlow's audit is heuristic. It checks suspicious public record fields, sensitive values written into mappings, shallow private-to-final data flow, and leftover TODO markers. It is useful, especially for catching an accidental finalization leak, but it is not a proof of economic correctness or a substitute for review.

6. Connect the guarded MCP server

Register AleoFlow without setting its broadcast opt-in variable.

bash
which aleoflow
claude mcp add aleoflow -- /absolute/path/to/aleoflow mcp

Restart the client if it does not refresh MCP tools immediately. Ask it to list the AleoFlow tools. You should see the safe and dry-run names, including aleoflow_build, aleoflow_test, aleoflow_audit, aleoflow_run, and aleoflow_execute_dry_run.

You should not see:

  • aleoflow_deploy_broadcast
  • aleoflow_execute_broadcast
  • aleoflow_send_broadcast

AleoFlow 0.2.0 has dedicated MCP gating tests around this boundary. With the environment flag absent, broadcast tools are omitted from tools/list, and direct calls are refused. With opt-in enabled, the tools appear but still require confirm: true. Safe schemas do not expose a private-key argument or a free-form broadcast flag.

Give the agent a precise task rather than a vague request to handle deployment:

text
Work only in the guarded-payments project. Use AleoFlow MCP to build, test,
audit, run preview_payment with 125u64 and 9field, then dry-run
create_payment with the same inputs. Fix compiler errors in local files.
Do not request, print, read, or modify any private key. Do not enable or
attempt any broadcast tool. Report raw AleoFlow failures before changing code.

The server returns real command output instead of a model-written summary. That is good architecture. Compiler errors should reach the agent intact, because a friendly paraphrase can remove the exact error code or source location needed for the next edit.

Testing

Run the whole local verification sequence from the project root:

bash
aleoflow build --path .
aleoflow test --path .
aleoflow audit .
aleoflow run preview_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field \
  --path .
aleoflow execute create_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  125u64 \
  9field \
  --path .

Now try the negative case:

bash
aleoflow run preview_payment \
  aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
  0u64 \
  9field \
  --path .

The run should fail at assert(amount > 0u64). AleoFlow preserves Leo's raw error and may append its best-effort translation. Trust the raw compiler or VM output when the translation disagrees.

Check the MCP boundary separately. Ask the agent to list its AleoFlow tools, then ask whether aleoflow_execute_broadcast exists. Do not set the opt-in flag merely to test the agent's self-control. We are testing capability denial, not manners.

One uncomfortable detail remains: an agent with a general shell could invoke aleoflow execute ... --broadcast directly, bypassing MCP. Disable shell access for the session, apply command allowlists, or run the agent in a container without credentials or network egress. MCP gating controls MCP tools. Nothing more.

What's next

A private payment record is a useful base, though it is not yet money movement. The next version could import credits.aleo, transfer credits, and return the payment record in the same call. Cross-program credit transfers add fee handling and record selection, so I would keep broadcast disabled until those paths have devnode coverage.

For local state iteration, pair this project with Build a Quota Ledger on Aleo with leo devnode. Use the current Leo 4.4 syntax from this tutorial when adapting the workflow.

For more test design, continue with Build a Test-First Allowance Ledger in Leo. Once the program is deployed and a frontend needs to inspect it, Build an ABI-Driven Aleo Transaction Form with the Provable SDK covers the client side.

Keep the broadcast tools off for ordinary coding sessions. When deployment day arrives, use a separate, short-lived session with a low-value account, inspect the dry-run output yourself, and enable only the authority needed for that operation. The agent can write code all week without becoming your wallet.

Sources