fed3526b20
설정·스크립트·스킬·문서·큐레이션 메모리 추적. 시크릿(credentials/identity)·런타임 상태(state/logs/sessions/sqlite)· 백업(clobbered/bak)·dream 캐시는 .gitignore로 제외. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
2.8 KiB
Python
107 lines
2.8 KiB
Python
"""
|
|
Kill switch.
|
|
|
|
state/orders_disabled 파일 존재 시 모든 진입점에서 거부.
|
|
LLM 에이전트(클로·레이) 세션에서 호출 시 즉시 차단 (OPENCLAW_AGENT 환경변수).
|
|
|
|
모든 매매 진입점 함수 첫 줄에서 guard_or_raise() 호출이 강제이다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone, timedelta
|
|
from pathlib import Path
|
|
|
|
WORKSPACE_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
LIMITS_FILE = Path(__file__).resolve().parent / 'limits.json'
|
|
|
|
KST = timezone(timedelta(hours=9))
|
|
|
|
|
|
class SidecarBlocked(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _limits() -> dict:
|
|
return json.loads(LIMITS_FILE.read_text(encoding='utf-8'))
|
|
|
|
|
|
def _kill_switch_path() -> Path:
|
|
return WORKSPACE_ROOT / _limits()['kill_switch_file']
|
|
|
|
|
|
def _agent_env_blocked() -> bool:
|
|
var = _limits().get('telegram', {}).get('block_when_agent_env_set')
|
|
return bool(var and os.getenv(var))
|
|
|
|
|
|
def is_disabled() -> bool:
|
|
return _kill_switch_path().exists()
|
|
|
|
|
|
def guard_or_raise() -> None:
|
|
if _agent_env_blocked():
|
|
raise SidecarBlocked('LLM agent session cannot invoke order module (OPENCLAW_AGENT env set)')
|
|
if is_disabled():
|
|
raise SidecarBlocked('Order module is disabled (sidecar ON)')
|
|
|
|
|
|
def disable(reason: str = 'manual') -> Path:
|
|
p = _kill_switch_path()
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(
|
|
json.dumps({'disabled_at': datetime.now(KST).isoformat(), 'reason': reason}, ensure_ascii=False, indent=2),
|
|
encoding='utf-8',
|
|
)
|
|
try:
|
|
os.chmod(p, 0o600)
|
|
except OSError:
|
|
pass
|
|
return p
|
|
|
|
|
|
def enable() -> bool:
|
|
p = _kill_switch_path()
|
|
if not p.exists():
|
|
return False
|
|
p.unlink()
|
|
return True
|
|
|
|
|
|
def status() -> dict:
|
|
p = _kill_switch_path()
|
|
if not p.exists():
|
|
return {'disabled': False, 'path': str(p), 'agent_env_blocked': _agent_env_blocked()}
|
|
try:
|
|
meta = json.loads(p.read_text(encoding='utf-8'))
|
|
except (OSError, ValueError):
|
|
meta = {}
|
|
meta.update({'disabled': True, 'path': str(p), 'agent_env_blocked': _agent_env_blocked()})
|
|
return meta
|
|
|
|
|
|
def _cli(argv: list[str]) -> int:
|
|
if not argv or argv[0] == 'status':
|
|
print(json.dumps(status(), ensure_ascii=False, indent=2))
|
|
return 0
|
|
cmd = argv[0]
|
|
if cmd == 'disable':
|
|
reason = ' '.join(argv[1:]) or 'manual'
|
|
p = disable(reason)
|
|
print(f'sidecar disabled: {p}')
|
|
return 0
|
|
if cmd == 'enable':
|
|
ok = enable()
|
|
print('sidecar enabled' if ok else 'sidecar already enabled (no file)')
|
|
return 0
|
|
print(f'unknown command: {cmd}', file=sys.stderr)
|
|
print('usage: sidecar.py {status | disable [reason] | enable}', file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(_cli(sys.argv[1:]))
|