BORT (BAP-578) integration plugin for Hermes Agent
Project description
hermes-bort
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.addKnowledgeSourceDelegatedat 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.
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, claim_root, ...) |
Records a learning leaf on the logic contract via record_learning (Hunter / Trading V5 / CTO). |
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 toKnowledgeRegistryV2as aMEMORYsource through the operator's WRITE grant. - On next session start: the provider pulls active
MEMORYsources fromKnowledgeRegistry.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 sourcedescription, 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
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hermes_bort-0.1.1.tar.gz.
File metadata
- Download URL: hermes_bort-0.1.1.tar.gz
- Upload date:
- Size: 72.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be633a5edf202f38b536775125013a0664473e5fed3125cf406417d0d26cab46
|
|
| MD5 |
ad556bb5462551f676dc3d7b0e6e8d6f
|
|
| BLAKE2b-256 |
5b0d63b23d94030b57549247e5f94cd7a5e257ad968b5a7ab28ddde8e6f02b87
|
File details
Details for the file hermes_bort-0.1.1-py3-none-any.whl.
File metadata
- Download URL: hermes_bort-0.1.1-py3-none-any.whl
- Upload date:
- Size: 67.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e606dcf85867ef89f99f6cd491fc2b20d050a3bdf705e114a850d5a3124f0123
|
|
| MD5 |
05c0183f7c583667facb76f8df1d5f89
|
|
| BLAKE2b-256 |
b4ef91deed23a6f66004d9a899bc6b73b55ab09b5ec3e6dbac9b3ab1ac660959
|