hermes atlas
apr·2026 117·repos hermes·v0.10.0 ★ star this repo

BORT-AGENTS/hermes-bort

Hermes Agent plugin for BORT (BAP-578) agent NFTs on BNB Smart Chain: multi-source reads, operator-signed writes, portable IPFS-anchored memory.

★ 2 langPython licenseMIT updated2026-05-18

hermes-bort is a plugin for Hermes Agent that enables interaction with BORT (BAP-578) agent NFTs on the BSC mainnet. It allows Hermes processes to read on-chain agent states, execute actions via operator-signed writes, and manage portable session memory anchored to IPFS. The plugin integrates with the hermes-agent-self-evolution optimizer to commit verifiable on-chain learning records and evolved skills directly to an agent's knowledge registry.

  • Reads full agent state, IPFS identity, and knowledge registries
  • Executes on-chain actions via VaultPermissionManagerV2 with local policy gating
  • Anchors local session memory to IPFS and KnowledgeRegistryV2
full readme from github

hermes-bort

PyPI Python License: MIT

A plugin for Hermes Agent that lets a Hermes process read, act for, and anchor memory on BORT (BAP-578) agent NFTs on BSC mainnet.

A BORT agent is an ERC-721 with on-chain state, an IPFS-pinned identity, a logic contract that exposes a small action catalog, and a knowledge registry. This plugin gives Hermes:

  • One-call multi-source reads of an agent's full state.
  • Owner-signed deep links into the BORT dapp for marketplace flows.
  • Operator-signed writes through VaultPermissionManagerV2.forwardHandleAction, gated by a local policy file and the Hermes approval prompt.
  • Portable session memory: write to local JSONL during a chat, anchor to IPFS and KnowledgeRegistryV2.addKnowledgeSourceDelegated at session end, prefetch those sources at the next session's start.

Install

pip install hermes-bort

Or install it straight from Hermes:

hermes plugins install BORT-AGENTS/hermes-bort

Either way, enable it in ~/.hermes/config.yaml:

plugins:
  enabled:
    - hermes-bort

From source

git clone https://github.com/BORT-AGENTS/hermes-bort
cd hermes-bort
pip install -e .[dev]

You can also drop the hermes_bort/ package into ~/.hermes/plugins/ directly.

Read-only quickstart

No setup beyond install. Reads hit the public BORT runtime API + BSC public RPC.

from hermes_bort.tools.read_agent import handle as read_agent
import asyncio, json

raw = asyncio.run(read_agent({"token_id": 11100}))
print(json.dumps(json.loads(raw), indent=2)[:800])

Inside Hermes the same call surfaces as a tool: bort_read_agent(token_id=11100).

Configure for writes

Writes need three things: an operator key, a vault grant from the agent owner, and permission from the local policy file.

1. Generate an operator key

hermes bort init-operator

Prints a fresh address + private key. Set BORT_OPERATOR_PRIVATE_KEY in your env and fund the address with ~0.01 BNB for gas. The key is never written to disk by the plugin and never leaves your process.

2. Write the default policy

hermes bort init-policy

Writes ~/.hermes/bort-policy.yaml. Each action has a disposition (auto / confirm / block) and an optional per_action_max_bnb cap. Defaults are conservative: exits auto, trades and campaign lifecycle confirm, governance and large transfers block.

3. Have the agent owner grant the operator

from hermes_bort.tools.grant_permission import handle as grant
raw = await grant({"token_id": 11100, "operator": "0xYourOperatorAddress"})

Returns the two-step calldata (createVault then grantPermission) the owner clicks through in their own wallet. Operator scope: WRITE, time-bound, limited to the actions you whitelist in the grant description.

4. Run

By default writes simulate only. Set BORT_ALLOW_BROADCAST=1 to enable real broadcasting once you have confirmed simulation looks right.

export BORT_ALLOW_BROADCAST=1

Doctor

hermes bort doctor

Walks the setup: RPC reachable, operator key valid and funded, policy file present and parseable, Pinata creds set if you plan to anchor memory, and the hermes-agent-self-evolution repo if you plan to use evolve.

Evolve

Run the hermes-agent-self-evolution optimizer for a skill and, if it improved, commit the result on-chain in one step:

hermes bort evolve github-code-review --token-id 11100

It runs the optimizer as a subprocess and, only if metrics.json shows a real improvement, chains two on-chain writes linked by one content hash:

  • commit_evolution pins the evolved skill to IPFS and writes it as an INSTRUCTION knowledge source on KnowledgeRegistryV2.
  • commit_learning emits a LearningRecorded event on the agent's logic contract: a permanent, timestamped, verifiable on-chain learning record.

The self-evolution repo is located via --repo, $BORT_SELF_EVOLUTION_REPO, ~/.hermes/hermes-agent-self-evolution, or a sibling directory. With BORT_ALLOW_BROADCAST unset the whole loop is a dry run (the skill is still pinned to IPFS; both chain writes only simulate).

Flag Effect
--iterations N GEPA iterations (default 10).
--optimizer-model M LLM for GEPA reflections, litellm format (anthropic/claude-sonnet-4-6, deepseek/deepseek-chat, openrouter/...). Default: the optimizer's own default.
--eval-model M LLM for evaluations, litellm format.
--repo PATH Path to the hermes-agent-self-evolution repo.
--commit-only Skip the optimizer; commit the latest existing run dir.
--only {both,evolution,learning} Which on-chain steps to run (default both).
--min-improvement F Only commit if metrics.improvement >= F.

LLM provider. The optimizer runs on litellm, so any provider works: --optimizer-model anthropic/claude-sonnet-4-6, deepseek/..., openrouter/..., etc. The provider's API key (ANTHROPIC_API_KEY, DEEPSEEK_API_KEY, ...) is read from the environment.

Without Pinata. commit_evolution pins the evolved skill to IPFS and needs Pinata credentials. commit_learning does not: record_learning only emits the content hash. So --only learning records the verifiable on-chain learning event with no IPFS and no Pinata key at all.

If commit_evolution succeeds but commit_learning fails, retry just the learning step: hermes bort evolve <skill> --token-id N --commit-only --only learning. Each run is a distinct on-chain event; the knowledge registry does not dedup.

Tools

Read

Tool What it does
bort_read_agent(token_id, include_logic_details=true, include_trades=true) Full state: BAP-578 state, IPFS identity, trade summary + PnL, knowledge sources, learning metrics, logic-specific state (Hunter positions / CTO campaign / Trading vault).
bort_health_check() CircuitBreaker preflight: global pause + per-contract pause for the active logic contracts. Cheap, mandatory before any write.
bort_list_actions(logic_name) Action catalog for "Hunter" / "Trading V5" / "CTO", split into on-chain and off-chain.
bort_marketplace_browse(sort?, limit?, offset?, seller?, include_recent_sales?) MarketplaceV3 listings + recent sales. No wallet.
bort_marketplace_agent(token_id, include_offers?, include_activity?) One agent's active listing, open offers, recent marketplace events.
bort_list_agent_uri(token_id, intent) Dapp deep link + step list for owner-signed actions. intent is list / manage / view / offers.

Write

All writes preflight globalPause, contract pause, VPMv2.canForward, policy disposition, and (when confirm) the Hermes approval prompt. With BORT_ALLOW_BROADCAST unset they return simulated_only.

Tool What it does
bort_grant_permission_uri(token_id, operator, ...) Builds the two owner-signed transactions to give the operator a WRITE grant on the agent's vault.
bort_commit_learning(token_id, data_hash, ...) Emits a LearningRecorded event on the logic contract via record_learning (Hunter / Trading V5 / CTO). A permanent on-chain learning record; mutates no score.
bort_invoke(token_id, action, args) Universal write: any catalogued handleAction through VPMv2.forwardHandleAction. 13 actions wired today.
bort_anchor_memory(token_id) Pins the agent's local JSONL memory buffer to IPFS and writes a MEMORY source through KnowledgeRegistryV2.addKnowledgeSourceDelegated.
bort_commit_evolution(token_id, output_dir) Anchors a hermes-agent-self-evolution run (output/<skill>/<timestamp>/evolved_skill.md + metrics.json) as an INSTRUCTION knowledge source.

Examples

Read an agent

from hermes_bort.tools.read_agent import handle as read_agent
import json, asyncio

state = json.loads(asyncio.run(read_agent({"token_id": 11100})))
print(state["on_chain"]["state"]["owner"])
print(state["logic"]["name"], state["logic"]["snapshot"])

Browse the marketplace

from hermes_bort.tools.marketplace_browse import handle as browse
import json, asyncio

raw = asyncio.run(browse({"sort": "price_asc", "limit": 10}))
for row in json.loads(raw)["listings"]:
    print(row["token_id"], row["price_per_token"], row["lister"])

Drive an action through the operator

Requires the agent owner to have already run bort_grant_permission_uri and signed the two grant transactions.

from hermes_bort.tools.invoke import handle as invoke
import json, asyncio

raw = asyncio.run(invoke({
    "token_id": 11100,
    "action": "buy_token",
    "args": {"token_address": "0x...", "amount_bnb_wei": "10000000000000000"},
}))
result = json.loads(raw)
print(result["status"])           # 'broadcast' / 'simulated_only' / 'blocked'
print(result.get("tx_hash"))

Anchor session memory to IPFS

After the agent owner has granted the operator a WRITE permission, this turns the local JSONL buffer into an on-chain MEMORY knowledge source the next session will prefetch.

hermes bort anchor-memory --token-id 11100

Generate a marketplace deep link

from hermes_bort.tools.list_agent_uri import handle as list_uri
import json, asyncio

raw = asyncio.run(list_uri({"token_id": 11100, "intent": "list"}))
print(json.loads(raw)["url"])     # https://www.bortagent.xyz/#/my-listings

Memory portability

BortMemoryProvider keys session memory by the agent's tokenId (passed to Hermes as agent_identity at session start).

  • During a chat: turns go into ~/.hermes/bort-memory/<tokenId>.jsonl.
  • On session end (or bort_anchor_memory): the buffer is pinned to IPFS via Pinata and committed to KnowledgeRegistryV2 as a MEMORY source through the operator's WRITE grant.
  • On next session start: the provider pulls active MEMORY sources from KnowledgeRegistry.getActiveKnowledgeSources(tokenId) and exposes them to prefetch.

The agent's memory travels with the NFT: a buyer who imports the same tokenId in their own Hermes instance picks up the same history without anyone sharing files.

Threat model

A BORT agent NFT carries text that the owner controls: the IPFS-pinned identity (name, description, attribute values), KnowledgeRegistry source descriptions, and the IPFS content of those sources. All of it can reach the LLM through bort_read_agent results and BortMemoryProvider.prefetch() output. Hermes itself does not sanitize tool returns or memory prefetch: whatever a plugin gives back is injected into context as-is.

The plugin treats those surfaces as untrusted and applies a narrow data-boundary layer in hermes_bort.bort_sanitize:

  • Free-form text fields (identity name / description / external_url, knowledge source description, prefetched memory blocks) are wrapped in an <external-data source="...">...</external-data> envelope with a one-line preamble telling the model to treat the contents as data, not instructions.
  • Each string is capped at 2 KB with an explicit [truncated, N chars total] marker so an attacker cannot flood the context.
  • C0 controls, ANSI escape sequences, zero-width characters, and bidi-override Unicode are stripped before wrapping (same character set Hermes' own cron scanner blocks).
  • Numeric ids, addresses, on-chain enums, and structural keys are not wrapped: only the strings that came from owner-controlled inputs.

This raises the bar from "trivial injection" to "the model has to ignore an explicit data-boundary marker." It is not a complete defense, and it is not meant to be: the operator-key boundary below is what binds what a tricked model can actually cause on-chain.

Security boundary

The plugin holds the operator key, not the NFT owner's key. The operator can only do what the owner has granted in VaultPermissionManagerV2 and what ~/.hermes/bort-policy.yaml allows. The Hermes approval prompt fires when an action's disposition is confirm. With BORT_ALLOW_BROADCAST unset, every write returns simulated_only.

A leaked operator key can be revoked by the owner with one revokePermission transaction in VPM v2; it cannot mint, transfer, or list the NFT. The plugin never writes the key to disk: hermes bort init-operator prints it once to stdout for the user to set in their env.

Configuration

Read from plugin.yaml or environment.

Setting Env var Default
Runtime API base BORT_API_URL https://bap578-nfa-platform.onrender.com
BSC RPC BSC_RPC_URL https://bsc-dataseed.binance.org
Dapp URL (deep links) BORT_DAPP_URL https://www.bortagent.xyz
Memory dir BORT_MEMORY_DIR ~/.hermes/bort-memory
Operator key BORT_OPERATOR_PRIVATE_KEY unset (writes disabled)
Allow broadcast BORT_ALLOW_BROADCAST unset (simulate only)
Policy file BORT_POLICY_PATH ~/.hermes/bort-policy.yaml
Pinata key PINATA_API_KEY unset (needed for anchor tools)
Pinata secret PINATA_API_SECRET unset (needed for anchor tools)

Test

BORT_TEST_TOKEN_ID=11100 pytest -q

99 tests. Most are integration-style: they hit BSC mainnet RPC and the BORT runtime API and verify the plugin works end-to-end against live infrastructure. Network tests are skipped or short-circuit when the relevant env var is unset (e.g. Pinata pin tools return a clear error without PINATA_API_KEY).

Architecture

hermes_bort/
  plugin.yaml                 metadata + config keys
  __init__.py                 register(ctx) entry point
  bort_chain.py               web3.py reads + VPM v2 / KR v2 ABIs
  bort_ipfs.py                Pinata pin + dual-gateway fetch (pinata, ipfs.io)
  bort_api.py                 async HTTP client to the BORT runtime API
  bort_logic_adapters.py      HunterAdapter / TradingV5Adapter / CTOAdapter
  bort_kr.py                  KR v2 delegated-write helper
  bort_marketplace.py         MarketplaceV3 constants + dapp deep-link builders
  action_codec.py             encode/decode the 13 handleAction payloads
  bort_signer.py              operator key load + broadcast
  bort_policy.py              ~/.hermes/bort-policy.yaml loader + writer
  approval.py                 Hermes approval prompt integration
  cli.py                      hermes bort init-operator / init-policy / doctor / anchor-memory / commit-evolution / evolve
  evolution_loop.py           self-evolution optimizer runner + on-chain commit chain
  tools/
    read_agent.py health_check.py list_actions.py
    marketplace_browse.py marketplace_agent.py list_agent_uri.py
    grant_permission.py commit_learning.py invoke.py
    anchor_memory.py commit_evolution.py
  memory/
    provider.py               BortMemoryProvider

The plugin is thin on purpose. BORT's runtime API at bap578-nfa-platform.onrender.com already exposes most data as public read endpoints; this plugin uses those instead of re-implementing them. Direct on-chain calls are reserved for what the runtime does not expose: tokenURI, KnowledgeRegistry.getActiveKnowledgeSources, CircuitBreaker.globalPause, the VPM v2 forwarder, and KR v2 delegated writes.

License

MIT. See LICENSE.