安全与沙盒
沙盒、权限等级、钩子管道,以及 6 阶段资金安全栈
本页介绍交易执行和沙盒机制。面向普通用户的 Chat 提醒请参阅财务安全提醒。
Minara 在两个层面实施安全保障:适用于所有工具调用的系统级隔离与权限门控,以及适用于交易路径的金融专项安全栈。
Minara Agent 的输出仅供参考,不构成金融或投资建议。在运行软件、授权交易或使用策略前,请阅读完整项目免责声明。在适用法律允许的最大范围内,Minara.AI 不对因使用本项目而产生或与使用本项目有关的损失负责。
为何资金安全需要 6 个阶段? 每个阶段针对不同类型的错误。LLM 可能幻觉一个代币名称、遗忘用户偏好、遭到提示词注入,或持有过时的价格数据。将 6 个成本低且相互独立的检查叠加在一起,意味着一个错误必须突破所有关卡才能真正动用资金。单一的"LLM 判断"检查成本更高,且会遗漏一行类型守卫就能拦截的分类错误。
本页涵盖两个层面,从基础开始介绍。
📘 面向操作员的配套说明请参阅 安全 章节。 该章节讲解 4 层独立的安全机制(command-guard、OS jail、fund-moving confirm、script-risk gate)在日常使用中如何呈现、各自防什么。本页是 实现参考;那一章是用户使用层的讲解。
实际应用参考:每个涉及资金的功能页面(交易、投资组合、预测)均链接至此。首次交易演示中展示了面向用户的预览步骤(6 阶段安全栈中的第 3 阶段)。
基础:沙盒、权限等级与钩子管道
每次工具调用都会经过两个相互独立的层:沙盒(文件系统隔离,即工具能够访问的范围);权限等级加钩子管道(行为门控,即工具当前被允许执行的操作)。
沙盒
所有文件系统工具均以 $dataDir/sandbox/files/(默认:~/.minara/sandbox/files/)为根目录。每个路径参数都经过 apps/agent/src/tools/_shared/sandbox.ts 中的 resolveInSandbox() 处理,具体步骤如下:
- 将请求路径解析至沙盒根目录。
- 拒绝包含
..且会逃逸根目录的路径。 - 解析符号链接,拒绝指向沙盒外部的链接。
- 返回工具可安全使用的绝对解析路径。
工具在物理层面无法读写 apps/agent/src/、用户主目录或沙盒之外的任何位置。此规则适用于 read_file、write_file、patch、search_files 及所有文件相关工具。
Shell 出口也受到锁定。 terminal 工具会从每个子进程中清除 38 个凭据前缀,shell 包装器出口(bash -c curl 等)默认禁止。
权限等级
每个 ToolEntry 携带一个 permissionTier:
| 等级 | 名称 | 示例 |
|---|---|---|
| 1 | READ_ONLY | get_price、get_balance、read_file、web_search |
| 2 | CONFIRM_ONCE | analyze_market、deep_research_run、小额闪兑 |
| 3 | ALWAYS_CONFIRM | write_file、patch、buy_token、docx_create |
| 4 | MANUAL_ONLY | 向外部地址 transfer_token、切换紧急停止开关 |
钩子管道
每次工具调用前,全局 BeforeToolCallHook 会按以下顺序执行:
- 紧急停止开关。 若
safetyConfig.killSwitch已激活,所有交易工具(等级 ≥ 2)均被阻止。 - 每日消费上限。 累计涉及资金的交易量会与
safetyConfig.dailySpendCap进行比对。 - 单笔上限。 单笔交易金额与
safetyConfig.perTxMax进行比对。 - 等级门控。 在自主轮次(无用户参与)中,除非
safetyConfig.autopilotEnabled为true,否则等级 3 及以上工具均被阻止。 - 仅限手动执行。 等级 4 工具始终需要用户确认的往返同步。
状态存储在 SQLite 的 audit_log 表中。每次调用均记录其推理过程、参数、结果及钩子决策。不会有任何内容被静默删除。
提示词注入防御
Agent 通过以下机制防御来自市场数据、工具输出和记忆召回的提示词注入:
- 分隔符隔离。 不可信内容被随机唯一分隔符包裹,并告知 LLM 不得遵循其中的任何指令。
- 零宽字符清除。 移除 ZWSP、ZWJ、RLO 等字符。
- 内容扫描。 内容进入提示词前,与经整理的注入载荷数据库进行正则匹配。
参见 apps/agent/src/core/prompt-builder.ts 和 tests/unit/prompt-injection.test.ts。
金融专项安全栈
权限等级钩子链拦截明显的错误调用。金融专项安全栈拦截隐性的错误交易:以 50% 滑点执行的闪兑、单一代币 5 倍仓位、一有回调即爆仓的 15 倍杠杆永续合约。该模块位于 apps/agent/src/finance/,将六个可独立测试的组件组合成完整的交易安全路径。
安全栈结构
trade intent
│
▼
┌───────────────┐
│ token-safety │ scam detection, canonical address, chain resolution
└───────┬───────┘
▼
┌───────────────┐
│ position-sizing│ fixed_usd / fixed_fraction / half_kelly
└───────┬───────┘
▼
┌───────────────┐
│ exposure-limits│ per-token / per-chain / per-asset-class caps
└───────┬───────┘
▼
┌───────────────┐
│ slippage │ simulate → check price impact → reject if too high
└───────┬───────┘
▼
┌───────────────┐
│ risk-manager │ per-tx max, daily cap, emergency stop (atomic debit)
└───────┬───────┘
▼
SafeTradingClient → Minara backend
│
▼
┌───────────────┐
│ stop-loss │ periodic workflow closes positions on exit trigger
└───────────────┘每个阶段都是独立模块。顶层 full-risk-manager.ts 负责组合,而非膨胀为单体。这种组合设计是核心所在:任何一个组件都可以替换或扩展,无需改动其他部分。
risk-manager.ts:最低基础防线
apps/agent/src/finance/risk-manager.ts 是始终运行的第一阶段基础防线,包含三个硬约束:
- 单笔交易上限(默认
$500)。拒绝estimated_value_usd超过上限的交易。 - 每日消费上限(默认
$2,000)。使用BEGIN IMMEDIATE对daily_spendSQLite 表进行原子检查和扣减,避免并发交易通过竞争超出上限。 - 紧急停止开关。 激活后,所有等级 2 及以上的交易均被拒绝,直至调用
/unkill。
单笔上限和每日上限由 ~/.minara/settings.json 中 safety 区段的字段控制:
{
"maxTransactionAmount": 500,
"dailySpendCap": 2000,
"killSwitchActive": false,
"allowedTokens": [],
"blockedTokens": ["SQUID", "SAFEMARS"]
}白名单默认为空(即所有代币均可通过)。黑名单始终强制执行。可通过 minara config set safety.maxTransactionAmount 1000 修改。
token-safety.ts:入口检查
以优先级 50 作为交易钩子运行,在权限等级钩子之前执行。负责三项职责:
- 规范地址解析。 当用户说"在 Arbitrum 买 USDT"时,钩子从
CANONICAL_ADDRESSES中查找规范地址,确保交易发送至真实的 USDT 合约,而非使用相同 ticker 的仿冒合约。 - 已知欺诈检测。 维护一组经过整理的代币 ticker 和地址,无论白名单/黑名单配置如何,均予以硬性拦截。该集合存储在源代码中,添加条目需提交 PR。
- 链解析。 将模糊的链标识符("eth"、"ethereum"、"mainnet")映射为 Minara 后端所需的规范链 ID。
若代币无法解析,交易将以结构化错误被拒绝,LLM 可据此说明原因("在 'ethereum' 上无法识别 'SAFEMARS' 为规范代币,且与欺诈列表匹配")。
position-sizing.ts:仓位大小
三种策略,通过 ~/.minara/settings.json 中的 safety.sizing.strategy 选择:
fixed_usd
size_usd = min(fixedAmountUsd, max_transaction_usd)确定性策略。"始终交易 $100。"适合定投 (DCA) 工作流。
fixed_fraction
size_usd = min(portfolio_value_usd * fraction, max_transaction_usd)按投资组合比例。"始终使用 2% 的权益。"适合风险平价配置,随着投资组合缩水,每笔交易金额自动下调。
half_kelly
f_star = (p * b - q) / b // p = win prob, q = 1 - p, b = payoff ratio
size_usd = portfolio_value_usd * f_star * kellyMultiplier // default 0.5基于优势和方差的最优下注规模。默认使用半 Kelly,因为完整 Kelly 在优势真实存在时也会产生剧烈回撤。需要 LLM 提供 win_probability 和 payoff_ratio 参数;若任一缺失,则回退至 fixed_fraction。
每种策略的计算结果始终受单笔交易上限约束。SizingDecision.clipped 标志记录计算结果是否触及上限,审计查询可据此了解上限的实际约束情况。
exposure-limits.ts:集中度控制
基于当前投资组合(而非历史数据)计算三层敞口限制:
interface ExposureLimitsConfig {
maxPerTokenUsd: number; // default $5,000
maxPerChainUsd: number; // default $15,000
maxPerAssetClassFraction: number; // default 0.40 (40% in any one class)
maxTotalExposureUsd: number; // default $50,000
}敞口检查在交易执行前运行。若新交易会使敞口超过四个限制中的任一项,钩子将以具体违反的上限为由拒绝执行。
资产类别由 learning/methodology-store.ts → classifyAsset 计算,与技能系统的资产类别分类法一致。因此,在路由器中标记为 crypto_meme 的交易,在敞口检查中同样标记为 crypto_meme。统一的词汇表使得"meme 币不超过 40%"无需任何胶水代码即可强制执行。
slippage-protection.ts:价格影响门控
大额交易适用更严格的滑点预算:
| 交易规模 | 最大价格影响 |
|---|---|
| 低于 $1,000 | 2.0% |
| $1,000 – $10,000 | 1.0% |
| 超过 $10,000 | 0.5% |
分级结构既能防止小额交易承受 5% 的价格影响(令人恼火但可恢复),也能防止大额交易承受 1%(对于鲸鱼级订单可能造成灾难性损失)。上述阈值均为默认值,每个字段均可在 ~/.minara/settings.json 的 safety 区段下配置。
执行闪兑前,钩子调用 Minara 后端的 /v1/tx/cross-chain/swaps-simulate 接口获取预估输出和价格影响。若影响超过该等级上限,交易将被拒绝。此时实际闪兑尚未发生,模拟成本低廉,拒绝操作干净利落。
永续合约杠杆上限
maxLeverage: 10硬约束。leverage: 15x 的永续合约订单将以 leverage_exceeds_cap 被拒绝。上限以单笔交易为单位;目前尚无投资组合级别的杠杆汇总(若需要,可在敞口限制器中添加)。
最低输出比率
minOutputRatio: 0.95 // expect at least 95% of input USD out针对输出异常偏低的闪兑报价的最后一道检查。就算价格影响看起来正常,若模拟显示"$100 的交易只会得到 $40",该检查仍会将其拦截。
stop-loss.ts:退出管理
仓位级别的止损规则,共三种类型:
fixed_percent
当价格从入场价下跌 threshold_pct 时退出。
{ type: "fixed_percent", threshold_pct: 0.10 } // 10% drop triggers exittrailing
当价格从入场后最高点下跌 threshold_pct 时退出,而非从入场价计算,可锁定收益。
{ type: "trailing", threshold_pct: 0.08 } // 8% drawdown from hightime_based
无论盈亏,在固定时长后退出。
{ type: "time_based", max_age_ms: 86400000 } // 24 hours三种类型均支持可选的 take_profit_pct,可在盈利时同样触发平仓。
运行机制
止损规则由周期性工作流(workflow/templates/stop-loss-monitor.ts)检查。该工作流按计划轮询开放仓位,判断各触发条件是否满足,并通过常规交易路径下达平仓订单。
这一点至关重要:止损模块本身不会直接调用交易后端,只负责决策。平仓订单仍会经过完整安全栈(权限等级、每日上限、滑点检查等所有环节)。止损在交易所故障期间尝试退出时,同样遵守所有门控,不享有任何专用旁路。
组合:FullRiskManager
full-risk-manager.ts 将所有组件连接为一个交易钩子:
new FullRiskManager(db, safetyConfig, {
sizing: DEFAULT_SIZING_CONFIG,
exposure: DEFAULT_EXPOSURE_LIMITS,
slippage: DEFAULT_SLIPPAGE_CONFIG,
});组合方式刻意保持明确。可以替换任意组件(自定义仓位策略、为保守用户设置更严格的敞口限制、在测试中禁用滑点检查),无需改动其他部分。底层风险管理器始终运行,这是最低基础防线。
配置入口
安全配置存储在 ~/.minara/settings.json 的 safety 区段中,可通过 minara config 修改:
minara config list # show every field
minara config get safety.sizing.strategy
minara config set safety.sizing.strategy half_kelly
minara config set safety.exposure.maxPerTokenUsd 2500
minara config set safety.slippage.largeTradeMaxImpact 0.003验证在读取时进行:无效值(负数上限、杠杆 > 100、threshold_pct > 1.0)会使安全配置加载器回退至默认值并向结构化日志输出警告。不存在以无效配置启动的情况。
可观测性
每次交易钩子决策均记录在两张表中:
audit:工具调用、参数、结果。tier_events:触发的钩子及原因。
常用排查查询:
-- Why was my swap rejected yesterday?
SELECT a.tool_name, te.decision, te.reason, a.created_at
FROM audit a
LEFT JOIN tier_events te ON te.trace_id = a.trace_id
WHERE a.tool_name IN ('swap', 'buy', 'sell')
AND a.blocked = 1
AND date(a.created_at) = '2026-04-14';
-- How often are sizing caps binding?
SELECT COUNT(*) FROM audit
WHERE tool_set = 'trade'
AND json_extract(result_json, '$.sizing.clipped') = 1;
-- Which tokens hit exposure limits most often?
SELECT json_extract(args_json, '$.token'), COUNT(*)
FROM audit
WHERE block_reason LIKE '%exposure%'
GROUP BY 1 ORDER BY 2 DESC LIMIT 10;扩展安全栈
- 自定义仓位策略。 在
SizingStrategy中添加新变体,在PositionSizer中为其实现computeSize,并在 env-vars 及本页记录新的sizing.*配置字段。 - 新增敞口维度。 在
ExposureLimitsConfig中添加字段,在ExposureLimiter.check中实现检查,并在交易钩子的拒绝原因中体现。编写同时覆盖通过和失败两种情况的单元测试。 - 调整滑点分级。 修改
DEFAULT_SLIPPAGE_CONFIG中的阈值或添加新等级。分级表格刻意保持简洁,除非有充分理由,否则不要将其改为连续函数。 - 新增止损类型。 在
StopLossType中添加变体,在StopLossEvaluator中实现触发逻辑,并确保周期性工作流模板能够识别。
不要添加绕过 FullRiskManager 的交易路径。单一门控特性是安全模型可辩护的基础。为"可信"调用方开设快速通道,是那种在审查时看似无害、却在最需要审计日志的那天将其破坏的变更。