안전 및 샌드박스
샌드박스, 권한 등급, 훅 파이프라인, 그리고 6단계 자금 안전 스택
이 페이지는 거래 실행과 샌드박스를 설명합니다. 일반 사용자용 Chat 기능은 금융 안전 알림을 참고하십시오.
Minara는 두 가지 수준에서 안전을 적용합니다. 모든 도구 호출에 적용되는 시스템 전역 격리 및 권한 게이트, 그리고 거래 경로에 적용되는 금융 특화 안전 스택입니다.
Minara Agent의 출력은 참고용이며 금융 또는 투자 조언이 아닙니다. 소프트웨어 실행, 거래 승인 또는 전략 사용 전에 프로젝트 면책 조항 전문을 확인하세요. 관련 법률이 허용하는 최대 범위에서 Minara.AI는 이 프로젝트 사용과 관련된 손실에 대해 책임을 지지 않습니다.
금융에 6단계가 필요한 이유 각 단계는 서로 다른 유형의 오류를 잡아냅니다. LLM은 티커를 잘못 생성하거나, 선호도를 잊거나, 프롬프트 인젝션에 노출되거나, 단순히 오래된 가격 정보를 사용할 수 있습니다. 비용이 낮으면서도 서로 독립적인 6가지 검사를 중첩하면, 오류가 실제 자금을 이동시키려면 모든 검사를 통과해야 합니다. 실제로는 그런 일이 발생하지 않습니다. 하나의 큰 "LLM 판단" 검사는 비용이 더 높고, 1줄짜리 타입 가드로 잡을 수 있는 범주적 오류를 놓칩니다.
이 페이지는 기초부터 시작해 두 가지를 모두 다룹니다.
📘 운영자 시각의 대응 문서는 보안 챕터 입니다. 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, 그리고 모든 파일 관련 도구에 적용됩니다.
셸 외부 접근도 제한됩니다. 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 이상)가 차단됩니다. - 일일 지출 한도. 자금 이동 누적 거래량이
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/ 아래에 있으며, 독립적으로 테스트 가능한 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는
항상 실행되는 Phase 1 기준선입니다. 세 가지 하드 제약이 적용됩니다.
- 건당 최대 금액 (기본값
$500).estimated_value_usd가 한도를 초과하는 거래를 거부합니다. - 일일 지출 한도 (기본값
$2,000).BEGIN IMMEDIATE를 사용해daily_spendSQLite 테이블에 원자적 검사-차감을 수행하므로, 동시 거래가 경쟁 상태로 한도를 초과할 수 없습니다. - 비상 정지. 활성화되면
/unkill이 호출될 때까지 모든 등급 2 이상의 거래가 거부됩니다.
건당 한도와 일일 한도는 ~/.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 컨트랙트로 향하도록 합니다. - 알려진 스캠 탐지. 허용 목록/차단 목록 설정과 관계없이 하드 차단되는 소규모 선별 토큰 티커 및 주소 집합입니다. 이 집합은 소스 코드에 있으므로 항목 추가는 PR이 필요합니다.
- 체인 해석. 모호한 체인 식별자("eth" vs "ethereum" vs "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으로 분류됩니다. 단일 어휘를 공유하므로 "밈코인 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% 하락 시 청산 트리거trailing
진입가가 아닌 진입 이후 최고가 대비 threshold_pct만큼 하락하면 청산합니다. 수익을 잠금합니다.
{ type: "trailing", threshold_pct: 0.08 } // 최고점 대비 8% 하락time_based
P&L과 관계없이 일정 시간이 지나면 청산합니다.
{ type: "time_based", max_age_ms: 86400000 } // 24시간세 가지 유형 모두 손실뿐 아니라 수익 시에도 청산할 수 있는 선택적 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 # 모든 필드 표시
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: 어떤 훅이 왜 실행되었는지.
자주 사용하는 조사 쿼리 예시입니다.
-- 어제 스왑이 거부된 이유는?
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';
-- 규모 한도가 구속력을 발휘하는 빈도는?
SELECT COUNT(*) FROM audit
WHERE tool_set = 'trade'
AND json_extract(result_json, '$.sizing.clipped') = 1;
-- 어떤 토큰이 노출 한도에 가장 자주 걸리나?
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.*설정 필드를 환경 변수와 이 페이지에 문서화하십시오. - 새 노출 차원.
ExposureLimitsConfig에 필드를 추가하고,ExposureLimiter.check에 검사를 구현한 후, 거래 훅의 차단 이유에 표시하십시오. 통과와 실패 케이스를 모두 커버하는 단위 테스트를 작성하십시오. - 슬리피지 계층 변경.
DEFAULT_SLIPPAGE_CONFIG의 임계값을 변경하거나 새 계층을 추가하십시오. 계층 테이블은 의도적으로 작습니다. 충분한 이유가 없다면 연속 함수로 바꾸지 마십시오. - 새 손절 유형.
StopLossType에 변형을 추가하고,StopLossEvaluator에 트리거를 구현한 후, 주기적 워크플로 템플릿이 이를 인식하는지 확인하십시오.
FullRiskManager를 우회하는 거래 경로는 절대 추가하지 마십시오. 단일 게이트 속성이 안전 모델을 방어 가능하게 만드는 것입니다. "신뢰할 수 있는" 호출자를 위한 빠른 경로는 코드 리뷰에서 무해해 보이지만, 가장 필요한 날 감사 로그를 망가뜨리는 종류의 변경입니다.