All plugins

trade-preflight

Deterministic, fail-closed execution firewall for AI-initiated equity orders. A pre_tool_call hook normalizes broker-specific trade calls into one canonical order, evaluates it against an operator-owned policy (gross buy notional cap, max quantity, symbol / order-type / side allowlists) and blocks anything it cannot prove compliant; every decision and execution outcome is recorded in SQLite. Ships two paper-trading execution tools with deliberately different broker schemas (symbol/side/quantity/limit_price vs ticker/direction/qty/price), a read-only dry-run tool, a policy inspector and an audit reader. Disclosure - no network calls, telemetry, shell commands, background processes or stored credentials; no requires_env; reads only its own shipped policies/default.json (copied once into the plugin data directory to seed the operator policy) and writes only inside plugin_data_dir('trade-preflight') (the policy file and the SQLite audit/ledger database); execution is simulated locally in SQLite and never reaches a broker or a real trading venue, so the plugin adds no real trading capability; the policy file is operator-owned and an agent with unrestricted shell access as the same OS user could modify it, which is a documented limitation rather than a guarantee.

MohamedhazeemToolsv0.1.0Updated 7 hours ago

About

Deterministic, fail-closed execution firewall for AI-initiated equity orders. A pre_tool_call hook normalizes broker-specific trade calls into one canonical order, evaluates it against an operator-owned policy (gross buy notional cap, max quantity, symbol / order-type / side allowlists) and blocks anything it cannot prove compliant; every decision and execution outcome is recorded in SQLite. Ships two paper-trading execution tools with deliberately different broker schemas (symbol/side/quantity/limit_price vs ticker/direction/qty/price), a read-only dry-run tool, a policy inspector and an audit reader. Disclosure - no network calls, telemetry, shell commands, background processes or stored credentials; no requires_env; reads only its own shipped policies/default.json (copied once into the plugin data directory to seed the operator policy) and writes only inside plugin_data_dir('trade-preflight') (the policy file and the SQLite audit/ledger database); execution is simulated locally in SQLite and never reaches a broker or a real trading venue, so the plugin adds no real trading capability; the policy file is operator-owned and an agent with unrestricted shell access as the same OS user could modify it, which is a documented limitation rather than a guarantee.

TradePreflight: Equities Execution Firewall

The agent proposes. The policy gate decides.

TradePreflight is a native Hermes Agent
plugin that intercepts AI-initiated equity trade tool calls and blocks them unless
they satisfy an operator-owned policy. It is deterministic, local-first, and
fail-closed: if the plugin cannot prove an order is compliant, the order does not
execute.

Project status: complete (Slices 0–4). The interception gate, broker-agnostic
adapter/Decimal layer, policy rule engine, paper-trading ledger, and reproducible
demo are all live.


Table of contents


Why TradePreflight

An LLM can generate a plausible-looking order in milliseconds. It cannot be
trusted to respect position limits, notional caps, or symbol restrictions. Prompt
instructions are suggestions; a policy gate is an enforcement mechanism.

TradePreflight sits on Hermes' pre_tool_call hook and applies the same decision
to every broker adapter:

  1. Capture the raw tool call before the handler runs.
  2. Normalize broker-specific arguments into one canonical NormalizedOrder.
  3. Evaluate the order against the operator's policy file.
  4. Block, with a human-readable reason, or allow the call to proceed.
  5. Record the decision and the eventual execution outcome in SQLite.

The model never sees the policy file unless the operator exposes it through the
read-only trade_policy_get tool. The model cannot edit it.

Installation

Requirements

  • Python 3.10+
  • Hermes Agent >= 0.23.1 with native plugin support
  • No third-party Python packages. The plugin imports only the standard library
    and the documented plugins.plugin_storage helper.
  • No broker account, API key, or network access is required

Install as a native Hermes plugin

hermes plugins install trade-preflight
hermes plugins doctor trade-preflight --ci

Or install from a checkout:

git clone https://github.com/Mohamedhazeem/TradePreflight.git
cp -R TradePreflight ~/.hermes/plugins/trade-preflight
hermes plugins enable trade-preflight
hermes plugins doctor trade-preflight --ci

Restart Hermes (or the gateway) so the plugin is loaded.

Install for local development

cd TradePreflight
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
pytest

This repository is a Hermes native plugin, not a distributable Python package.
The root __init__.py is the Hermes entry point; the implementation lives in
src/trade_preflight and is imported with relative imports, so every module
is loaded under the hermes_plugins.trade-preflight namespace Hermes owns and
is evicted correctly when the plugin is updated or reloaded.

pyproject.toml holds tooling configuration only (pytest, coverage, ruff) and
requirements-dev.txt holds the development dependencies. There is no
[project] table on purpose: a [project] table makes the plugin manager
treat the plugin directory as an installable workspace member named
trade-preflight, which collides with the plugin id and breaks uv lock /
hermes plugins enable when more than one profile has the plugin enabled.

Operator data locations

DataLocation
Policy fileplugin_data_dir("trade-preflight")/policy.json
SQLite databaseplugin_data_dir("trade-preflight")/data.db
Shipped default policypolicies/default.json (copied on first run)

plugin_data_dir() resolves under the active Hermes profile
(<HERMES_HOME>/plugin-data/trade-preflight/) and survives plugin updates.
Never place operator state in the plugin install directory.

Quick start

  1. Start Hermes and ask for a trade:

    Buy 20 shares of AAPL at $150 using Broker A.
    
  2. Inspect the policy the gate is using:

    Use trade_policy_get.
    
  3. Dry-run an order without executing it:

    Use trade_check with symbol AAPL, side buy, quantity 20,
    order_type limit, limit_price "150.00".
    
  4. Review the audit trail:

    Use trade_audit with limit 20.
    
  5. Edit the policy file as the operator, then restart or wait for the next call.
    The policy is re-read on every guarded tool call and every decision records
    the SHA-256 of the policy that produced it.

Architecture

TradePreflight follows Domain-Driven Design with a dependency rule that points
inward. Broker translation, persistence, and Hermes wiring are all replaceable;
the domain does not know they exist.

                    Hermes Agent
                         |
             pre_tool_call / post_tool_call
                         |
                 hooks/execution_gate.py
                    /            \
        adapters/base.py       services/audit.py
          /          \                |
 broker_a.py    broker_b.py       SQLite (plugin_db)
          \          /                |
        NormalizedOrder               |
               |                      |
        services/engine.py  <----------+
               |
          domain/policy.py

Layers

LayerResponsibilityMay import
domain/NormalizedOrder, Policy, Decision, validationstdlib only
adapters/Broker A/B argument translation, adapter registrydomain
services/Policy loading, rule evaluation, SQLite auditdomain
tools/Hermes tool schemas + handlersservices, domain
hooks/pre_tool_call / post_tool_call, fail-closed wiringall inner layers

The dual-adapter proof

The policy engine never sees a broker schema. Two adapters prove it:

Broker ABroker BCanonical
symboltickersymbol
sidedirectionside
quantityqtyquantity
order_typeorder_typeorder_type
limit_pricepricelimit_price
— derived —— derived —notional

Both adapters produce the same NormalizedOrder for equivalent inputs. Adding
Broker C requires one adapter and one registry entry; the policy engine and rules
do not change.

Why Decimal

Binary floats cannot represent most decimal fractions exactly. 0.1 + 0.2 is
not 0.3 in IEEE-754, and a limit check that is off by a fraction of a cent is a
correctness and compliance bug. Every monetary value is a decimal.Decimal
parsed from a string. Booleans, NaN, Infinity, negative values, and
fractional shares are rejected at the domain boundary. See
ADR 001.

Tools

ToolTypePurpose
paper_place_order_aexecutionSimulated Broker A order: symbol, side, quantity, order_type, limit_price
paper_place_order_bexecutionSimulated Broker B order: ticker, direction, qty, order_type, price
trade_checkread-onlyNormalize and dry-run an order against the policy. Never executes.
trade_policy_getread-onlyReturn the active policy and its SHA-256.
trade_auditread-onlyReturn recent audit rows, newest first.

All five tools are declared in plugin.yaml and registered from the root
__init__.py. The two execution tools are guarded by pre_tool_call; the
read-only tools are not.

Handler contract

def handler(args: dict, **kwargs) -> str:
    """Always return a JSON string, including on error. Never raise."""

Policy

The policy is a JSON object owned by the operator:

{
  "max_gross_buy_notional_usd": "10000.00",
  "max_quantity": 200,
  "symbol_allowlist": ["AAPL", "MSFT"],
  "order_type_allowlist": ["limit"],
  "side_allowlist": ["buy"]
}
KeyTypeRule
max_gross_buy_notional_usddecimal stringBuy notional may not exceed this value
max_quantityintegerShare quantity may not exceed this value
symbol_allowliststring arraySymbol must be listed (case-insensitive, normalized to upper)
order_type_allowliststring arrayOrder type must be listed (MVP: limit only)
side_allowliststring arraySide must be listed (MVP: buy only)

Rules:

  • Missing keys, unknown keys, wrong types, empty allowlists, non-finite values,
    and malformed JSON are configuration errors. A configuration error blocks
    every guarded call.
  • max_gross_buy_notional_usd is parsed and compared as Decimal, never float.
  • The policy file is re-read per guarded call so operator edits take effect
    without a restart.
  • Every audit row stores the SHA-256 of the exact policy used for the decision.
  • There is no policy-editing tool. Only the operator may edit the file.

Audit lifecycle

pre_tool_call writes the initial decision; post_tool_call correlates the
handler outcome by the Hermes tool_call_id.

StateWritten byMeaning
EXECUTION_BLOCKEDpre_tool_callPolicy violation or fail-closed error. Handler never ran.
EXECUTION_ALLOWEDpre_tool_callPolicy passed. Awaiting execution.
EXECUTION_SUCCEEDEDpost_tool_callHandler returned a recognized success payload.
EXECUTION_FAILEDpost_tool_callHandler raised or returned an error payload.
EXECUTION_OUTCOME_UNKNOWNpost_tool_callHandler ran but the result shape was unrecognized.

Audit writes are best-effort and never weaken a block. Decimal values are stored
as TEXT; timestamps are ISO-8601 UTC.

Security boundaries

Read this section carefully. Overstating coverage is a safety defect.

Guarded

  • paper_place_order_a
  • paper_place_order_b

These are the only tools intercepted by pre_tool_call. The check is keyed by
exact tool name.

Not guarded

  • Manual broker app trades are not protected. If a human places an order in
    a brokerage app or website, TradePreflight never sees it. The plugin is an
    execution firewall for Hermes tool calls, not a broker-side risk system.
  • Arbitrary terminal commands, scripts, curl, or broker SDK calls made outside
    a registered tool.
  • Other plugins' tools, including other trading tools that do not use these
    adapters.
  • Trades placed before the plugin loads or after it is disabled.
  • Market orders, fractional shares, options, futures, crypto, and any asset class
    other than long-only equities. These are rejected by the domain layer, not
    risk-managed.

Policy integrity

The plugin does not expose a policy-editing tool and never accepts policy values
from the model. However, the policy file lives in the active Hermes profile. An
agent with unrestricted shell access under the same OS user could read or modify
it. Mitigations are OS-level: run Hermes as a dedicated user, restrict file
permissions (0600), use Hermes approvals for shell access, and keep the policy
outside any directory the agent can write.

Operational boundaries

  • No external API calls, telemetry, or network sockets.
  • No real broker integration in the MVP; execution is simulated in SQLite.
  • Paper results are not market data and do not prove a strategy is profitable.
  • The plugin is not a substitute for a broker's own risk controls or regulatory
    obligations.

Disclosure

What a user gets when installing the plugin:

BehaviourTradePreflight
Network calls to third-party servicesNone
Telemetry / analytics / usage reportingNone
Shell commands or subprocessesNone
Background or long-running processesNone
Stored credentialsNone — the plugin declares no requires_env and reads no credential store
Reads outside the plugin directoryNone
Reads inside the plugin directorypolicies/default.json, copied once into the plugin data directory to seed the operator policy
WritesOnly inside plugin_data_dir("trade-preflight"): the operator policy file and the SQLite audit/ledger database

The plugin is pure Python with zero third-party dependencies and makes no
outbound HTTP request of any kind.

Demo

./demo/run_demo.sh

One command, two halves:

  1. The offline story — the real plugin code driving the real gate
    against a temporary workspace. Six acts: a compliant order executes, a
    notional breach and a quantity breach are blocked, the same policy blocks
    the other broker's schema, garbage arguments and a broken policy are
    refused, and the audit trail and the paper ledger close the case.
    Ten tool calls, two handler invocations, two ledger rows.
  2. The live half — the exact prompts to paste into Hermes, where the same
    gate fires on a real conversation.

No Hermes, no network, no credentials, no broker account. The 90-second video
plan is in demo/STORYBOARD.md, and the story's claims
are asserted by tests/integration/test_demo_story.py, so the recording
cannot drift from the plugin's real behaviour.

Development

python -m compileall -q src
pytest
python demo/story.py
hermes plugins validate . --install-deps
hermes plugins doctor . --ci

CI runs the same commands on every push (see .github/workflows/ci.yml).

Repository conventions:

  • AGENTS.md is binding for all contributors.
  • Domain tests must cover every policy rule and every Decimal edge case.
  • tests/unit/ contains pure domain/adapter/engine tests.
  • tests/integration/ exercises hooks, tools, and SQLite together.
  • Do not add third-party financial libraries. The allowed dependency surface is
    the Python Standard Library.

Roadmap

SliceDeliverableState
0Interception gate, 100-share quantity check, handler counterDone
1NormalizedOrder, strict Decimal validation, Broker A/B adaptersDone
2Policy loading, schema validation, rule engineDone
3Paper ledger writes, outcome correlation, trade_auditDone
4Demo, hardening, full integration suiteDone

Allowed orders now write one row to paper_ledger and return a
{"status": "SUCCESS", ...} payload. Blocked orders leave the ledger unchanged.

License

MIT. See LICENSE.

README from the reviewed commit d5e7a1e; it updates when the author re-pins.

More by Mohamedhazeem

3 plugins by Mohamedhazeem in the catalog.

Developer Mode

Explore built-in Hermes toolsets and actions.

HermesAgent

Developer?

Create a Hermes extension for Desktop or agent workflows.

Developer Section