MINARA

セーフティとサンドボックス

サンドボックス、パーミッションティア、フックパイプライン、そして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.tsresolveInSandbox() を通過します。この関数は以下を行います:

  • 要求されたパスをサンドボックスルートに対して解決する。
  • ルートから脱出する .. を含むパスを拒否する。
  • シンボリックリンクを解決し、サンドボックス外を指すものを拒否する。
  • ツールが安全に使用できる絶対解決済みパスを返す。

ツールは apps/agent/src/ 配下、ユーザーのホームディレクトリ、またはサンドボックス外のどこかにあるファイルを読み書きすることが物理的に不可能です。これは read_filewrite_filepatchsearch_files、およびすべてのファイル関連ツールに適用されます。

シェルへの外部通信もロックされています。 terminal ツールはすべてのサブプロセスから38種類の認証情報に類似した環境変数プレフィックスを除去し、シェルラッパー経由の外部通信(bash -c curl など)はデフォルトで拒否されます。

パーミッションティア

すべての ToolEntrypermissionTier を持ちます:

ティア名称
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. 1日あたりの支出上限。 資金移動の累計量が safetyConfig.dailySpendCap に対して追跡されます。
  3. 1取引あたりの上限。 個別の取引金額が safetyConfig.perTxMax に対してチェックされます。
  4. ティアゲーティング。 自律ターン(ユーザーが介在しない)では、safetyConfig.autopilotEnabled が true でない限り、ティア3以上のツールがブロックされます。
  5. MANUAL_ONLY の強制。 ティア4のツールは常にユーザー確認のラウンドトリップが必要です。

状態は SQLite の audit_log テーブルに保存されます。すべての呼び出しは、その推論、引数、結果、フックの判断とともにログに記録されます。何かが無断で削除されることは一切ありません。

プロンプトインジェクション対策

Agent は、マーケットデータ、ツール結果、メモリ呼び出しからのプロンプトインジェクションを以下の方法で防御します:

  • デリミタ分離。 信頼されていないコンテンツは、LLM が指示に従わないよう事前に指示されたランダムな一意のデリミタで囲まれます。
  • ゼロ幅文字の除去。 ZWSP、ZWJ、RLO などを削除します。
  • コンテンツスキャン。 プロンプトに入る前に、厳選されたインジェクションペイロードデータベースに対して正規表現マッチングを行います。

詳細は apps/agent/src/core/prompt-builder.tstests/unit/prompt-injection.test.ts を参照してください。

資金安全スタック

パーミッションティアのフックチェーンは明らかに不正な呼び出しを阻止します。資金安全スタックは微妙に不正な取引を阻止します。例えば、50% のスリッページで実行されるスワップ、1つのトークンに対する5倍のポジション、最初の下落で爆死する15倍レバレッジの永続先物などです。このモジュールは apps/agent/src/finance/ 配下にあり、独立してテスト可能な6つのコンポーネントを完全な取引セーフティパスへと組み合わせています。

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のフロアです。3つのハード制約があります:

  1. 1取引あたりの上限(デフォルト $500)。estimated_value_usd が上限を超える取引は拒否されます。
  2. 1日あたりの支出上限(デフォルト $2,000)。BEGIN IMMEDIATE を使用した daily_spend SQLite テーブルへの冪等なチェックアンドデビットにより、並行取引が競合して上限を超えることを防ぎます。
  3. 緊急停止。 有効な場合、/unkill が呼ばれるまで、すべてのティア2以上の取引が拒否されます。

1取引あたりの上限と1日あたりの上限は ~/.minara/settings.jsonsafety セクション配下のフィールドで制御されます:

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

許可リストはデフォルトで空(すべてのトークンが通過)です。ブロックリストは常に適用されます。minara config set safety.maxTransactionAmount 1000 で編集できます。

token-safety.ts: エントリーゲート

優先度50の取引フックとして、パーミッションティアフックのに実行されます。3つの役割があります:

  1. 正規アドレスの解決。 ユーザーが「Arbitrum で USDT を買う」と言った場合、このフックは CANONICAL_ADDRESSES から正規アドレスを検索し、同じティッカーを持つ詐欺コントラクトではなく本物の USDT コントラクトに取引が向くようにします。
  2. 既知の詐欺検出。 許可リスト/ブロックリストの設定に関係なくハードブロックされる、厳選されたトークンティッカーとアドレスの小さなセットです。このセットはソースコードに保存されているため、エントリーを追加するには PR が必要です。
  3. チェーン解決。 曖昧なチェーン識別子("eth" vs "ethereum" vs "mainnet")を Minara バックエンドが期待する正規のチェーン ID にマッピングします。

トークンが解決しない場合、取引は構造化されたエラーで拒否されます。LLM はそれを説明できます(「'ethereum' 上の 'SAFEMARS' は正規トークンとして認識できず、詐欺リストに一致します」)。

position-sizing.ts: 取引サイズの決定

~/.minara/settings.jsonsafety.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_probabilitypayoff_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,0001.0%
$10,000 超0.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: 出口管理

ポジションレベルのストップロスルールです。3つのタイプがあります:

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

3つすべてがオプションの 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.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超のレバレッジ、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 をバイパスする取引パスを追加しないでください。シングルゲートのプロパティがセーフティモデルを守りやすくしています。「信頼された」呼び出し元のためのファストパスは、レビューでは無害に見えるが、最も必要な日に監査ログを壊すような変更です。

目次