Frame
한 줄 소개: KSME 회계 기준 준수 금융관리 SaaS. Postgres schema-per-entity 격리 + ACID double-entry + append-only audit log. Blueprint 에이전트가 MCP ~44개 도구로 접근 (조회 14 + 작업 30+).
기술 스택
| 항목 | 값 |
|---|---|
| 언어 | Python 3.12+ |
| 프레임워크 | FastMCP (stdio + HTTP/SSE) |
| DB | PostgreSQL 16 |
| ORM | SQLAlchemy 2.0 Core (declarative 아님) + psycopg v3 |
| Migration | Alembic dual-env (shared + entity) |
| 인증 | PyJWT (HS256 frame JWT + RS256 Microsoft JWKS) |
| LLM | Anthropic Claude (file format 분석, 분류) |
| HTTP server | Starlette + uvicorn |
포트
| 포트 | 용도 |
|---|---|
| 3700 | PostgreSQL 16 (frame-postgres) |
| 3710 | MCP HTTP blue (active) |
| 3711 | MCP HTTP green (passive) |
| 3712 | axe-frame-proxy (Caddy, blue/green selector) |
4 가지 surface
- MCP (HTTP/SSE) — Claude Agent SDK 가 도구로 호출
- CLI (
frame ...) — 운영자 명령 (migrate, register-entity, mcp-token, …) - DB 직접 접근 — psql 으로 정합성 검사, 운영자만
- Export — 회계 사무소용 CSV/JSON
MCP Tools (~54개)
src/frame/mcp/server.py 의 @mcp.tool() 데코레이터 기준. 핵심 도구:
| Tool | Scope 요구 | 설명 |
|---|---|---|
query_balance | read | 계정 잔액 조회 |
query_trial_balance | read | 시산표 |
list_journals | read | 분개장 |
get_journal | read | 단일 분개 상세 |
list_raw_transactions | read | 입출금 표 |
search_evidence | read | 증거 전문 검색 |
list_open_items | read | 미결 항목 |
list_periods | read | 회계 기간 조회 |
post_journal | write | 분개 등록 (5계층 검증) |
reverse_journal | write | 분개 역분 |
flag_uncertainty | write | 미결 생성 |
resolve_open_item | write | 미결 해결 |
ingest_file_blob | write | 파일 추출 |
reconcile_and_propose | write | 대사 제안 |
closing_preview | close | 마감 미리보기 |
closing_commit | close | 마감 확정 |
register_resolution, list_resolutions, link_journal_to_resolution | write | 결의서 관리 |
compose_kb_mmf_q_journals, compose_kbsc_q_journals | write | 분기 마감 자동 분개 |
register_evidence_blob, promote_source_file | write | 증거·파일 관리 |
transition_period | close | 기간 상태 전이 |
register_entity, list_sub_entities | admin | entity 등록 + 자회사/펀드 등 sub-entity 조회 (D-ops-30 Phase 2) |
list_scrape_schedules, get_scrape_schedule | read | 포털 스크래퍼 주기 조회 (D-frame-scrape-schedule) |
set_scrape_schedule | write | 스크래퍼 주기 설정 (raw cron 또는 daily/weekly/monthly 프리셋, KST) |
| 기타: B/S·I/S statements, fixed asset, export, snapshot 등 | 각각 | 정합성·내보내기 |
전체 정확한 카운트는 grep -c '@mcp.tool' /Users/axe/frame/src/frame/mcp/server.py 로 확인 (현재 54).
Schema discovery — GET /frame/schemas
Blueprint 의 artifact + PARA 지식 레이어 (D-bp-artifact-1) 가 fact 의 typed schema 권위를 MCP 에 위임. frame 은 자기가 생산하는 fact kind 를 /schemas 엔드포인트로 노출 — Blueprint 가 fetch + cache + version (B-bp-artifact-schema-discovery).
curl -H "Authorization: Bearer $FRAME_MCP_TOKEN" \
https://axe.axelabs.ai/frame/schemas응답 envelope (안정 형식):
{
"version": "1.0",
"service": "frame",
"schemas": {
"[email protected]": {
"description": "단일 계정의 시점 잔액 (debit/credit 합계 + 순 balance).",
"produced_by": ["query_balance"],
"key_fields": ["entity_id", "account_code", "as_of_date", "debit", "credit", "balance", "policy_id"]
},
"[email protected]": { ... },
"[email protected]": { ... }
}
}13 schemas 현재 노출 (balance / trial_balance / journal / journal_list / raw_transaction / open_item / resolution / fixed_asset_book_value / income_statement / balance_sheet / cash_flow_statement / equity_changes_statement / general_ledger). 추가 schema 는 src/frame/mcp/schemas.py (SOT) 에 등재.
cash_flow_statement 활동 분류 (간접법, src/frame/ops/statements.py): 명시적 dict (OPERATING_ASSETS/OPERATING_LIABILITIES/INVESTING_ACCOUNTS/FINANCING_ACCOUNTS) 우선, dict 에 없는 신규 계정만 type+prefix fallback (D-frame-3): asset 且 code prefix 13 → 투자활동 (1300번대 투자자산 전체, 신계정 자동 포함); liability 且 차입금 코드(BORROWING_LIABILITIES=2105) → 재무활동. ⚠️ 2101 미지급금/2102 미지급세금 등 영업성 부채는 영업활동 유지 (차입금 코드만 명시 — 미지급금 재무 누출 차단). 분류 완전 시 reconciliation_diff=0·verified=true, 누락 계정은 unclassified_accounts 로 노출.
- 인증: required (JWTAuthMiddleware 적용 —
_PUBLIC_PATHS에 미포함). MCP 클라이언트의 동일 토큰 재사용 - versioning: schema_id 의
@{version}suffix (예:[email protected]→@2.0incompatible 시), 환경 envelopeversion은 응답 모양 변경 시에만 bump - 소스 권위:
src/frame/mcp/schemas.py가 SOT — 외부 schema registry 없음
데이터 모델
Shared schema
| 테이블 | 역할 |
|---|---|
entity | 등록된 entity 메타 (corp + fund 평등 평면). 컬럼: id PK · legal_name (영문 사업자등록 SoT) · display_name (한글 표시명, NULL→legal_name 폴백, 0010_shared) · biz_no · schema_name · accounting_standard · fiscal_year_start_month · entity_kind (corporate/kip/kvf, D-ops-22) · fund_meta JSONB (펀드 결성·약정·LP 등, D-ops-22) · closed_at (청산일, D-ops-22) |
entity_relationship | entity 간 관계 (PR #26 + D-ops-22). entity_a (holder) / entity_b (target) / kind 8 종 (parent_subsidiary · sister_company · common_controlling_party · common_director · related_party · joint_venture · gp_managed_fund · lp_invested_fund) / ownership_numerator/denominator/unit (amount_krw/shares/units — % 는 반올림 오차로 폐기) / effective_from·until / evidence_ids[] |
cross_journal_link (D-ops-22) | cross-entity mirror 분개 pair. link_kind (gp_commitment / mgmt_fee / carried_interest / capital_call / distribution / other) + (left_entity_id, left_journal_id) ↔ (right_entity_id, right_journal_id). UNIQUE 양쪽. schema 격리 환경 무결성 |
format_profile | 파일 포맷 캐시 (vendor + column_signature) |
format_profile_clarification | clarify Q&A 누적 |
account_template | 표준 계정과목 (KSME/KGAAP/KIFRS) |
idempotency_record | 멱등성 키 (cross-entity write dedup) |
oauth_authorization_codes | OAuth proxy 잔재 (현재 dormant) |
scrape_schedule (D-frame-scrape-schedule) | 포털 스크래퍼 주기. source_key · entity_id · enabled · cron (KST) · window_days · last_run_at/last_status/next_due_at. MCP set_scrape_schedule 로 조정, 호스트 frame scrape-due 가 실행 |
Per-entity schema (axec, axev, …)
| 테이블 | 역할 |
|---|---|
account | 계정 (chart-of-accounts) |
journal | 분개 헤더 |
journal_line | 분개 라인 (debit/credit) |
raw_transaction | 원천 거래 (은행 입출금 등) |
bank_account | 계좌 메타 |
source_file | 업로드된 파일 (content_hash, portable storage_url) |
fiscal_period | 회계 기간 (open/soft_closed/hard_closed) |
open_item | 미결 (severity, resolution_note) |
evidence | 증거 (kind, content_hash, portable storage_url) |
audit_log | 쓰기 추적 (모든 INSERT/UPDATE/DELETE) |
entity_meta | 사업자등록번호, 법인명 |
fixed_asset | 고정자산 |
security_holder | 주주 (PII 암호화) |
commitment_ledger | 펀드 약정·납입 ledger (kip/kvf 전용 — D-ops-30 Phase 3) |
lp_master | LP master (external_entity_id FK → shared.entity, D-ops-30 Phase 3) |
accounting_policy.standard CHECK (D-frame-fund-ksme-policy-check, 2026-05-22): 0003 의 CHECK 가 fund kind 의 fund_ksme 미허용하여 KIP/KVF entity 등록 시 zombie schema (0 tables). 0003 코드 패치 + 0018 migration 으로 ('ksme','kgaap','kifrs','fund_ksme') 확장. fund_ksme chart_template (D-ops-30 Migration #2) 가 fund entity bootstrap 시 자동 적용.
evidence / source_file blob 저장 (D-frame-2, 2026-06-04): 모든 evidence·source_file blob 은 content-addressed 로 .local/files/<hash[:2]>/<hash>/<filename> 에 복사 (Postgres 에 bytea 없음 — 큰 파일은 file store). storage_url 은 portable = relative <hash[:2]>/<hash>/<filename>, read 시 frame.ingest.pipeline.resolve_storage_path() 가 FRAME_FILE_STORAGE_PATH (.local/files, 컨테이너 /app/.local/files) 기준 absolute 로 매핑. local://<host 경로> 또는 CWD 의존 absolute 경로 금지 — 컨테이너가 host 경로 read 불가 + 원본 rename/move 시 깨짐 (host /Users/.../.local/files ↔ 컨테이너 /app/.local/files 불일치). resolver 는 legacy 형식 (portable / /app/... / /Users/... / local://<abs>) 을 모두 tolerant. migration 0020_normalize_evidence_storage_url 가 기존 row 를 portable 로 정규화 (blob 실존 시만, 부재 row 미변경). 무결성 probe: GET /frame/health/storage.
디렉토리 구조
src/frame/
├── auth.py TokenClaims, encode/decode_token
├── auth_oidc.py Microsoft id_token 검증
├── config.py customers.yaml 로더 + accessor
├── cli.py 37+ click commands
├── errors.py FrameError 계층
├── observability.py @instrument, Prometheus
│
├── db/
│ ├── engine.py async pool, context manager
│ ├── shared.py shared 테이블 정의
│ ├── entity.py entity 메타 factory (35개 테이블)
│ └── migrations.py upgrade_all(), register_entity()
│
├── ingest/ 파일 → raw_transaction
│ ├── reader.py xls/xlsx/csv parser
│ ├── llm.py Claude format 분석
│ ├── profile.py format_profile 캐시
│ ├── hometax.py 홈택스 vendor
│ ├── kb_fund.py KB 펀드
│ └── card.py 카드사
│
├── accounting/ M2+ 회계 계층
│ ├── chart.py chart-of-accounts
│ ├── journal.py 분개 모델
│ ├── classify.py raw → journal_line
│ ├── balance.py trial_balance, B/S
│ └── reconcile.py 대사
│
├── ops/ MCP tool 로직
│ ├── queries.py
│ ├── write.py post_journal (5계층 검증)
│ ├── period.py list_periods, transition_period
│ ├── workflow.py reconcile_and_propose, closing_*
│ ├── open_item.py
│ ├── quarterly_close.py
│ └── snapshot.py
│
├── mcp/ MCP 서버
│ ├── server.py FastMCP instance, 44 tools 등록
│ ├── http_server.py Starlette, OAuth middleware
│ ├── prompts.py 4 prompts (reconcile_walkthrough 등)
│ ├── oauth.py OAuth proxy (D-ops-15, dormant)
│ └── tools/ 각 tool 구현 (13 .py)
│
├── security/
│ └── pii.py pgcrypto pgp_sym_encrypt
│
alembic/
├── versions/ shared (0001~0007)
└── entity_versions/ per-entity (0001~0016)
docs/ops/ 운영 정책 6 파일
├── naming-conventions.md
├── access-control.md
├── jwt-issuance.md
├── mcp-client-config.md
├── onboarding-operator.md
└── onboarding-employee.mdCLI 명령어
# DB
frame migrate # shared + 모든 entity
frame register-entity --id axec --legal-name "에이엑스이 코퍼레이션"
# fund (D-ops-22, 2026-05-22) — KIP / KVF 등록
frame register-entity --id axev_kvf_001 \
--legal-name "AXE 시드 1호 벤처투자조합" \
--kind kvf \
--fund-meta '{"founded_at":"2024-03-15","commitment_total_krw":5000000000,"term_years":7}'
frame list-entities
# 기간
frame open-period --entity axec --year 2026
frame close-period --entity axec --period-id 7 --status soft_closed
frame list-periods --entity axec
# 추출
frame seed-profile <spec.json>
frame ingest --entity axec --file <path>
frame list-issues --entity axec
# 스크래핑 (외부 포털 → ingest, host-only / launchd)
frame scrape lottecard # 롯데 법인카드 승인내역(개인정보 미포함) → axec 적재
frame scrape lottecard --entity axtest --since 2026-04-01 --until 2026-04-30
# 헤드풀 Playwright (EIWAF 가 headless 차단). 법인전체 = 법인의 모든 카드.
# 자격증명: FRAME_LOTTE_PORTAL_USERNAME / _PASSWORD (vault → .env.local).
frame scrape-due [--dry-run] # launchd tick: shared.scrape_schedule 중 due 인 것만 실행
# 주기는 DB(MCP set_scrape_schedule)로 조정. com.frame.scrape-tick 이 08–21시 매시 호출.
# 회계
frame post-journal --entity axec --spec-path <json>
frame classify --entity axec
frame trial-balance --entity axec --as-of 2026-05-31
frame integrity-check --entity axec
# 토큰 발급
frame mcp-token --sub [email protected] --customer axe \
--entity axec:read,write,close \
--entity axev:read,write,close \
--ttl 2592000
# 서버
frame mcp-serve # stdio
frame mcp-health <token>OAuth-RP 흐름
Claude Code/claude.ai → POST /frame/mcp (no auth)
frame → 401 + WWW-Authenticate: Bearer resource_metadata="..."
client → GET /.well-known/oauth-protected-resource
frame → {resource: "https://axe.axelabs.ai/frame/mcp", authorization_servers: [Microsoft]}
client → Microsoft OAuth flow (PKCE)
Microsoft → access_token (aud = Application ID URI)
client → POST /frame/mcp Authorization: Bearer <access_token>
frame middleware → JWKS RS256 verify + aud + iss + email extract → entity 매핑
frame → MCP session 생성 → tool 호출 가능상세: 인증 · 권한.
Custom Connector 등록 (신규 사용자)
운영자: axe secret send AZURE_FRAME_MCP_CLIENT_SECRET --service frame --to <local-part> → URL 발급 → /api/admin/broadcast-dm 자동 Teams DM. 전체 흐름은 /architecture/secrets § 사람에게 전달. 수신자 화면 절차는 /onboard/claude-frame-setup (5분 가이드).
환경 변수
D-ops-18 (2026-05-21): 비밀 vars (
FRAME_DB_PASSWORD,FRAME_JWT_SECRET,FRAME_SECRET_KEY,AZURE_FRAME_MCP_CLIENT_SECRET,FRAME_PII_PASSPHRASE_*,CLAUDE_CODE_OAUTH_TOKEN) 는 모두 env_file.env.local에서 직접 dump.docker-compose.yml의environment:블록에 default-empty 변수 치환 ($VAR형식) 으로 중복 정의 금지 — Compose precedence 가 env_file 의 vault 값을 shadow 했던 trap. 환경변수 = vault SoT 단일 출처 (axe secret pull frame이 vault →.env.localatomic write).
# DB
FRAME_DB_HOST=postgres
FRAME_DB_PORT=5432
FRAME_DB_USER=frame
FRAME_DB_PASSWORD=
FRAME_DB_NAME=frame
# JWT
FRAME_JWT_SECRET= # ≥32 bytes
AZURE_FRAME_MCP_CLIENT_SECRET= # OAuth confidential client
# PII
FRAME_PII_PASSPHRASE_AXEC=
FRAME_PII_PASSPHRASE_AXEV=
FRAME_PII_PASSPHRASE_REALCHOICE=
FRAME_PII_SALT_DIR=/root/.frame
# MCP HTTP
FRAME_MCP_HOST=0.0.0.0
FRAME_MCP_PORT=3710
FRAME_MCP_LOG_LEVEL=INFO
FRAME_MCP_ALLOWED_HOSTS=frame-mcp:*,localhost:*,...
FRAME_MCP_ALLOWED_ORIGINS=https://axe.axelabs.ai,...
# Multi-tenant
FRAME_CUSTOMERS_YAML=/etc/axe/customers.yaml
FRAME_CUSTOMER_ID=axe
FRAME_DEPLOY_COLOR=blue
# Blueprint platform identity (D-axe-idp-1) — 전부 빈 기본값 = 비활성(non-breaking).
# BLUEPRINT_ISSUER set 시 Blueprint 발급 platform 토큰을 신뢰. 상세: architecture/platform-identity.
BLUEPRINT_ISSUER= # 미설정=무시(현재 동작). set 시 활성
BLUEPRINT_JWKS_URL= # 미설정 시 issuer 에서 유도
BLUEPRINT_AUDIENCE=https://axe.axelabs.ai
# 롯데카드 스크래퍼 (host-only — MCP 컨테이너엔 없음, launchd 만 사용)
FRAME_LOTTE_PORTAL_USERNAME= # 법인관리자 로그인 ID
FRAME_LOTTE_PORTAL_PASSWORD= # vault frame/axe/lotte-portal-password
# Storage (host bind mount)
FRAME_STORAGE_HOST_DIR=${HOME}/frame/.local # default — host 의 home 기준. /Users/axe/... hardcode 금지⚠️
/Users/axe/절대경로 hardcode 금지:FRAME_STORAGE_HOST_DIR(그리고 docker-compose 의 volume mount default) 는 반드시${HOME}/...패턴을 사용. customer macmini 의 운영 user 가/Users/<other>/인 경우 hardcode 된/Users/axe/...는 path 부재 → volume bind fail 또는 frame storage 무효 (2026-05-25 트루비아 측 발견 — R1 fix wrapper.env.local에${HOME}명시, B-frame-keychain-to-vault 후속 fix 후 compose default 도${HOME}으로 자동 일반화).
운영 노트
- Blue/green deploy 지원 (port 3710/3711 동시 실행, alias swap)
- Append-only: raw_transaction, journal_line 의 DELETE/UPDATE 권한이 REVOKE
- PII: pgp_sym_encrypt + entity 별 passphrase (분실 = 영구 손실, 의도)
- 정합성:
frame integrity-check5가지 검사 (journal_balance, audit_completeness, period_consistency, source_link_coverage, evidence_storage_presence — 증빙 bytes 가 디스크에 실존하는지 orphan 탐지). nightlycom.frame.integrity-check, error/blocker → open_item 자동 등록. - LLM 비용 추적:
llm_call테이블에 모든 호출 기록 - 포털 스크래퍼 스케줄·SLA (D-frame-scrape-schedule): 주기는
shared.scrape_schedule의 KST cron (MCPset_scrape_schedule로 조정), 호스트com.frame.scrape-tick이 08–21시 매시 due 만 실행. SLA catch-up — 다운타임 중 놓친 실행은 복구 후 첫 in-window 틱에서 1회 실행하며, look-back 을now−last_run(상한 365일)까지 넓혀 그 기간 전체를 backfill (row_hash 중복제거, 데이터 손실 0). 부팅/wake 시RunAtLoad로 즉시 catch-up,get_scrape_schedule의overdue로 가시화.
신규 customer 1 차 사이클 runbook (D-day ~ D+30)
신규 customer (예: realchoice) 가 6/1 D-day 이후 첫 한 달 동안 frame 내부에 회계 데이터를 적재하고 1 차 사이클을 닫는 권장 순서. B-frame-lpm-classify (자동 분류) 가 완성 (ETA D+90, 아래) 되기 전 시점이라 분개 일부는 수작업 분개로 보완합니다.
| 시점 | 작업 | 명령 / 도구 |
|---|---|---|
| D-day (6/1) | frame stack 부팅 (운영자 측) | axe customers ingest <id> ... --apply + axe onboard <id> --apply |
| D+1 | chart of accounts auto-seed — entity 등록 시점에 44-account KSME 자동 복사 (D-ops-21) | frame register-entity --id <id> --legal-name "..." --kind corporate --accounting-standard ksme |
| D+2 ~ D+7 | 원천 데이터 ingest — KB/거래은행 거래내역 CSV 업로드. LLM 이 포맷 자동 인식 → raw_transaction 적재 | ingest_file_blob MCP tool (또는 frame ingest <file> CLI) → analyze_file_format 자동 호출 |
| D+8 ~ D+14 | trial balance / 결산 잔액 등록 — D2 결정의 “결산 시점 trial balance” 적재. 외부 회계법인 검수본 기준 | register_resolution + post_journal (개시 분개) |
| D+15 ~ D+21 | payroll 매칭 — Hive Phase 1 활성 후 pending_payroll INSERT → frame match_pending_sweep 자동 매칭 → 잔여 수동 매칭 | list_pending_payroll / match_pending_to_raw_tx / match_pending_sweep |
| D+22 ~ D+28 | 수작업 분개 보완 — analyze_file_format 가 인식한 raw_transaction 중 자동 매칭 못 한 것은 운영자 또는 customer 측 회계 담당자가 post_journal 로 수작업 분개. LLM proposal 보조는 가능하나 commit 은 사람 confirm | post_journal + query_trial_balance 으로 매일 진척 확인 |
| D+30 | 1 차 사이클 회고 — 자동 매칭률 / 수작업 분개 시간 / 실패 패턴 측정 → B-frame-lpm-classify (M2 top) 의 prompt 학습 데이터로 활용 | query_trial_balance / income_statement / balance_sheet |
B-frame-lpm-classify ETA (자동 분류 pipeline)
| ETA | 도달 수준 | 회신 시점 |
|---|---|---|
| D+90 (2026-09-01 목표) | minimum viable — LLM 이 raw_transaction 보고 journal_line proposal 생성, 운영자/회계 담당자 confirm 후 commit. 정확도 60~80% (axe 백테스트 기준) | 2026-08 중 베타 → 9 월 production |
| D+120 (2026-10-01) | rule-based pre-classifier 결합 — 반복 거래는 정규식 룰, 1 회 거래는 LLM. 정확도 80%+ | 2026-10 |
| 2026-12-01 | 2027-01-01 cutover 직전 stable — axec/axev 1 년치 backtest 완료 + 회귀 test green | cutover 2 주 전 |
D+30 ~ D+90 동안의 Truvia 측 분개 부담은 **위 표의 D+8D+28 의 수작업 + 매칭 보조** 로 흡수. 1 차 사이클 = 한 달 raw_transaction + payroll + 결산 잔액 = 추정 200500 건 분개. 자동 매칭 0% 가정 시 회계 담당자 1 인 × 2~5 일/월 effort.
Frame 내부 운영의 SSOT
- 본 페이지 = 운영자 측 SSOT (signed by 액스코퍼레이션 주식회사)
/Users/axe/frame/DECISIONS.md= frame 내부 결정 누적/services/hive의 payroll 흐름 = framepending_payroll의 입력 채널- 외부 회계법인 cross-check workflow =
B-frame-cross-check-workflow(별도 backlog, Truvia 측 외부 회계법인 회신 시 등재)
관련 문서
- Blueprint service — MCP 클라이언트
- Frame OAuth 설정 (Azure)
- 신규 직원 connector setup
- /Users/axe/frame/DECISIONS.md — D-ops-1~15
- /Users/axe/frame/docs/ops/ — 운영 정책 6 파일