セーフティとサンドボックス
サンドボックス、パーミッションティア、フックパイプライン、そして6段階の資金安全スタック
このページでは取引実行とサンドボックスを説明します。利用者向けの Chat 機能は財務安全リマインダーを参照してください。
Minara は2つのレベルでセーフティを適用しています。すべてのツール呼び出しに適用されるシステム全体の分離とパーミッションゲート、そして取引パスに適用される金融特化のセーフティスタックです。
Minara Agent の出力は参考情報であり、金融または投資の助言ではありません。ソフトウェアの実行、取引の承認、または戦略の利用前に、プロジェクトの免責事項全文を確認してください。適用法で認められる最大限の範囲で、Minara.AI は本プロジェクトの利用に関連する損失について責任を負いません。
なぜ金融に6段階が必要なのか? 各段階は異なる種類のミスを検出します。LLM はティッカーを誤認したり、嗜好を忘れたり、プロンプトインジェクションを受けたり、単に古い価格データを持っていたりすることがあります。6つの軽量かつ直交したチェックを積み重ねることで、エラーがすべての段階をすり抜けなければ実際の資金は動きません。実際にそのようなことは起きていません。1つの大きな「LLM 判定」チェックはコストが高く、1行の型ガードで検出できるカテゴリ的なエラーを見逃してしまいます。
このページでは、基盤から順に両方を説明します。
📘 オペレーター向けの対となるドキュメントは セキュリティ チャプター です。4 つの独立した 安全層(command-guard、OS jail、fund-moving confirm、script-risk gate)が日常利用でどのように現れ、それぞれ何を防ぐかを解説して います。このページは実装側のリファレンス、あちらはユーザー 視点のガイドです。
実際の使用例: 資金移動を伴うすべての機能ページ(取引、ポートフォリオ、予測市場)はここにリンクしています。初めての取引のウォークスルーでは、ユーザー向けのプレビューステップ(6段階スタックのステージ3)を確認できます。
基盤: サンドボックス、ティア、フックパイプライン
すべてのツール呼び出しは2つの独立したレイヤーを通過します。サンドボックス(ファイルシステムの分離。ツールが触れる範囲を定義)と、パーミッションティアおよびフックパイプライン(動作のゲーティング。ツールが現時点で許可されている操作を定義)です。
サンドボックス
すべてのファイルシステムツールは $dataDir/sandbox/files/(デフォルト: ~/.minara/sandbox/files/)をルートとします。すべてのパス引数は apps/agent/src/tools/_shared/sandbox.ts の resolveInSandbox() を通過します。この関数は以下を行います:
- 要求されたパスをサンドボックスルートに対して解決する。
- ルートから脱出する
..を含むパスを拒否する。 - シンボリックリンクを解決し、サンドボックス外を指すものを拒否する。
- ツールが安全に使用できる絶対解決済みパスを返す。
ツールは apps/agent/src/ 配下、ユーザーのホームディレクトリ、またはサンドボックス外のどこかにあるファイルを読み書きすることが物理的に不可能です。これは read_file、write_file、patch、search_files、およびすべてのファイル関連ツールに適用されます。
シェルへの外部通信もロックされています。 terminal ツールはすべてのサブプロセスから38種類の認証情報に類似した環境変数プレフィックスを除去し、シェルラッパー経由の外部通信(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)がブロックされます。 - 1日あたりの支出上限。 資金移動の累計量が
safetyConfig.dailySpendCapに対して追跡されます。 - 1取引あたりの上限。 個別の取引金額が
safetyConfig.perTxMaxに対してチェックされます。 - ティアゲーティング。 自律ターン(ユーザーが介在しない)では、
safetyConfig.autopilotEnabledが true でない限り、ティア3以上のツールがブロックされます。 - MANUAL_ONLY の強制。 ティア4のツールは常にユーザー確認のラウンドトリップが必要です。
状態は SQLite の audit_log テーブルに保存されます。すべての呼び出しは、その推論、引数、結果、フックの判断とともにログに記録されます。何かが無断で削除されることは一切ありません。
プロンプトインジェクション対策
Agent は、マーケットデータ、ツール結果、メモリ呼び出しからのプロンプトインジェクションを以下の方法で防御します:
- デリミタ分離。 信頼されていないコンテンツは、LLM が指示に従わないよう事前に指示されたランダムな一意のデリミタで囲まれます。
- ゼロ幅文字の除去。 ZWSP、ZWJ、RLO などを削除します。
- コンテンツスキャン。 プロンプトに入る前に、厳選されたインジェクションペイロードデータベースに対して正規表現マッチングを行います。
詳細は apps/agent/src/core/prompt-builder.ts と tests/unit/prompt-injection.test.ts を参照してください。
資金安全スタック
パーミッションティアのフックチェーンは明らかに不正な呼び出しを阻止します。資金安全スタックは微妙に不正な取引を阻止します。例えば、50% のスリッページで実行されるスワップ、1つのトークンに対する5倍のポジション、最初の下落で爆死する15倍レバレッジの永続先物などです。このモジュールは apps/agent/src/finance/ 配下にあり、独立してテスト可能な6つのコンポーネントを完全な取引セーフティパスへと組み合わせています。
スタックの構造
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のフロアです。3つのハード制約があります:
- 1取引あたりの上限(デフォルト
$500)。estimated_value_usdが上限を超える取引は拒否されます。 - 1日あたりの支出上限(デフォルト
$2,000)。BEGIN IMMEDIATEを使用したdaily_spendSQLite テーブルへの冪等なチェックアンドデビットにより、並行取引が競合して上限を超えることを防ぎます。 - 緊急停止。 有効な場合、
/unkillが呼ばれるまで、すべてのティア2以上の取引が拒否されます。
1取引あたりの上限と1日あたりの上限は ~/.minara/settings.json の safety セクション配下のフィールドで制御されます:
{
"maxTransactionAmount": 500,
"dailySpendCap": 2000,
"killSwitchActive": false,
"allowedTokens": [],
"blockedTokens": ["SQUID", "SAFEMARS"]
}許可リストはデフォルトで空(すべてのトークンが通過)です。ブロックリストは常に適用されます。minara config set safety.maxTransactionAmount 1000 で編集できます。
token-safety.ts: エントリーゲート
優先度50の取引フックとして、パーミッションティアフックの前に実行されます。3つの役割があります:
- 正規アドレスの解決。 ユーザーが「Arbitrum で USDT を買う」と言った場合、このフックは
CANONICAL_ADDRESSESから正規アドレスを検索し、同じティッカーを持つ詐欺コントラクトではなく本物の USDT コントラクトに取引が向くようにします。 - 既知の詐欺検出。 許可リスト/ブロックリストの設定に関係なくハードブロックされる、厳選されたトークンティッカーとアドレスの小さなセットです。このセットはソースコードに保存されているため、エントリーを追加するには PR が必要です。
- チェーン解決。 曖昧なチェーン識別子("eth" vs "ethereum" vs "mainnet")を Minara バックエンドが期待する正規のチェーン ID にマッピングします。
トークンが解決しない場合、取引は構造化されたエラーで拒否されます。LLM はそれを説明できます(「'ethereum' 上の 'SAFEMARS' は正規トークンとして認識できず、詐欺リストに一致します」)。
position-sizing.ts: 取引サイズの決定
~/.minara/settings.json の safety.sizing.strategy で選択される3つの戦略があります:
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エッジと分散を考慮した最適なベットサイジングです。デフォルトはハーフケリー。フルケリーはエッジが本物でも過酷なドローダウンを引き起こすためです。LLM が引数として win_probability と payoff_ratio を提供する必要があります。どちらかが欠けている場合、サイザーは fixed_fraction にフォールバックします。
すべての戦略は常に1取引あたりの上限でクリップされます。SizingDecision.clipped フラグは計算されたサイズが上限に達したかどうかを記録し、監査クエリで上限がバインディングになっている状況を確認できます。
exposure-limits.ts: 集中リスクの制御
現在のポートフォリオ(過去のものではない)に対して計算される3層のエクスポージャー制限:
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
}エクスポージャーチェックは取引実行の前に実行されます。新しい取引が4つの制限のいずれかを超えるエクスポージャーを引き起こす場合、フックは違反した具体的な上限とともに拒否します。
アセットクラスは learning/methodology-store.ts → classifyAsset によって計算され、スキルシステムのアセットクラス分類と重なります。ルーターで crypto_meme とラベル付けされた取引は、エクスポージャーチェックでも crypto_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: 出口管理
ポジションレベルのストップロスルールです。3つのタイプがあります:
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 hours3つすべてがオプションの take_profit_pct をサポートし、損失だけでなく利益でもクローズできます。
実行の仕組み
ストップロスルールは定期的なワークフロー(workflow/templates/stop-loss-monitor.ts)によってチェックされます。このワークフローはスケジュールに従ってオープンポジションをポーリングし、各トリガーが発火するかどうかを計算し、通常の取引パスを通じてクローズ注文を発行します。
これが重要です。ストップロスモジュール自体は取引バックエンドを直接呼び出しません。判断するだけです。クローズ注文は依然として完全なセーフティスタック(パーミッションティア、1日あたりの上限、スリッページチェックなど)を通過します。取引所の障害中にストップロスが退出しようとしても、すべてのゲートが尊重されます。特権的なバイパスはありません。
組み合わせ: FullRiskManager
full-risk-manager.ts はすべてを1つの取引フックにまとめます:
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超のレバレッジ、1.0超の threshold_pct)は、セーフティ設定ローダーをデフォルトにフォールバックさせ、構造化ログに警告を出力させます。無効な設定で起動することはできません。
オブザーバビリティ
すべての取引フックの判断は2つのテーブルに記録されます:
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を実装し、新しいsizing.*設定フィールドを env-vars とこのページにドキュメント化します。 - 新しいエクスポージャー次元。
ExposureLimitsConfigにフィールドを追加し、ExposureLimiter.checkにチェックを実装し、取引フックのブロック理由に表示します。パスとフェイルの両方のケースをカバーするユニットテストを書きます。 - 異なるスリッページ段階設定。
DEFAULT_SLIPPAGE_CONFIGの閾値を変更するか、新しいティアを追加します。段階テーブルは意図的に小さくしています。正当な理由がない限り、連続関数に変換しないでください。 - 新しいストップロスタイプ。
StopLossTypeにバリアントを追加し、StopLossEvaluatorにトリガーを実装し、定期的なワークフローテンプレートがそれを認識することを確認します。
FullRiskManager をバイパスする取引パスを追加しないでください。シングルゲートのプロパティがセーフティモデルを守りやすくしています。「信頼された」呼び出し元のためのファストパスは、レビューでは無害に見えるが、最も必要な日に監査ログを壊すような変更です。