feat(agent): add dedicated crypto_agent for wallet & market ops (#1397) (#1736)

This commit is contained in:
Steven Enamakel
2026-05-14 11:35:33 -07:00
committed by GitHub
parent 4d5e0d5aad
commit 38924e37cc
9 changed files with 422 additions and 1 deletions
+15
View File
@@ -887,6 +887,21 @@ const CAPABILITIES: &[Capability] = &[
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "automation.crypto_agent",
name: "Crypto Agent",
domain: "automation",
category: CapabilityCategory::Automation,
description: "Dedicated wallet & market specialist sub-agent. The orchestrator \
routes transfers, swaps, contract calls, balance lookups, and \
exchange trading requests here. The agent enforces a read → \
simulate → confirm → execute flow, refuses to fabricate chain ids \
or token addresses, and gates every write call behind explicit \
user confirmation.",
how_to: "Automatic — invoked by the orchestrator when a crypto wallet or market action is requested. Connect a wallet via Settings > Recovery Phrase first.",
status: CapabilityStatus::Beta,
privacy: LOCAL_CREDENTIALS,
},
Capability {
id: "automation.welcome_agent",
name: "Welcome Message",
@@ -0,0 +1,63 @@
id = "crypto_agent"
display_name = "Crypto Agent"
delegate_name = "do_crypto"
when_to_use = "Crypto wallet & market specialist — drives wallet balances, quotes, transfers, swaps, contract calls, and exchange trading workflows on the user's connected wallet identities. Use whenever a request is about on-chain balances, sending/receiving tokens, swapping or routing across chains, signing a contract call, or executing/inspecting market orders on a connected crypto exchange. Always read/simulate before signing; refuse to proceed on missing wallet setup, missing chain, missing liquidity, or unconfirmed intent."
temperature = 0.2
max_iterations = 8
sandbox_mode = "none"
# Crypto agent has a tight single-purpose voice and gets its own safety
# rules from the prompt body — the global identity/skills boilerplate
# would dilute them, but the standard safety preamble stays on as a
# belt-and-suspenders gate on financial-risk actions.
omit_identity = true
omit_memory_context = true
omit_safety_preamble = false
omit_skills_catalog = true
[model]
hint = "agentic"
[tools]
# Narrow allowlist. Wallet + market primitives only — no shell, no
# file_write, no broad HTTP, no integration delegation. Names line up
# with the wallet RPC controllers in `src/openhuman/wallet/schemas.rs`
# (read: status/balances/supported_assets/chain_status; quote/prepare:
# prepare_transfer/prepare_swap/prepare_contract_call; execute:
# execute_prepared) and the financial-apis crypto/FX series exposed
# via `stock_*`. Tools that aren't yet registered are silently dropped
# by the tool filter at spawn time, so this list also describes the
# agent's *intended* tool surface as wallet + exchange tools land.
named = [
# Read-only wallet inspection.
"wallet_status",
"wallet_balances",
"wallet_supported_assets",
"wallet_chain_status",
# Quote / prepare. These MUST be called before any execute_prepared
# — they return a deterministic transaction blob plus fee/slippage
# estimates the agent shows the user for confirmation.
"wallet_prepare_transfer",
"wallet_prepare_swap",
"wallet_prepare_contract_call",
# Sign + broadcast a previously-prepared blob. Refuses to fabricate
# parameters — only consumes a `prepared_id` returned by the
# matching `wallet_prepare_*` call earlier in this turn.
"wallet_execute_prepared",
# Market data — crypto series, FX rates, commodities — for grounding
# quote sanity checks and price questions before any execute step.
"stock_quote",
"stock_exchange_rate",
"stock_crypto_series",
# Memory recall lets the agent ground execution in the user's
# previously-stated preferences (default chain, slippage tolerance,
# named addresses) instead of re-asking every time.
"memory_recall",
# Confirmation gate — the agent MUST call this before any
# wallet_execute_prepared / write-side exchange order. The runtime
# routes the prompt to the user and blocks until they reply.
"ask_user_clarification",
# Time grounding for "as of <when>" framing and freshness checks on
# quotes before execute.
"current_time",
]
@@ -0,0 +1 @@
pub mod prompt;
@@ -0,0 +1,67 @@
# Crypto Agent
You are the **Crypto Agent** — OpenHuman's specialist for wallet and market operations on the user's connected crypto identities. Every action you take moves real money, so your default posture is **read, simulate, confirm, then execute**.
## What you handle
- Reading balances, positions, supported chains and assets across the user's connected wallet identities (EVM, BTC, Solana, Tron, …).
- Quoting transfers, swaps and contract calls; surfacing fees, slippage and the destination route.
- Executing **only the exact blob** that was returned from a matching `wallet_prepare_*` call earlier in this turn — never a parameter set you invented.
- Pulling crypto / FX market data to sanity-check a quote before signing.
- Pointing the user back to **Settings → Connections** when a chain, exchange, or wallet identity isn't set up.
## What you do NOT handle
- Generic web research, news summaries, regulatory analysis — defer to the researcher.
- Code writing, file edits, shell access, broad HTTP. You have no shell, no file_write, no curl.
- Service integrations like Gmail / Notion / Slack — delegate via the orchestrator.
- Autonomous background trading. You only act on an in-band user instruction with an explicit confirmation.
## Hard rules
1. **No fabrication.** Never invent chain ids, token contract addresses, market symbols, fee values, slippage numbers, exchange order ids, or tool names. If you don't have it from a tool result or the user, ask. If a tool isn't in your tool list, say so — do not pretend it exists.
2. **Read before write.** Before any `wallet_prepare_*` call, confirm the relevant balance / chain status with `wallet_balances` / `wallet_chain_status` (or a recent earlier-in-turn result). Before any `wallet_execute_prepared`, confirm the freshness of the prepared blob with `current_time` — re-prepare if the quote is older than ~60s.
3. **Quote before execute.** A `wallet_execute_prepared` call MUST be preceded by a matching `wallet_prepare_*` call **in this same turn**, and the `prepared_id` you pass MUST be the one that call returned. No exceptions.
4. **Confirm before execute.** Before calling `wallet_execute_prepared` (or any write-side exchange order), call `ask_user_clarification` with a tight summary: `from → to`, asset + amount, chain, fee, slippage, and any non-obvious detail (bridging, approval first, etc.). Only proceed on an explicit yes.
5. **Stop cleanly on missing setup.** If a wallet identity, chain, exchange connection, or required auth is missing, do not retry, do not guess. Say which thing is missing, point to **Settings → Connections** (or **Settings → Recovery Phrase** for wallet identities), and stop.
6. **Stop cleanly on insufficient liquidity / balance.** If a quote fails for liquidity, slippage, or balance reasons, surface the reason verbatim, suggest the smallest viable adjustment (lower amount, different route), and wait for the user.
7. **Never log secrets.** Do not echo private keys, seed phrases, mnemonics, exchange API secrets, or signed transaction payloads in your replies. Quote the public address and the prepared id, nothing more.
## Standard flow
1. **Frame the intent.** Restate the request in one short sentence: who pays, what asset, on which chain, to whom, why. If anything is ambiguous (chain, asset, recipient), ask once with `ask_user_clarification`.
2. **Inspect.** `wallet_status` + `wallet_balances` (and `wallet_chain_status` for the target chain) to confirm the account exists, has the asset, and the chain is reachable. For market questions, `stock_crypto_series` / `stock_exchange_rate` to ground the answer.
3. **Quote.** Call the right `wallet_prepare_*` once. Inspect fees, slippage, route. If anything is wildly off (slippage > a sensible bound, fee > a sensible fraction of the transfer, route involves unexpected hops), surface it as a concern, not a fait accompli.
4. **Confirm.** Summarise the prepared transaction and call `ask_user_clarification`. Show: source identity (truncated address), destination (full address + label if known), asset + amount, native fee, slippage, est. landing time, prepared id.
5. **Execute.** On explicit confirmation, call `wallet_execute_prepared` with the exact `prepared_id`. Report back the broadcast result (tx hash / order id), and the chain explorer URL only if the tool returned one — do not synthesise explorer links from the hash.
6. **On failure.** Show a **sanitized** summary of the tool's error — never echo raw payloads, signed transaction blobs, full RPC responses, stack traces, request ids, or any field that could embed a secret. Redact long opaque tokens to a short prefix (e.g. `0xfee…dead`). Then name the likely cause in one line (e.g. "RPC rejected — nonce gap", "insufficient gas"), and stop. Do not auto-retry write operations.
## Output shape
Keep replies tight and grounded.
> checking balances on eth
>
> you've got 2.43 ETH on ethereum. quote for the 0.5 ETH transfer to `0xabc…123` is:
>
> - fee: ~0.0012 ETH (~$3)
> - eta: ~12s
> - prepared id: `tx_8f2…`
>
> ok to send?
After execution:
> sent. tx `0xfee…dead` — confirmed in block 19,422,118.
On a missing prerequisite:
> no solana identity set up yet — head to **Settings → Recovery Phrase** to derive one, then ping me back.
On a failed quote:
> swap quote failed: slippage would exceed 5% on this route. try a smaller amount or a different DEX route.
## Why this prompt exists
The orchestrator delegates crypto work here precisely because generic agents over-assume tool availability and under-confirm financial intent. **Your value is caution, not breadth.** When in doubt, stop and ask.
@@ -0,0 +1,133 @@
//! System prompt builder for the `crypto_agent` built-in agent.
//!
//! Crypto Agent is a narrow-scope, write-capable specialist. The body
//! is the archetype's read/simulate/confirm/execute contract, followed
//! by the standard tool + workspace blocks so the model sees the
//! `wallet_*` / `stock_*` schemas the runtime injected. Identity,
//! skills catalogue and global memory context are omitted — they would
//! dilute the financial-safety voice the archetype establishes.
use crate::openhuman::context::prompt::{
render_safety, render_tools, render_user_files, render_workspace, PromptContext,
};
use anyhow::Result;
const ARCHETYPE: &str = include_str!("prompt.md");
pub fn build(ctx: &PromptContext<'_>) -> Result<String> {
tracing::debug!(
agent_id = ctx.agent_id,
model = ctx.model_name,
tool_count = ctx.tools.len(),
skill_count = ctx.skills.len(),
"[agent_prompt][crypto_agent] build_start"
);
let mut out = String::with_capacity(8192);
out.push_str(ARCHETYPE.trim_end());
out.push_str("\n\n");
let user_files = render_user_files(ctx)?;
let user_files_present = !user_files.trim().is_empty();
if user_files_present {
out.push_str(user_files.trim_end());
out.push_str("\n\n");
}
let tools = render_tools(ctx)?;
let tools_present = !tools.trim().is_empty();
if tools_present {
out.push_str(tools.trim_end());
out.push_str("\n\n");
}
let safety = render_safety();
out.push_str(safety.trim_end());
out.push_str("\n\n");
let workspace = render_workspace(ctx)?;
let workspace_present = !workspace.trim().is_empty();
if workspace_present {
out.push_str(workspace.trim_end());
out.push('\n');
}
tracing::trace!(
agent_id = ctx.agent_id,
prompt_len = out.len(),
user_files_present,
tools_present,
workspace_present,
"[agent_prompt][crypto_agent] build_done"
);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat};
use std::collections::HashSet;
fn empty_ctx() -> PromptContext<'static> {
use std::sync::OnceLock;
static EMPTY_VISIBLE: OnceLock<HashSet<String>> = OnceLock::new();
PromptContext {
workspace_dir: std::path::Path::new("."),
model_name: "test",
agent_id: "crypto_agent",
tools: &[],
skills: &[],
dispatcher_instructions: "",
learned: LearnedContextData::default(),
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
tool_call_format: ToolCallFormat::PFormat,
connected_integrations: &[],
connected_identities_md: String::new(),
include_profile: false,
include_memory_md: false,
curated_snapshot: None,
user_identity: None,
}
}
#[test]
fn build_returns_nonempty_body() {
let body = build(&empty_ctx()).unwrap();
assert!(!body.is_empty());
assert!(body.contains("Crypto Agent"));
}
#[test]
fn build_enforces_read_simulate_confirm_execute() {
let body = build(&empty_ctx()).unwrap();
// The four phases must all be visible in the prompt — the agent's
// entire safety story rests on them.
assert!(
body.contains("read, simulate, confirm, then execute")
|| body.contains("read/simulate/confirm/execute"),
"prompt must spell out the read→simulate→confirm→execute contract"
);
assert!(
body.contains("ask_user_clarification"),
"prompt must require explicit user confirmation before execute"
);
assert!(
body.contains("prepared_id"),
"execute step must consume a prepared_id, not fabricated parameters"
);
}
#[test]
fn build_forbids_fabrication_and_logging_secrets() {
let body = build(&empty_ctx()).unwrap();
assert!(
body.contains("No fabrication"),
"prompt must explicitly forbid fabricating chain/token/market params"
);
assert!(
body.contains("Never log secrets") || body.contains("never log secrets"),
"prompt must forbid echoing private keys / seed phrases"
);
}
}
+135 -1
View File
@@ -77,6 +77,11 @@ pub const BUILTINS: &[BuiltinAgent] = &[
toml: include_str!("integrations_agent/agent.toml"),
prompt_fn: super::integrations_agent::prompt::build,
},
BuiltinAgent {
id: "crypto_agent",
toml: include_str!("crypto_agent/agent.toml"),
prompt_fn: super::crypto_agent::prompt::build,
},
BuiltinAgent {
id: "tools_agent",
toml: include_str!("tools_agent/agent.toml"),
@@ -177,7 +182,7 @@ mod tests {
fn all_builtins_parse() {
let defs = load_builtins().expect("built-in TOML must parse");
assert_eq!(defs.len(), BUILTINS.len());
assert_eq!(defs.len(), 15, "expected 15 built-in agents");
assert_eq!(defs.len(), 16, "expected 16 built-in agents");
}
#[test]
@@ -497,6 +502,135 @@ mod tests {
}
}
/// Crypto Agent (#1397) is the dedicated specialist for wallet
/// actions and market operations. It must have a *narrow* tool
/// allowlist (no shell, no file_write, no broad HTTP), MUST keep
/// the safety preamble on (financial-risk gate), and MUST require
/// quote/confirm-before-execute via `ask_user_clarification`.
#[test]
fn crypto_agent_has_narrow_wallet_market_tools_and_safety_on() {
let def = find("crypto_agent");
// Hint must be agentic — the agent reasons about quotes vs.
// executes across multiple tool calls per turn.
assert!(matches!(def.model, ModelSpec::Hint(ref h) if h == "agentic"));
assert_eq!(def.sandbox_mode, SandboxMode::None);
// Financial-risk agent — global safety preamble stays ON.
assert!(
!def.omit_safety_preamble,
"crypto_agent must keep the global safety preamble — financial-risk gate"
);
match &def.tools {
ToolScope::Named(tools) => {
// Wallet read surface.
for required in [
"wallet_status",
"wallet_balances",
"wallet_supported_assets",
"wallet_chain_status",
] {
assert!(
tools.iter().any(|t| t == required),
"crypto_agent needs read tool `{required}`"
);
}
// Quote / prepare surface.
for required in [
"wallet_prepare_transfer",
"wallet_prepare_swap",
"wallet_prepare_contract_call",
] {
assert!(
tools.iter().any(|t| t == required),
"crypto_agent needs prepare tool `{required}`"
);
}
// Execute surface — gated by the prepared blob from a
// matching prepare_* call in the same turn.
assert!(
tools.iter().any(|t| t == "wallet_execute_prepared"),
"crypto_agent needs wallet_execute_prepared"
);
// Confirmation gate — MUST be present so the prompt's
// "confirm before execute" rule is mechanically enforceable.
assert!(
tools.iter().any(|t| t == "ask_user_clarification"),
"crypto_agent needs ask_user_clarification to gate write ops"
);
// Market grounding + context helpers. Pin the full set so a
// TOML edit that silently drops `stock_quote`,
// `stock_exchange_rate`, `memory_recall`, or `current_time`
// gets caught here — the agent's quote-before-execute
// discipline and "ground in user preferences before re-asking"
// behaviour both depend on these being present.
for required in [
"stock_quote",
"stock_exchange_rate",
"stock_crypto_series",
"memory_recall",
"current_time",
] {
assert!(
tools.iter().any(|t| t == required),
"crypto_agent needs supporting tool `{required}`"
);
}
// Hard exclusions — no broad-surface or write-anywhere tools.
// Includes the orchestrator-level delegate_* tools so a future
// TOML edit can't accidentally hand crypto writes to the
// generic integrations or code-execution paths.
for forbidden in [
"shell",
"file_write",
"curl",
"http_request",
"composio_execute",
"composio_list_tools",
"spawn_subagent",
"spawn_worker_thread",
"delegate_to_integrations_agent",
"delegate_run_code",
"delegate_research",
"delegate_plan",
] {
assert!(
!tools.iter().any(|t| t == forbidden),
"crypto_agent must NOT have `{forbidden}` — keeps blast radius bounded"
);
}
}
ToolScope::Wildcard => panic!("crypto_agent must have a Named tool scope"),
}
// Keep iteration cap tight — quote → confirm → execute is a
// 3-step loop, not a research crawl.
assert!(
def.max_iterations <= 10,
"crypto_agent max_iterations must stay tight (got {})",
def.max_iterations
);
assert!(def.omit_identity);
assert!(def.omit_memory_context);
assert!(def.omit_skills_catalog);
}
/// Routing: the orchestrator must list `crypto_agent` in its
/// `subagents` so a `delegate_do_crypto` tool is synthesised at
/// agent-build time. Without this entry the orchestrator can't
/// route crypto-shaped requests to the specialist.
#[test]
fn orchestrator_subagents_include_crypto_agent() {
use crate::openhuman::agent::harness::definition::SubagentEntry;
let def = find("orchestrator");
let listed = def.subagents.iter().any(|e| match e {
SubagentEntry::AgentId(id) => id == "crypto_agent",
_ => false,
});
assert!(
listed,
"orchestrator.subagents must list `crypto_agent` so the \
routing layer can synthesise `delegate_do_crypto`"
);
}
#[test]
fn welcome_has_onboarding_and_memory_tools() {
let def = find("welcome");
+1
View File
@@ -7,6 +7,7 @@ mod loader;
pub mod archivist;
pub mod code_executor;
pub mod critic;
pub mod crypto_agent;
pub mod help;
pub mod integrations_agent;
pub mod morning_briefing;
@@ -44,6 +44,12 @@ subagents = [
"tools_agent",
"critic",
"archivist",
# Crypto wallet & market specialist (#1397). Synthesised into a
# `delegate_do_crypto` tool at agent-build time. Route any wallet,
# transfer, swap, contract-call, or exchange trading request here —
# the agent enforces a strict read → simulate → confirm → execute
# contract that the generic delegation surface does not.
"crypto_agent",
# NOTE: `summarizer` used to be listed here for the runtime-only
# oversized-tool-result hook. That path is currently disabled
# (`context.summarizer_payload_threshold_tokens = 0`) after recursive
@@ -26,6 +26,7 @@ Follow this sequence for every user message:
- Yes: use direct tools (`current_time`, `cron_*`, `memory_*`, `composio_list_connections`, etc.).
- No: continue.
4. **Does this need other specialised execution?**
- If the request is about a **crypto wallet or market action** — balances, transfers, swaps, contract calls, on-chain positions, or trading on a connected exchange — use `delegate_do_crypto`. It enforces read → simulate → confirm → execute and refuses to fabricate chain ids, token addresses, market symbols, or unsupported tools. **Do not** route crypto write operations through `delegate_to_integrations_agent` or `delegate_run_code`.
- If code writing/execution/debugging is required, use `delegate_run_code`.
- If web/doc crawling is required, use `delegate_researcher`.
- If complex multi-step decomposition is required, use `delegate_plan`.