MINARA

安全与沙盒

沙盒、权限等级、钩子管道,以及 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_filewrite_filepatchsearch_files 及所有文件相关工具。

Shell 出口也受到锁定。 terminal 工具会从每个子进程中清除 38 个凭据前缀,shell 包装器出口(bash -c curl 等)默认禁止。

权限等级

每个 ToolEntry 携带一个 permissionTier

等级名称示例
1READ_ONLYget_priceget_balanceread_fileweb_search
2CONFIRM_ONCEanalyze_marketdeep_research_run、小额闪兑
3ALWAYS_CONFIRMwrite_filepatchbuy_tokendocx_create
4MANUAL_ONLY向外部地址 transfer_token、切换紧急停止开关

钩子管道

每次工具调用前,全局 BeforeToolCallHook 会按以下顺序执行:

  1. 紧急停止开关。safetyConfig.killSwitch 已激活,所有交易工具(等级 ≥ 2)均被阻止。
  2. 每日消费上限。 累计涉及资金的交易量会与 safetyConfig.dailySpendCap 进行比对。
  3. 单笔上限。 单笔交易金额与 safetyConfig.perTxMax 进行比对。
  4. 等级门控。 在自主轮次(无用户参与)中,除非 safetyConfig.autopilotEnabledtrue,否则等级 3 及以上工具均被阻止。
  5. 仅限手动执行。 等级 4 工具始终需要用户确认的往返同步。

状态存储在 SQLite 的 audit_log 表中。每次调用均记录其推理过程、参数、结果及钩子决策。不会有任何内容被静默删除。

提示词注入防御

Agent 通过以下机制防御来自市场数据、工具输出和记忆召回的提示词注入:

  • 分隔符隔离。 不可信内容被随机唯一分隔符包裹,并告知 LLM 不得遵循其中的任何指令。
  • 零宽字符清除。 移除 ZWSP、ZWJ、RLO 等字符。
  • 内容扫描。 内容进入提示词前,与经整理的注入载荷数据库进行正则匹配。

参见 apps/agent/src/core/prompt-builder.tstests/unit/prompt-injection.test.ts

金融专项安全栈

权限等级钩子链拦截明显的错误调用。金融专项安全栈拦截隐性的错误交易:以 50% 滑点执行的闪兑、单一代币 5 倍仓位、一有回调即爆仓的 15 倍杠杆永续合约。该模块位于 apps/agent/src/finance/,将六个可独立测试的组件组合成完整的交易安全路径。

finance-safety diagram

安全栈结构

          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 是始终运行的第一阶段基础防线,包含三个硬约束:

  1. 单笔交易上限(默认 $500)。拒绝 estimated_value_usd 超过上限的交易。
  2. 每日消费上限(默认 $2,000)。使用 BEGIN IMMEDIATEdaily_spend SQLite 表进行原子检查和扣减,避免并发交易通过竞争超出上限。
  3. 紧急停止开关。 激活后,所有等级 2 及以上的交易均被拒绝,直至调用 /unkill

单笔上限和每日上限由 ~/.minara/settings.jsonsafety 区段的字段控制:

{
  "maxTransactionAmount": 500,
  "dailySpendCap": 2000,
  "killSwitchActive": false,
  "allowedTokens": [],
  "blockedTokens": ["SQUID", "SAFEMARS"]
}

白名单默认为空(即所有代币均可通过)。黑名单始终强制执行。可通过 minara config set safety.maxTransactionAmount 1000 修改。

token-safety.ts:入口检查

以优先级 50 作为交易钩子运行,在权限等级钩子之前执行。负责三项职责:

  1. 规范地址解析。 当用户说"在 Arbitrum 买 USDT"时,钩子从 CANONICAL_ADDRESSES 中查找规范地址,确保交易发送至真实的 USDT 合约,而非使用相同 ticker 的仿冒合约。
  2. 已知欺诈检测。 维护一组经过整理的代币 ticker 和地址,无论白名单/黑名单配置如何,均予以硬性拦截。该集合存储在源代码中,添加条目需提交 PR。
  3. 链解析。 将模糊的链标识符("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_probabilitypayoff_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,0002.0%
$1,000 – $10,0001.0%
超过 $10,0000.5%

分级结构既能防止小额交易承受 5% 的价格影响(令人恼火但可恢复),也能防止大额交易承受 1%(对于鲸鱼级订单可能造成灾难性损失)。上述阈值均为默认值,每个字段均可在 ~/.minara/settings.jsonsafety 区段下配置。

执行闪兑前,钩子调用 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 exit

trailing

当价格从入场后最高点下跌 threshold_pct 时退出,而非从入场价计算,可锁定收益。

{ type: "trailing", threshold_pct: 0.08 }  // 8% drawdown from high

time_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.jsonsafety 区段中,可通过 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 的交易路径。单一门控特性是安全模型可辩护的基础。为"可信"调用方开设快速通道,是那种在审查时看似无害、却在最需要审计日志的那天将其破坏的变更。

本页目录