# AXE Labs Docs — 전체 본문 (LLM 단일 ingest 용) 생성: 2026-06-08T16:08:51.801Z 사이트: https://docs.axelabs.ai 각 페이지는 URL prefix + frontmatter + 본문 형식. 페이지 경계는 "" 마커. --- # AXE Labs Platform > 멀티테넌트 SaaS 플랫폼 — 액스코퍼레이션 주식회사 (`axec`) 가 운영하는 자체 인프라. URL: https://docs.axelabs.ai/ # AXE Labs Platform > 👋 **처음 오셨나요?** 본인 역할로 바로 이동하세요 — 이 사이트 외 다른 첨부·메시지 의존 없이 자력 완료 가능합니다. > > | 누구이신가요? | 가는 곳 | 소요 | > |---|---|---| > | **고객사 IT 담당자** (회사가 AXE Labs 도입 결정 → 자기 회사 macmini 준비 중) | [→ 고객사 IT 가이드](/partner) (4-step) | ~45 분 | > | **신규 직원** (회사가 이미 AXE Labs 사용 중 → 본인 connector setup) | [→ 신규 직원 온보딩](/onboard) | ~10 분 | > | **운영자 (액스코퍼레이션 주식회사)** | [→ 운영자 runbook](/ops/runbook/customer-onboarding) | — | AXE Labs 는 **운영 주체 (액스코퍼레이션 주식회사) 가 다수의 customer 를 동일 패턴으로 호스팅** 할 수 있도록 설계된 멀티테넌트 SaaS 플랫폼입니다. 각 customer (현재 axe, 진행 중 realchoice) 는 자기 macmini 위에서 동일한 stack — frame · blueprint · vault · index — 을 실행하고, 운영 주체의 자동화 계정 (`ai@axellc.com`) 이 단일 CLI 와 SSOT 설정 파일(`customers.yaml`)로 전체를 관리합니다. ## 핵심 서비스 ``` ┌────────────────────────────────────────────────────────────────┐ │ AXE LABS — multi-tenant platform │ ├────────────────────────────────────────────────────────────────┤ │ │ │ blueprint : AI-native workspace (Next.js + Claude SDK) │ │ frame : 회계 backend (Postgres + MCP) │ │ stream : S&OP MCP (sales · inventory · settlement) │ │ magnet : 마케팅 자동화 MCP (Meta / Naver / Threads) │ │ vault : Self-host Vaultwarden + Microsoft SSO │ │ index : (예정) 검색·전체 색인 layer │ │ │ └────────────────────────────────────────────────────────────────┘ ``` 각 서비스는 별도 git repo + Docker Compose 단위로 배포되며, customer 격리는 **OS-level (macmini 1대 = customer 1개)** 입니다. ## 문서 구성 | 섹션 | 대상 | 인증 | |---|---|---| | [플랫폼 아키텍처](/architecture) | 모든 독자 | 공개 | | [서비스 카탈로그](/services) | 모든 독자 | 공개 | | [직원 온보딩](/onboard) | 신규 직원 | 공개 | | [고객사 IT 가이드](/partner) | 고객사 IT 담당자 | 공개 (민감 정보는 별도 채널) | | [운영자 (내부)](/ops) | 운영자 (`ai@axellc.com`) | Microsoft Entra ID 로그인 | | [운영 정책](/operations) | 운영자 + 감사인 | 공개 (정책 자체) | ## Quick Links - **고객사 IT (도입 결정 후 첫 방문)**: [고객사 IT 가이드 — 4-step 흐름](/partner) - **고객사 IT (Azure CLI 1줄)**: [Entra ID 앱 등록 §Option A](/partner/registration#option-a--cli-1-줄-권장) - **신규 직원**: [Frame connector 4-step setup](/onboard/claude-frame-setup) - **터미널 · Claude Code · Codex · CI**: [AXE CLI (`axe`) 설치 + 로그인 1회](/services/cli) — MCP 커넥터 불요, 토큰 하나로 전 서비스 - **운영자**: [신규 customer onboarding](/ops/runbook/customer-onboarding) - **아키텍처 결정**: [DECISIONS](/ops/decisions) ## AI / LLM 진입점 - **[`/llms.txt`](https://docs.axelabs.ai/llms.txt)** — [llmstxt.org](https://llmstxt.org/) 스펙 페이지 인덱스. AI agent 가 사이트 구조를 파악하는 표준 entry point. - **[`/llms-full.txt`](https://docs.axelabs.ai/llms-full.txt)** — 전체 페이지 본문을 1 파일로 concat (현재 ~190 KB). 단일 ingest 로 컨텍스트 전체 로딩. - **[`/api/search?q=...`](https://docs.axelabs.ai/api/search?q=OAuth)** — JSON full-text search. score 기반 ranking + snippet + markdown_url. LLM 이 페이지 위치 찾을 때. - **Per-page raw markdown** — 모든 URL 에 `.md` suffix (예: [`/services/frame.md`](https://docs.axelabs.ai/services/frame.md)). frontmatter + canonical URL 주석 포함. - **[`/sitemap.xml`](https://docs.axelabs.ai/sitemap.xml)** — 전체 URL 목록. - **[GitHub repo](https://github.com/soohunkang/axelabs-docs)** — mdx 원본 직접 조회. ## 도메인 / URL 컨벤션 ``` {customer}.axelabs.ai/ ← blueprint (apex) {customer}.axelabs.ai/frame/mcp ← frame MCP endpoint {customer}.axelabs.ai/vault ← Vaultwarden admin.axelabs.ai ← 운영자 콘솔 (Microsoft SSO 게이트) docs.axelabs.ai ← 본 문서 (이곳) ``` * `axelabs.ai` — 플랫폼 (이 docs 가 있는 곳) * `axellc.com` — corporate (이메일·기업 홈페이지, 플랫폼과 분리) ## 운영 메타 | 항목 | 값 | |---|---| | 운영 주체 | 액스코퍼레이션 주식회사 (`axec` entity, 자동화 계정 `ai@axellc.com`) | | 1차 customer | axe (라이브 2025-01) | | 2차 customer | realchoice (예정 2026-06-01) | | 회계 기준 | KSME / K-GAAP | | 시간대 | Asia/Seoul | | 인증 | Microsoft Entra ID OAuth 2.0 / OIDC | | 데이터 격리 | OS-level (customer/macmini) + schema-per-entity (DB) | --- # 플랫폼 아키텍처 > AXE Labs 멀티테넌트 SaaS 플랫폼의 토폴로지, 도메인, 인증, 격리 모델. URL: https://docs.axelabs.ai/architecture # 플랫폼 아키텍처 AXE Labs 의 아키텍처는 **소규모 운영자(1인) 가 다수의 customer 를 일관된 패턴으로 호스팅** 할 수 있도록 설계되었습니다. 모든 결정은 [DECISIONS](/ops/decisions) 에 누적 기록되며, 본 페이지는 그 결과의 정적 요약입니다. > 약어가 익숙하지 않으면 [용어 사전](/glossary) 먼저 참조. 핵심: **MCP** (Model Context Protocol, AI 가 외부 도구·데이터에 접근하는 표준), **OIDC** (OpenID Connect, 신원 확인), **PKCE** (Proof Key for Code Exchange, OAuth 코드 가로채기 방지). ## 6 가지 설계 원칙 1. **격리는 OS-level — customer 1개 = macmini 1대.** 다중 customer 가 한 머신을 공유하지 않습니다. SPOF (macmini 자체 장애) 는 cold storage 백업으로 흡수. 2. **테넌트 ID = 이메일 도메인 → `customers.yaml` 매핑.** 사용자가 `ai@axellc.com` 으로 로그인하면 `axellc.com → axe` customer, `realchoice.co.kr → realchoice` customer 로 자동 라우팅. 3. **Path-based 1-level subdomain.** `{customer}.axelabs.ai/{service}` 형식. 2-level wildcard SSL 무료 미지원 → path-based 로 회피. 4. **Tailscale 메시 + SSH key.** 운영자 ↔ customer macmini 간 push 기반 deploy. 자체 mTLS X. 5. **Microsoft Entra ID + customer 별 own tenant.** 각 customer 의 직원은 자기 회사 tenant 로 인증. 6. **GitHub source-of-truth + push deploy.** customer macmini 가 pull-polling 하지 않음. ## High-Level Topology ``` ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ 직원 노트북 (Windows / macOS) │ │ ├─ Claude Code 네이티브 앱 + Microsoft Entra ID 로그인 │ │ └─ claude.ai 측에 Custom Connector 1회 등록 │ │ │ │ │ ↓ (Anthropic backend OAuth flow) │ │ │ │ Microsoft Entra ID (customer tenant) │ │ - axe: 122fb574-... (axellc.com) │ │ - realchoice: <TBD> (realchoice.co.kr) │ │ │ │ ↓ (Bearer access_token, aud = MCP URL) │ │ │ │ Cloudflare DNS / Cloudflare Tunnel (axelabs-tunnel) │ │ │ │ │ ↓ (host.docker.internal:3712) │ │ │ │ ┌──── customer macmini (axe-macmini / realchoice-macmini) ────┐ │ │ │ │ │ │ │ axe-frame-proxy (Caddy, port 3712) ← blue/green selector │ │ │ │ │ │ │ │ │ ├──→ frame-mcp-blue :3710 (active) │ │ │ │ └──→ frame-mcp-green :3711 (passive) │ │ │ │ │ │ │ │ │ ↓ │ │ │ │ frame-postgres :3700 │ │ │ │ │ │ │ │ blueprint :3100 axe-vaultwarden :443/vault │ │ │ │ stream :8780 magnet :8770 │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────┘ ``` ## 인증 흐름 (간략) ``` 1. 직원 → claude.ai Custom Connector → URL https://{customer}.axelabs.ai/frame/mcp 2. claude.ai 백엔드 → frame /mcp (no auth) → 401 + WWW-Authenticate 3. claude.ai → /.well-known/oauth-protected-resource → AS = Microsoft Entra 4. claude.ai → Microsoft authorize 엔드포인트 (PKCE S256 + client_secret) 5. 브라우저 → Microsoft 로그인 → consent → code 발급 6. claude.ai → Microsoft /token → access_token (aud = Application ID URI) 7. claude.ai → frame /mcp with Authorization: Bearer <access_token> 8. frame middleware → Microsoft JWKS RS256 검증 + audience match + entity 매핑 9. frame → MCP session 생성 → tools 호출 ``` 상세는 [인증 · 권한](/architecture/auth) 참조. ## 다음 단계 - [Topology · 네트워크](/architecture/topology) — Cloudflare Tunnel, Docker network, proxy 패턴 상세 - [도메인 · DNS](/architecture/domains) — Cloudflare zone, 1-level subdomain 결정 근거 - [인증 · 권한](/architecture/auth) — Microsoft Entra ID, JWT, dual-token model - [데이터 격리 모델](/architecture/data) — schema-per-entity, audit_log, PII 암호화 - [배포 · 무중단](/architecture/deploy) — blue/green frame swap, axe-frame-proxy - [백업 · DR](/architecture/backup) — restic 3-tier (local / ring P2P / cold SSD) --- # Artifact 모델 · PARA 지식 레이어 > Blueprint 의 typed fact layer. PARA 와 결합해 work/knowledge 를 organize. MCPs/Teams/Mail/OneDrive 의 citation chain. URL: https://docs.axelabs.ai/architecture/artifacts # Artifact 모델 · PARA 지식 레이어 > **[2026-06-06 방향 갱신]** PARA 의 상위 설계 철학은 [/architecture/para-os](/architecture/para-os) 로 정리됨 (PARA=조직론, dispatch="집"/link-default, governance 결정로그, 죽은 딜 본질). 본 페이지의 **dispatch `copy-with-provenance` → `link-default` 로 revise** ([D-bp-para-1](/ops/decisions)), Area=workspace-container → 조직론 노드 + R/A 내포. 단 저장·citation·schema discovery 세부는 본 페이지가 유효한 reference. > > **[2026-06-06 구현 토폴로지 — [D-bp-rust-1](/ops/decisions)]** Artifact 레이어는 Blueprint 의 **첫 Rust organ** 으로 이관된다 (TS→Rust strangler-fig). 신규 substrate 서비스(Rust+axum)가 `blueprint-postgres` 의 **`substrate` schema** 에서 `artifact`/`artifact_link`/`mcp_schema` 를 소유 — `workspace_id`·`entity_id` 는 opaque 외부 ref(**Artifact-scoped 경계**), Workspace+paraLayer SoR 는 Prisma `public` 잔류. 모델 교정(처음부터 1급): **home/link("집")** = `artifact_link` 가 멤버십 본체 + **`annotation`**(Area별 해석), `parent_artifact_id` → **`derived_from_id`**(진짜 파생만), **2-level `scope`**(personal/shared), **citation 8종**(아래 7종 + `gate.decision` [D-gate-2](/ops/decisions)). 아래 본문의 Prisma SQL·"copy default"·"7 kind" 서술은 **현 TS(M6 Stage-1) 기록**이며 cutover(ADR §6 S3) 전까지 병존 — 권위 = [D-bp-rust-1](/ops/decisions) + ADR `docs/adr/blueprint-rust-migration.md`. ## 한 줄 정의 **Artifact = Blueprint 의 typed fact unit**. source 문서가 아니라 agent 가 직접 read 하는, 사전 추출된 사실 한 조각. per-field schema + per-field citation + confidence + audit trail + paraLayer scope 를 갖춤. Blueprint Postgres 거주 (D-config-16), large content / 원본 파일은 OneDrive · frame · hive · Teams · mail 등 원위치 유지하고 artifact 는 stable ID 로만 참조 — data 중복 0. ## 왜 필요한가 현재 Blueprint 의 knowledge layer 는 [ctx skill](https://docs.axelabs.ai/services/blueprint) 의 markdown PKM 한 단계뿐. agent 가 grep + cat 으로 markdown 본문을 읽어 사실을 추출하는 방식이라 다음이 모두 불가능: | 격차 | 현재 (markdown only) | 결과 | |---|---|---| | Cross-functional query | "portfolio cos 중 Q4 churn > 15%" 를 grep 으로 못 풀음 | Area query 가 LLM 토큰 폭증 | | Field-level provenance | 사실 한 줄의 출처 추적 불가 (markdown 안에 인용 표기 의무 X) | citation chain 끊김, IC 결정의 audit 불가능 | | Time-travel | "Project Deal X 가 지난 주 이후 무엇 바뀌었나" 답 불가 | 의사결정의 시점형 archaeology 불가 | | 결정론적 충돌 해소 | 두 agent 가 같은 markdown 동시 편집 시 last-write-wins | 사실 무결성 보장 X | | Multi-agent concurrent read-write | markdown lock 없음 | 동시 작업 시 race | 격차의 본질: 인프라는 dev-co level (per-customer macmini isolation · blue/green deploy · OAuth-RP · restic backup · 7-stack 응집) 이지만 knowledge layer 만 single-operator PKM tool 수준에 머문다. AI-native fund 가 자기 의사결정의 SOT 로 쓰려면 typed fact layer 가 필요. ## PARA 와의 관계 PARA (Tiago Forte) 의 Project / Area / Resource / Archive 4 layer 각각에 typed artifact 집합이 거주. PARA layer 가 artifact 의 lifecycle 과 visibility 를 결정. | Layer | 역할 | 대표 artifact 유형 | |---|---|---| | **Project** | 기한 + goal cross-functional 컨테이너 (IC for Deal X, Fund 2 fundraising, 2026 Q3 LP report) | `ICMemoArtifact` · `BoardMeetingNotesArtifact` · `LPCapitalCallArtifact` | | **Area** | function 별 지속 reference state (CFO · IR · Sourcing · Portfolio). read 빈도 높음, 끊임없는 갱신 | `PortcoBoardKPIArtifact` (Portfolio Monitoring) | | **Resource** | 재사용 가능한 versioned framework + template | `SectorFrameworkArtifact` | | **Archive** | 기한 지난 Project + outdated resource. pattern detection corpus | 위 모든 종류의 frozen snapshot | Project 가 closeout 되면 그 안의 artifact 들이 typed fact 단위로 Area / Resource / Archive 로 field-level dispatch. [M3 PARA Dispatch](/ops/roadmap#m3--blueprint-para-dispatch-d-bp-entity-1-pr-5) 가 현재 workspace-level (file-level) provenance (`sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt`, [D-bp-entity-2](/ops/decisions)) 까지 land — Artifact 는 그 위에서 **field-level** 진화. ### Cross-PARA query 예시 현재 ctx markdown 으로 불가능하지만 artifact layer 위에서 자연: - "portfolio cos 중 Q4 churn > 15%" — Area query (`PortcoBoardKPIArtifact.confidence ≥ 0.8`) - "과거 deal 중 현재 Sentry 와 IRR trajectory 비슷한 것" — Archive cross-Project pattern match - "portco Y 가장 최근 revenue, citation 포함" — Area lookup with citation chain - "Project Deal X 가 지난 주 이후 무엇 바뀌었나" — time-travel diff - "LP X 와 마지막 comm 후 변한 portfolio fact" — IR Area × Portfolio Area cross-query ## 데이터 모델 Blueprint Postgres 의 `Artifact` + `Citation` 테이블 (Prisma). schema 는 versioned (`schema_id` FK), content 는 JSONB (schema 별 typed field), citations 는 JSONB array. ```sql CREATE TABLE artifact ( id uuid PRIMARY KEY, schema_id text NOT NULL REFERENCES artifact_schema(id), -- MCP discovery schema (e.g., `frame.balance@1.0`, `hive.employee@1.0`) 또는 free-form (`freeform.llm@auto`). MCP authority — Blueprint 는 registry 아니라 discovery/mirror workspace_id uuid NOT NULL REFERENCES workspace(id), entity_id text NOT NULL REFERENCES entity(slug), -- D-bp-entity-3 NOT NULL para_layer text NOT NULL CHECK (para_layer IN ('PROJECT','AREA','RESOURCE','ARCHIVE')), content jsonb NOT NULL, -- schema 별 typed field citations jsonb NOT NULL DEFAULT '[]'::jsonb, -- per-field citation array confidence numeric(3,2) NOT NULL DEFAULT 1.0, -- 0.00 ~ 1.00 audit_trail jsonb NOT NULL DEFAULT '[]'::jsonb, -- propose/review/confirm/edit events parent_artifact_id uuid REFERENCES artifact(id), -- PARA dispatch fork chain created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), archived_at timestamptz -- soft delete (Archive 진입 시 set) ); CREATE INDEX artifact_workspace_idx ON artifact (workspace_id); CREATE INDEX artifact_entity_para_idx ON artifact (entity_id, para_layer); CREATE INDEX artifact_schema_idx ON artifact (schema_id); CREATE INDEX artifact_parent_idx ON artifact (parent_artifact_id) WHERE parent_artifact_id IS NOT NULL; ``` `audit_trail` 의 한 entry 는 `{ event, actor, ts, diff?, prompt? }` 형식. 변경 history 가 row 안에 누적되며 `updated_at` 만 갱신 — pg_jsonschema validation 으로 무결성 가드 (확장 add-on). ## Citation 형태 Citation 은 외부 source 의 **stable ID** 만 가짐. data 자체는 원위치 (OneDrive driveItem / frame query / hive ID / index DB ref / gate decision cert / Teams chat+msg / mail thread+msg / external URL) — artifact 에 중복 0. 8 kind: ```jsonc // content 의 field 별 1+ citation { "field": "revenue_q4_2025", "citations": [ // 1) OneDrive Excel cell { "kind": "onedrive", "drive_item_id": "01ABCD...", "version": "v3", "sheet": "Q4 Recurring", "cell": "B14" }, // 2) frame typed query (회계 도메인) { "kind": "frame.balance", "entity": "axec", "account": "4001", "period": "2025-Q4", "queried_at": "2026-05-22T03:14:00Z" }, // 3) hive employee record (HR 도메인) { "kind": "hive.employee", "entity": "axev", "employee_id": "uuid-...", "queried_at": "2026-05-22T03:14:00Z" }, // 4) Teams chat message { "kind": "teams.message", "chat_id": "19:...", "message_id": "1700000000000" }, // 5) Mail thread message { "kind": "mail.thread", "thread_id": "AAMkAD...", "message_id": "AAMkAD...." }, // 6) External web fetch (snapshot 시점 명시) { "kind": "external.web", "url": "https://...", "fetched_at": "2026-05-22T03:14:00Z" }, // 7) index typed query (투자 도메인 — D-index-6) { "kind": "index.financial_output", "deal_id": "uuid-iippo", "model_id": "uuid-iippo-v7-base", "scenario_code": "weighted", "output_code": "irr_loss_included", "computed_at": "2026-05-06T10:00:00Z" }, // 기타 index.* sub-kinds: index.deal · index.fund_investment · // index.dd_finding · index.ic_decision · index.portfolio_kpi · // index.financial_model · index.valuation · index.lp_comm 등 // 8) gate 거버넌스 결정 (gate.decision SoR 의 mirror — D-gate-2) { "kind": "gate.decision", "decision_id": "uuid-...", "cert_id": "modusign-...", // 모두싸인 서명완료 cert (citation only) "decided_at": "2026-06-06T01:00:00Z" } ] } ``` 각 kind 에 대응하는 **citation resolver** (`src/lib/artifact/citations/`) 가 stable ID 로 실 데이터를 fetch + cache. Trinity dashboard (Workspace × OneDrive × TeamsChat) 의 진화형 — 기존 GUID 3축 → field 단위 citation chain. ## MCPs 와의 분리 별 트랙. **MCP = domain-specific data system**, **Artifact + PARA = Blueprint 내부 work/knowledge organization meta-layer**. | Layer | 책임 | |---|---| | frame MCP | 회계 도메인 데이터 (journal, balance, period) — schema-per-entity Postgres | | hive MCP | HR 도메인 데이터 (employee, payroll, leave) — frame mirror | | magnet / stream / 기타 MCP | 각 도메인 데이터 시스템 | | **Artifact + PARA (Blueprint)** | **위 MCP 들이 발행하는 사실을 typed fact 로 organize. citation 으로 stable ID 참조** | Artifact 가 frame 의 `query_balance` 결과를 자기 안에 복사하지 않음. citation `{ kind: "frame.balance", entity, account, period, queried_at }` 만 보유. agent 가 artifact read 시 resolver 가 frame MCP 를 다시 호출하거나 cache 사용. MCP backend 가 sole owner — artifact 는 reference + interpretation 의 home. ### Schema authority 분산 Schema 의 권위는 **MCP service** (frame · hive · magnet · stream · Matrix · …). 각 MCP 가 자기 도메인 schema 를 정의하고 `/schemas` endpoint 로 노출. Blueprint 는 그 schema 들을 **discovery / mirror** — fetch + cache + version. registry 아님. 새 MCP 추가 시 자기 schema 를 가지고 들어오므로 Blueprint 측 변경 0. MCP 가 정한 규칙이 없는 영역 (IC memo / Board meeting / Trip / LP comm / Sector framework 등 free-form) 은 **LLM 자율** — extract 시점에 ad-hoc typed structure 결정하거나 free-form content + citation/confidence wrap 으로 보존. AXE 가 미리 schema 다 정의할 필요 없음. | 영역 | Schema authority | 예 schema_id | |---|---|---| | 회계 (journal, balance, period, account) | **frame MCP** | `frame.balance@1.0` · `frame.journal@1.0` | | HR (employee, payroll, leave, compensation) | **hive MCP** | `hive.employee@1.0` · `hive.payslip@1.0` | | 투자 (deal, fund_investment, financial_model, dd_finding, ic_decision, portfolio_kpi, valuation, lp_comm) | **index MCP** | `index.deal@1.0` · `index.fund_investment@1.0` · `index.financial_output@1.0` ([14 schemas](/services/index/schema-catalog)) | | 마케팅 (campaign, channel, lead) | **magnet MCP** | `magnet.campaign@1.0` | | S&OP (forecast, plan) | **stream MCP** | `stream.forecast@1.0` | | 모니터링 (health, alert) | **Matrix MCP** | `matrix.health@1.0` | | 관계망 (person, interaction, relationship) | **cortex MCP** | `cortex.person@1.0` · `cortex.interaction@1.0` | | Board meeting / Trip / Sector framework (도메인 외 free-form) | **LLM 자율** | `freeform.llm@auto` 또는 skill-defined (`ic.memo@1.0`) | `/schemas` endpoint contract (각 MCP 가 표준화): ```jsonc GET /schemas { "service": "frame", "version": "1.2.0", "schemas": [ { "id": "frame.balance@1.0", "fields": { ... }, "description": "..." }, { "id": "frame.journal@1.0", "fields": { ... }, "description": "..." } ] } ``` Blueprint 의 `src/lib/artifact/schemas/` 가 각 MCP 의 `/schemas` 를 주기적으로 fetch → `artifact_schema` 테이블 upsert. 변경 감지 시 새 version row 추가 (기존 row 미변경 — 기존 artifact 의 citation chain 보존). ## ctx skill 의 진화 **대체 아님, 진화.** 기존 `ctx` (agent session delta → markdown append + parent KB propagation) 는 artifact store 의 **curation interface** 로 재정의. ``` source (OneDrive doc / frame query / Teams chat / mail thread / web fetch) ↓ ingest (skill: ingest, 별도 skill 신설 가능) ↓ typed extraction (schema-aware LLM extract, e.g., ICMemoArtifact §재무) ↓ proposed fact (audit_trail entry: { event: "propose", actor: "agent", ... }) ↓ ctx review (user confirm / edit / reject — UX 가 markdown diff + citation hover) ↓ confirmed fact + audit (audit_trail entry: { event: "confirm", actor: "user", ... }) ↓ agent reads via Query API (grep 안 함 — typed query + citation chain) ``` Markdown 은 보조 — review UX 의 diff 표면 + archive 보존 + emergency cat (DB 장애 시). primary read path 는 Query API. ### Markdown → artifact 점진 migration (AI-assisted) 기존 markdown PKM (`ctx/MEMORY.md` 의 누적 entry, project_*.md, feedback_*.md 등) 은 **목표는 전면 artifact 화**. 단 mass migration 안 함 — **항목 단위 AI-assisted ctx review** 로 점진: ``` 기존 markdown 파일 1개 (예: ctx/MEMORY.md 의 한 entry) ↓ LLM 이 read → typed extract 시도 (MCP discovery schema 매칭 또는 LLM 자율 free-form) ↓ proposed artifact (audit_trail: { event: "migration_propose", actor: "agent", source_markdown: "ctx/MEMORY.md#L42-58", ts: "..." }) ↓ ctx review (사용자 confirm / edit / reject — UX 가 source markdown 발췌 + extracted typed fact 나란히) ↓ confirmed artifact + 기존 markdown entry 에 `` archive 마킹 ↓ markdown 파일 전체 entry 가 모두 migrate 되면 그 파일 자체 deprecation ↓ 신규 fact 는 처음부터 artifact 로 — markdown append 안 함 ``` 핵심 원칙: - **점진** — 1주에 1 파일 또는 1 batch (10 entry) 수준. 인간 정비 cadence 안에서 - **AI-assisted** — LLM 이 자동 extract, 사람은 confirm 만 (인간 개입 최소화) - **archive 마킹** — migrated entry 가 어떤 artifact 로 변환됐는지 trace 보존 (``). 이후 audit / 재migration 가능 - **신규 fact = artifact 처음부터** — 마이그레이션 완료 전이라도 새 사실은 artifact 로 직접 들어옴 (markdown append 옵션 안 줌) ## Workspace lifecycle PARA 4 layer 는 Workspace 의 `paraLayer` 컬럼 (PROJECT / AREA / RESOURCE / ARCHIVE) 으로 표현. 각 Workspace 는 정확히 1 paraLayer 보유. lifecycle 의 핵심: **Dispatch ≠ Close** — 두 활동 분리. ### 4 paraLayer transition ``` create(paraLayer=PROJECT|AREA|RESOURCE) │ ▼ ┌──────── PROJECT (working) ─────────┐ │ │ │ dispatch (anytime — copy/link) │ │ artifact/file 을 다른 Workspace 로 │ │ reclassify (any-to-any) │ │ │ └────── close (paraLayer 변경 1회) ────┘ │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ AREA RESOURCE ARCHIVE (현재형) (재사용) (시점형) │ │ │ │ reclassify bidirectional 가능 │ │ (ARCHIVE resurrect 포함) │ └─────────────────┴─────────────────┘ ``` 규칙: - **create** — paraLayer 선택 (PROJECT 가 default, AREA/RESOURCE 도 직접 가능) - **dispatch (anytime)** — Project 진행 중에도 file/artifact 를 다른 Workspace 로 흘려보냄. 3 mode: **link**(기본, 집 배정) / **copy_curate**(재사용본 저작→Resource) / **move**(소모성→Archive) — 아래 §Dispatch 3 mode - **close (한 번)** — `workspace.paraLayer` 를 한 번 변경. PROJECT → ARCHIVE (보통) 또는 PROJECT → AREA/RESOURCE (reclassify). audit trail 에 close event 기록 - **reclassify (bidirectional)** — ARCHIVE → AREA 도 가능 (resurrect). 단 audit_trail 보존 — 누가 언제 왜 resurrect 했는지 ### Dispatch 3 mode — link(기본) / copy-curate / move ([D-bp-para-1](/ops/decisions)) > dispatch = "집(home)" 배정 ([para-os §6](/architecture/para-os)). **link 가 기본** (artifact 몸통 1 + 집 N). 구 "copy-default"(`copy-with-provenance`)는 폐기 — copy 는 *재사용본 저작* 예외만. | Mode | 의미 | 사용 사례 | |---|---|---| | **link** (기본) | `artifact_link` 에 (artifact_id, workspace_id, link_type, **annotation**, linked_at, linked_by) row 추가. artifact 본문 1 row, 여러 Workspace 가 share. `annotation` = Area별 해석 레이어 | 같은 PortcoBoardKPI 를 Portfolio Area + IC Project + LP Project 가 동시 참조 (각 Area 다른 annotation) | | **copy-curate** | method/지식을 *새 Resource artifact 로 저작* (사실 복제 아님). lineage 는 `derived_from_id`(진짜 파생만, 구 `parent_artifact_id`) | 죽은 딜 경쟁분석 → 섹터 Resource 큐레이션, 펀드 결성 템플릿 1호→2호 | | **move** | 소모성 working corpus → Archive → 폐기 (workspace 레벨 + OneDrive trinity-sync) | LoI 초안 v1~v5, dataroom 원본, 회의 스크래치 | `artifact_link` 테이블: ```sql CREATE TABLE artifact_link ( artifact_id uuid NOT NULL REFERENCES artifact(id) ON DELETE CASCADE, workspace_id uuid NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, link_type text NOT NULL CHECK (link_type IN ('reference', 'sub_project', 'cross_link')), linked_at timestamptz NOT NULL DEFAULT now(), linked_by text NOT NULL, -- user.id or agent.id PRIMARY KEY (artifact_id, workspace_id) ); CREATE INDEX artifact_link_workspace_idx ON artifact_link (workspace_id); ``` dispatch API (substrate, ADR §6 S1): - `dispatch_artifact(artifact_id, to_workspace_id, mode="link"|"copy_curate"|"move", annotation?)` → **link**(기본) = `artifact_link` row INSERT(+annotation), **copy_curate** = 새 Resource artifact + `derived_from_id`, **move** = workspace 레벨 + OneDrive 이동 - `unlink_artifact(artifact_id, workspace_id)` → link 해제 (`artifact_link` row DELETE). copy_curate 산출은 별 artifact 라 unlink 대상 아님 ### Area cardinality + AXEV 현재 seed **Area 1개 / function**: 중복 function 의 Area 가 둘 생기지 않도록 UI/LLM 가드. 새 Area 만들기 시도 시 기존 Area 와 function overlap 검출 → "기존 `` 와 merge 권고" 모달. operator 가 override 시도 시 audit 기록. AXEV 현재 Area seed (2026-05-23): | Area | Function | 비고 | |---|---|---| | **Finance** | 회계 / 재무 / cash flow / 예산 | frame MCP 의 data 가 주 | | **BOD** | 이사회 / 결의 / 거버넌스 | resolution + meeting note | | **Legal** | 계약 / 컴플라이언스 / 등기 | 법무 사항 | | **License** | AC (Advisory Committee) 라이센스 관리 | AC 관련 | Resource = 0 (아직 없음). naturally emerge — 첫 Project close 시점 또는 첫 framework consolidate 시점에 생성. ### UX 흐름 4 페이지 분리 UI 측면에서 paraLayer 별 entry point 를 분리 (cross-PARA query 는 별 surface): | URL | 역할 | 표시 | |---|---|---| | `/axe/projects` | 진행 중 Project 리스트 + close button | PROJECT only | | `/axe/areas` | function 별 living knowledge 그리드 + last-updated heatmap | AREA only | | `/axe/resources` | reusable template/framework | RESOURCE only | | `/axe/archive` | 시점형 의사결정 archaeology + search | ARCHIVE only | 각 page 에 "+New Workspace" 시 paraLayer 가 그 page 에 fix (PROJECT page = 새 PROJECT, AREA page = 새 AREA …). 4 page 위에 cross-PARA query 는 Knowledge Overview dashboard 에서 진입. Workspace create 시 paraLayer 선택 UI (mock): ``` ┌─ New Workspace ──────────────────────────────┐ │ Name: [_______________________________] │ │ Entity: [axev ▾] │ │ paraLayer: ( ) Project ( ) Area │ │ ( ) Resource ( ) Archive │ │ │ │ ▶ Target close date (optional, PROJECT only) │ │ [____________] │ │ │ │ [Cancel] [Create] │ └──────────────────────────────────────────────┘ ``` ### OneDrive 폴더 mirror trinity-sync 가 Workspace 의 paraLayer 변경을 OneDrive 폴더 이동으로 mirror. **2 시점** 에 file move 발생: | 시점 | 동작 | 예 | |---|---|---| | **dispatch (file 별)** | artifact 에 attached 된 file 1개를 다른 Workspace 로 dispatch 시, 그 file 만 destination Workspace 의 OneDrive 폴더로 부분 이동 (copy mode = OneDrive copy, link mode = OneDrive shortcut/symlink) | IC artifact 의 spreadsheet 1개를 Portfolio Area 로 link | | **close (workspace 전체)** | Workspace.paraLayer 변경 시 그 Workspace 의 OneDrive 폴더 전체가 destination paraLayer 의 root 로 이동 | `/Ventures/1. Project/Deal X/` → `/Ventures/4. Archive/Deal X/` | trinity-sync 가 OneDrive driveItem 이동 trigger + Blueprint Workspace.drivePath 업데이트. paraLayer 변경 자동 propagation. ## Knowledge Overview dashboard 회사 지식체계 P → A/R → A 흐름을 한눈에 조망하는 별도 page (`/axe/knowledge`). 4 paraLayer page 위에 cross-PARA view. ### 3-pane layout ``` ┌─────────────────────────────────────────────────────────────┐ │ Knowledge Overview (axev) │ ├──────────────────┬──────────────────┬───────────────────────┤ │ Layer counts + │ Sankey: PARA │ Cross-PARA query 입력 │ │ state │ flow (지난 30일) │ │ │ │ │ "portfolio cos 중 │ │ PROJECT: 12 │ PROJECT │ Q4 churn > 15%" │ │ - active: 8 │ ↓ dispatch │ │ │ - closing: 2 │ copy: 23 │ [Query] [Save] │ │ - stale: 2 │ link: 15 │ │ │ │ ↓ │ 결과: 3 artifact │ │ AREA: 4 │ AREA RESOURCE │ - portco Y (0.18) │ │ - Finance │ ↓ │ - portco Z (0.17) │ │ - BOD │ ↓ reclassify │ - portco W (0.16) │ │ - Legal │ 3 (manual) │ [view citation] │ │ - License │ │ │ │ │ ARCHIVE: │ │ │ RESOURCE: 0 │ close: 5 │ │ │ ARCHIVE: 27 │ resurrect: 1 │ │ └──────────────────┴──────────────────┴───────────────────────┘ ``` ### Area health alert 각 Area 의 last-updated timestamp 기반: - **stale** (30일 무변경) — Area 카드에 yellow ⚠ banner - **inactive** (90일 무변경) — Area 카드에 red ⏸ banner + "이 Area 가 living 인지 재평가" 알림 운영자 review queue 에 자동 추가. ### 추가 view (drill-down) | View | 표시 | |---|---| | **PARA flow over time** | 월 별 P→A/R/A dispatch count 추세 (lifecycle 활성도) | | **Artifact graph** | citation chain 의 graph 시각화 (artifact ↔ citation source) — 의존성 spot | | **Provenance trace** | 1 artifact 선택 → 그 artifact 의 모든 dispatch / link / migration history (audit_trail timeline) | ### 의도 Knowledge Overview 의 자체 가치 — **회사의 fact health 가 한눈에**. PARA OS 가 단지 "파일 정리" 가 아니라 살아있는 지식체계라는 것을 운영자/직원이 daily 로 확인할 surface. inactive Area, dispatch 끊긴 흐름, citation chain 깨진 artifact 가 시각적으로 detect 됨. ## MCP tool 분리 (paraLayer 별) UI 4 page 분리와 같은 원칙으로 MCP tool 도 paraLayer 별 분리. **LLM intent clarity** — tool name 자체에 paraLayer 의도 드러남. ### Workspace level tools | Tool | 역할 | |---|---| | `list_projects(entity?, state?)` | PROJECT only, state = active / closing / stale | | `list_areas(entity?)` | AREA only, last_updated 정렬 default | | `list_resources(entity?)` | RESOURCE only | | `search_archive(entity?, query, since?, until?)` | ARCHIVE only, time-travel + full-text | | `create_workspace(paraLayer, name, entity, target_close_date?)` | 신규 (4 paraLayer 어디든) | | `close_workspace(id, dispatch_plan?)` | paraLayer 변경 1회 + 동시 dispatch 가능 | | `reclassify_workspace(id, new_paraLayer)` | bidirectional reclassify (ARCHIVE resurrect 포함) | ### Artifact level tools | Tool | 역할 | |---|---| | `get_artifact(id)` | 단일 artifact + citations + audit_trail | | `list_artifacts(workspaceId, schema_id?, paraLayer?)` | filter by workspace / schema / paraLayer | | `propose_artifact(workspaceId, schema_id, content, citations, confidence?)` | agent propose (audit_trail event: "propose") | | `dispatch_artifact(artifact_id, to_workspace_id, mode="copy" \| "link")` | copy or link | | `unlink_artifact(artifact_id, workspace_id)` | link 해제 (copy 는 별 artifact 가 됐기 때문 미적용) | ### Knowledge query | Tool | 역할 | |---|---| | `query_knowledge(query, paraLayers?, entity?, since?)` | KnowQL-lite — cross-PARA / time-travel / confidence filter. agent 가 grep 안 함 | 이유: `list_workspaces(paraLayer=PROJECT)` 같은 generic tool 보다 `list_projects` 가 LLM 의 intent 명확. plan 단계 토큰 절감 + 잘못된 paraLayer 호출 가능성 차단. ## LLM 호출 + Access 정책 artifact 의 두 가지 access pattern 결정: ### Layer 1: LLM 호출 = Max plan OAuth 우선 Artifact extraction / proposing / review 의 LLM 호출은 **Claude Code OAuth token (Anthropic Max plan) 우선**. Anthropic API direct (per-token billing) 은 fallback / 대량 batch 용. | Surface | 호출 방식 | |---|---| | Teams bot | Claude Code OAuth (Max plan session) | | 로컬 Claude Code CLI | OAuth (operator session) | | 웹 UI (review queue, ctx review) | OAuth (Max plan, 사용자 본인 session) | | 대량 batch (한 번에 1000+ artifact extraction) | Anthropic API direct fallback | | vision-heavy ingest (pdf/pptx 대량) | Anthropic API direct ([기존 패턴 `vision_ingest.py`](/services/blueprint)) | **비용 측면** — per-token incremental cost 0 (Max plan 안 무제한). 대량 batch / vision-heavy 만 per-token billing 노출. **기존 패턴 정합** — `vision_ingest.py` 의 API direct 호출은 대량 vision 처리 한정. 같은 원칙을 artifact extraction 으로 확장. ### Layer 2: Access = MCP tool 위주, REST 최소화 Artifact access 의 primary layer 는 **Blueprint MCP tool** (위의 `query_knowledge` / `get_artifact` / `dispatch_artifact` 등). 별도 REST endpoint proliferation 회피. | Surface | Access | |---|---| | Claude Code (CLI / Teams bot / 웹 agent) | Blueprint MCP tool | | 외부 agent (third-party MCP client) | Blueprint MCP tool (OAuth-RP) | | 웹 UI (Blueprint Next.js) | 최소 REST endpoint (curation UX 용) | | Cross-service backend (frame ↔ Blueprint event consumer) | Postgres direct (격리 X — 같은 instance) | 이유: MCP tool surface 한 곳으로 통합 = audit 한 곳, OAuth-RP 한 번 (D-bp-mcp-1), 권한 모델 한 번 (D-bp-entity-17 EntityRole). REST endpoint 가 늘면 권한 모델 중복 + audit silo + version drift. ## 저장 결정 + 대안 비교 **Primary storage = Blueprint Postgres 16** (D-config-16 cutover 완료). 신규 인프라 0. per-customer macmini isolation (D1) 안에서 Blueprint Postgres 동거. Monolith first — 별도 `KnowledgeStore` service 분리 안 함. Blueprint 가 이미 PARA OS home, surface 분리는 추출 필요 시 나중. | 대안 | 거절 사유 | |---|---| | **Graph DB (Neo4j)** | citation chain 이 graph 형태이긴 하나 query workload 가 hash-lookup + filter 위주. Postgres JSONB GIN index 로 충분. 신규 dependency 추가만 부담 | | **Vector DB (Qdrant) primary** | 의미 검색은 필요 시 pgvector extension 으로. primary 로 두면 trans actional consistency / FK constraint / audit trail 다 잃음 | | **ClickHouse** | analytical query OLAP 강점은 있으나 artifact 는 row count < 100k 수준의 transactional/curation workload. column-store 이점 X | | **MongoDB** | JSONB 의 동등 기능 + sharding 미필요 + frame/hive 와 Postgres 일관성. 별 DB 추가 부담 | | **SQLite (file-per-workspace)** | concurrent read-write 모델 (multi-agent) 미적합. Postgres advisory lock 으로 자연 | ## 확장 (필요 시 add-on) 기본 Postgres 위에 Phase 별 add-on 가능. 모두 신규 인프라 없이 extension install 수준. | 확장 | 활성화 조건 | |---|---| | `pgvector` | semantic search (artifact content / citation text embedding) 필요 시. Stage 2 의 cross-PARA query 가 정확도 부족하면 도입 | | `pg_jsonschema` | content + audit_trail JSONB validation 강제 시. schema_id 별로 JSON Schema 등록 → INSERT/UPDATE 시 reject | | `temporal_tables` (또는 `timestamptz` column + soft archive) | time-travel query 정확도 필요 시. Stage 1 까지는 `updated_at` + `archived_at` 으로 충분 | ## 단계 (활성화 조건 충족 시 — 활성화 자체는 다음 결정) [M3 PARA Dispatch](/ops/roadmap#m3--blueprint-para-dispatch-d-bp-entity-1-pr-5) land + [M1 multi-tenant](/ops/roadmap#m1--stage-0--1-외부-출시) 종료 가 자연스러운 entry. 단 평행 진행 가능 — 외부 surface 영향 0. | Stage | 산출 | 기간 | |---|---|---| | **0. PoC** | filesystem-only 첫 schema (`ICMemoArtifact §재무`) — DB 변화 0. ctx 의 markdown diff UX 위에 typed extraction proof | ~3일 | | **1. DB 진입** | `Artifact` + `Citation` Prisma migration, KnowQL-lite HTTP endpoint, Blueprint UI 탭 1개 | ~3-4주 | | **2. Full integration** | 5종 schema (IC / Board / LP / Sector / KPI) active, PARA dispatch field-level engine, ctx 진화 (curation mode) | ~3-4주 | ## Open implementation decisions 본 페이지의 design 위에 implementation 단계에서 결정할 항목들. 각 항목마다 framework (방향성) 만 명시, 구체 결정은 deferred. **Implementation 단계에서 각 항목 결정 시 D-bp-artifact-N 추가 등재**. | # | 항목 | Framework (방향성) | |---|---|---| | **B1** | Agent ↔ artifact API contract | Agent (Claude / Teams bot) 의 artifact propose 권한은 implementation 시 [D-bp-entity-17](/ops/decisions) EntityRole 위에 layer 추가 검토. propose 는 ctx review queue 경유 default — 직접 confirmed 진입은 별 권한 grant 필요 | | **B2** | Schema evolution 호환성 | Schema versioning: MCP source schema 변경 = MCP 의 도메인 정책 따름 (frame/hive 각자 backward compat 책임). free-form artifact 는 `schema_id` 마다 append-only 보존 (`freeform.llm@auto-2026-05` 같은 timestamped variant). forced migration 도구는 implementation 시 결정 | | **B3** | Concurrent write 정책 | optimistic lock (`updated_at` version check) + audit_trail conflict event. detail TBD — 첫 multi-agent concurrent write 발견 시 결정 | | **B4** | Time-travel 정확도 | Stage 1 = `updated_at` filter (근사). Stage 2 = audit_trail event replay. snapshot table 신설 여부는 정확도 정당화 시점에 결정 (e.g., IRR 결정론 검증이 시점 정확도 요구할 때) | | **B5** | Failure modes (degradation) | graceful degradation 원칙: (a) LLM extraction 실패 → markdown fallback (raw content + warning); (b) Citation resolver 실패 → cached value + warning chip; (c) Dispatch partial fail → savepoint rollback (all-or-nothing). detail TBD | | **C1** | Cross-customer aggregation | per-customer macmini isolation ([D1](/ops/decisions)) 의도된 격리 유지. meta-layer aggregation (예: AXE 가 운영하는 N customer 의 PARA flow 집계) 미설계. 외부 GP 명시 요구 시 재검토 | | **C2** | axec PARA 구조 | axec / axev / sys entity 각각 자체 PARA tree. axec Area 정의는 axec workflow 진행 시 자연 emerge (현재 axec workflow 비중 낮음 — axev 가 primary) | | **C3** | Resource 외부 import | 외부 framework (e.g., Atomico VC playbook, McKinsey portfolio template) 의 RESOURCE 등재는 implementation 시 IP / license 정책 합의 후 결정. v1 = AXE 내부 사용만 | | **C4** | Multi-language artifact | v1 = 한국어 우선 (모든 schema 정의 + UI 한국어). content 안 영문 field 가 자연 등장 시 (예: 외국인 employee record, 영문 LP comm) schema 별 multi-lang 결정 | 각 항목은 implementation PR 시점에 D-bp-artifact-N 으로 등재 + 본 표 update. framework 만으로 시작 가능 (구체 결정 deferred 가 작업 정체 trigger 아님). ## 관련 결정 - [D-bp-artifact-1~7](/ops/decisions) (본 페이지 등재 동반, 2026-05-23) - [D-bp-entity-1](/ops/decisions) (Blueprint entity 개념 + PARA 4 layer schema) - [D-bp-entity-2](/ops/decisions) (PARA dispatch workspace-level provenance — 본 페이지가 field-level 진화) - [D-bp-entity-17](/ops/decisions) (EntityRole — propose/review 권한 layer 의 base) - [D-config-16](/ops/decisions) (Blueprint Postgres — artifact 거주지) ## 관련 페이지 - [/services/blueprint](/services/blueprint) — Blueprint 서비스 페이지의 artifact 섹션 - [/ops/roadmap M3, M6](/ops/roadmap) — PARA dispatch + artifact layer - [/architecture/data](/architecture/data) — 데이터 격리 모델 (entityId scope) - [/ops/backlog](/ops/backlog) — M6 태그 항목들 --- # 인증 · 권한 > Microsoft Entra ID OAuth 2.0, frame JWT, dual-token model. URL: https://docs.axelabs.ai/architecture/auth # 인증 · 권한 ## 3 가지 인증 경로 | 경로 | 사용 주체 | 토큰 | 검증 | |---|---|---|---| | **A. Microsoft Entra ID OAuth** | 직원 (Claude Code / claude.ai) | access_token (RS256, Microsoft 서명) | frame middleware: JWKS RS256 + aud match | | **B. frame HS256 JWT** | 운영자 / 서비스 토큰 (cron job 등) | HS256 (FRAME_JWT_SECRET 으로 서명) | frame middleware: `audience=client_id` | | **C. dual-token** | Blueprint agent + per-user 위임 | (agent_jwt, user_jwt) | intersection of permissions | `iss` claim 으로 자동 dispatch — `https://login.microsoftonline.com/...` 시작이면 A, 아니면 B. > **경로 D — Blueprint 플랫폼 토큰 (D-axe-idp-1, Phase 1+2 LIVE 2026-06-04)**: **Blueprint = 플랫폼 OIDC Provider** 가 4번째 신뢰 발행자로 합류 — 로그인 1회로 전 서비스. Blueprint 가 Entra 를 federate(기존 NextAuth 세션 재사용) + RS256 플랫폼 토큰 발행(`/oauth/*` + `/.well-known/{openid-configuration,jwks.json}` on `blueprint.axellc.com`). 각 서비스는 `iss=blueprint` 분기에서 Blueprint JWKS 로 RS256 검증(`aud=https://axe.axelabs.ai`), email→entity 는 기존 `customers.yaml` 그대로. **frame·hive·cortex·index·matrix + Blueprint 자체 MCP — 6개 서비스 전부 Blueprint 토큰 신뢰** (Python: `auth_blueprint.py` + `is_blueprint_iss` dispatch / Rust: `peek_iss`→`verify_rs256` vs Blueprint JWKS). 영속 설정(compose/`.env.local` 의 `BLUEPRINT_ISSUER` 기본값)·e2e 검증(`axe login` → `axe tools` GREEN). 비파괴 롤백 = 서비스 `BLUEPRINT_*` env 제거(unset=종전 Microsoft 경로). 설계·현행 SSOT: [/architecture/platform-identity](/architecture/platform-identity). ## 경로 A — Microsoft Entra ID (직원) 각 customer 의 Microsoft Entra ID tenant 에 **5 개의 app** 이 등록되어 있어야 합니다 (axe customer 2026-05-21 현재): | App 이름 | 용도 | 토큰 종류 | 비고 | |---|---|---|---| | Frame MCP | 회계 backend 접근 | access_token (aud=client_id, v2) | public client + PKCE (`b7ead15d-...` Hive 패턴) | | Hive MCP | HR backend 접근 | access_token (aud=client_id, v2) | public client + PKCE, `client_id=b7ead15d-2fea-4864-a5a8-b4b07d1629d4` | | Blueprint MCP | platform MCP at apex | access_token (aud=URI, v1) | public client + PKCE, `/api/mcp` apex | | Vaultwarden | password vault SSO | OIDC id_token | confidential, secret 보관 | | Blueprint Web | web UI 로그인 | OIDC id_token | NextAuth | 자동 등록: `az ad app create` + `az rest --method PATCH /applications/APP_ID` (identifierUris + requestedAccessTokenVersion=2 + oauth2PermissionScopes.value="mcp.access"). 운영자 손작업 = `axe.axelabs.ai` 도메인 검증 1회만. ### Frame MCP app 설정 (현재 axe live) ```yaml client_id: 137fc0ef-eb9f-4903-acbc-1a748add349c application_id_uri: https://axe.axelabs.ai/frame/mcp # 도메인 검증 필요 tenant_id: 122fb574-7efa-476a-95b6-bee81bce2cce platform: Web redirect_uris: - https://claude.ai/api/mcp/auth_callback - https://claude.com/api/mcp/auth_callback scopes: - openid - profile - email - https://axe.axelabs.ai/frame/mcp/mcp.access allow_public_client_flows: true # PKCE 활성 (manifest `isFallbackPublicClient: true`) # client_secret 발급 여부: # - 직접-Microsoft path (현재 live): 발급 X (PKCE-only, claude.ai 가 secret 안 보냄) # - OAuth proxy path (D-ops-15 dormant): 발급 + frame .env 에 보관 # access_token_version (manifest `requestedAccessTokenVersion`): # - null/1 → access_token aud = Application ID URI (https://...) # - 2 → access_token aud = client_id GUID # frame middleware 는 양쪽 모두 수용 (audiences=[client_id, app_id_uri]). ``` ### Azure App ID URI Microsoft v2 endpoint 의 **resource indicator** 와 **scope URI prefix** 가 정확히 일치해야 Microsoft 가 token 발급. 발견된 함정: - **AADSTS9010010**: scope=`api://137fc0ef-.../mcp.access` + resource=`https://axe.axelabs.ai/frame/mcp` → mismatch. - 해결: domain verify (`axe.axelabs.ai` 에 TXT `MS=ms...` 등록) → Application ID URI 를 `https://axe.axelabs.ai/frame/mcp` (URL 형식) 로 변경 → scope 도 같은 prefix. ## frame OAuth-RP middleware frame 서버의 `auth_oidc.py` 가 다음을 수행: 1. `Authorization: Bearer <access_token>` 헤더 추출 2. `iss` claim peek (서명 검증 전 unverified decode) → Microsoft 시작이면 A 경로 3. Microsoft JWKS fetch (1h TTL, kid miss 시 강제 갱신) 4. RS256 서명 검증 5. **audience 검증**: token `aud` 가 `{client_id GUID, Application ID URI}` 중 하나와 일치 (v1/v2 호환) 6. issuer 검증: `https://login.microsoftonline.com/{tenant_id}/v2.0` 7. `email` / `preferred_username` / `upn` 중 첫 비어 있지 않은 값 → email 8. `customers.yaml > user_entity_map[email]` → entity 권한 dict 매핑 9. `TokenClaims` 생성 → ContextVar 주입 → tool 호출에 사용 ### RFC 9728 protected-resource metadata path claude.ai Connector 와 다른 MCP client 가 OAuth challenge 시작 시 fetch 하는 metadata endpoint 는 **두 path** 에서 동일한 응답: | Path | 용도 | |---|---| | `/frame/.well-known/oauth-protected-resource` | server-level (RFC 8615 base URL convention) | | `/frame/mcp/.well-known/oauth-protected-resource` | resource-level (RFC 9728 — application_id_uri 의 sub-path). claude.ai Connector / Anthropic MCP client 가 이 path 를 fetch | 양쪽 모두 `_PUBLIC_PATHS` 로 unauthenticated 면제 + `inner.router` 에 같은 handler 마운트. 401 응답의 `WWW-Authenticate: Bearer realm="frame", resource_metadata="..."` 가 가리키는 URL 도 양쪽 모두 valid (D-ops-23, 2026-05-22 — 이전엔 resource-level path 401 → claude.ai Reconnect silently 실패). ### Schema discovery — auth-required `/frame/schemas` (그리고 hive 의 `/hive/schemas`) 는 RFC 9728 metadata 와 달리 **auth-required** — `_PUBLIC_PATHS` 미포함. JWTAuthMiddleware 가 일반 MCP 호출과 동일 검증 적용. Blueprint artifact + PARA 지식 레이어 ([D-bp-artifact-1](/ops/decisions)) 가 fetch 시 자기 MCP 토큰 재사용. 자세한 endpoint contract = [services/frame § Schema discovery](/services/frame#schema-discovery--get-frameschemas). ## 권한 모델 — Scope ``` read < write < close < admin ``` - `read` — query_balance, list_journals - `write` — post_journal, flag_uncertainty - `close` — soft_close period, hard_close (운영자) - `reopen` — 폐쇄된 period 재오픈 (드물게) - `admin` — chart of accounts 변경, policy 마이그레이션 권한은 **entity 별로** 부여 (예: `{"axec": ["read", "write"], "axev": ["read"]}`). ## dual-token (CFO + accountant 패턴) CFO 에이전트가 회계사 A 를 대신해 활동할 때: ``` Authorization: Bearer <CFO agent JWT> ← 광범위한 권한 X-User-Token: <accountant A JWT> ← axec 만, read+write ``` frame 의 `dual_authorize(agent, user, entity, scope)` 가 두 토큰의 권한 교집합으로 게이팅: ```python effective_scopes = agent.permissions["axec"] ∩ user.permissions["axec"] ``` 최소 권한 원칙 강제. agent 가 admin 이어도 user 가 read-only 면 read 만 가능. ## 운영자 자체 토큰 운영자 (`ai@axellc.com`) 가 CLI 에서 직접 호출 시: ```bash docker exec frame-mcp-blue python -m frame.cli mcp-token \ --sub ai@axellc.com \ --customer axe \ --entity axec:read,write,close \ --entity axev:read,write,close \ --ttl 2592000 # 30 days ``` → HS256 frame JWT 발급. 경로 B 로 검증. 운영자 토큰은 **Vaultwarden 의 `axe-frame-jwt` collection** 에 보관 (entity 별 vault). ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | Application ID URI 와 scope prefix 불일치 | AADSTS9010010 | 두 값 같은 URL prefix | | Allow public client flows: No + secret 없음 | AADSTS7000218 | Yes 토글 OR secret 입력 | | accessTokenAcceptedVersion=2 + aud check on URL | mcp_client_invalid | **v2 강제 + middleware multi-issuer 양쪽 모두**: (1) Azure app 등록 시 `requestedAccessTokenVersion=2` 강제 (bootstrap.sh manifest PATCH 시점), (2) middleware 가 v1+v2 issuer 양쪽 수용 (defense in depth — v1 token 이 어떤 사유로 들어와도 graceful 처리). 단독 (1) = bootstrap PATCH 누락 시 사고. 단독 (2) = client 측 v1 token 발급 자유 잔존. | | App ID URI 삭제 (cleanup 잘못) | AADSTS500011 resource principal not found | 복원 후 propagation 5-10분 대기 | | 잘못된 client_id 입력 (vaultwarden vs frame_mcp) | AADSTS700016 application not found | customers.yaml 확인 | | `az ad app create` 후 sign-in 시도 → 즉시 실패 | Entra trace ID + "Authorization with the MCP server failed". `az ad app create` 는 **application object 만 생성, Service Principal 자동 생성 X** | `az ad sp create --id ` 별도 호출 | | SP 있는데 sign-in 시 같은 메시지 | Service Principal 존재해도 `requiredResourceAccess` 비어 있고 admin consent 안 받음 | manifest 에 `mcp.access` (self) + Graph `User.Read` (e1fe6dd8-ba31-4d61-89e7-88639da4683d) `requiredResourceAccess` 추가 후 admin consent grant | | `az ad app permission admin-consent` → 403 Forbidden | 명령 실행 user 가 tenant admin role 없음 ("This operation can only be performed by an administrator") | (a) Portal UI: App registrations → 해당 app → API permissions → "Grant admin consent for {`{tenant}`}" 버튼, (b) admin user 로 `az login` 후 명령 재실행 | | `az ad app credential reset --append` parsing 시 stderr 섞임 | JSON 출력에 progress 메시지 혼합 → `python3 -c "json.load"` 실패 | `--query 'password' -o tsv` 로 단일 필드 추출 | | Vaultwarden SSO with Microsoft Entra ID → "Your provider does not send email verification status" | Entra ID id_token 에 `email_verified` claim 없음, Timshel fork 가 거부 | `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"` 추가 (D-ops-24, 2026-05-22) | | Blueprint OAuth scope 추가 (`SCOPES` in `src/lib/graph.ts`) ship 후 admin consent grant 누락 | (1) 모든 user 의 GraphToken refresh 침묵 실패 — UI 상 "Connected" 표시 유지하나 실 호출 시 401/403, (2) 새 user OAuth flow 차단 `AADSTS65001 Consent_VersionMismatch`, (3) `acquireTokenByClientCredential` 캐시된 옛 토큰 (insufficient-scope) 계속 반환 | (1) ship 직후 `az ad app permission admin-consent --id ` 실행, (2) `docker restart blueprint-app-green` (또는 -blue) — MSAL in-memory 캐시 폐기, (3) 자동화 후보: `axe ship blueprint` post-deploy hook + Blueprint daily token health check ([B-blueprint-scope-change-admin-consent-runbook](/ops/backlog) 참조). 절차 상세 = 아래 [OAuth scope 추가 시 운영자 절차](#oauth-scope-추가-시-운영자-절차) | | Frame/Hive MCP middleware 의 `_MICROSOFT_ISS_PREFIX` 가 v2 issuer prefix (`https://login.microsoftonline.com/`) 만 매치 (trap #33, Truvia 2026-05-25 보고) | Microsoft v1 access_token (iss=`https://sts.windows.net//`) 보내면 middleware 가 Microsoft 경로 미인식 → HS256 fallback path 진입 → `algorithms=["RS256"]` allowlist 가 RS256 token 을 잘못된 path 에서 reject → `The specified alg value is not allowed` | (1) Azure app 등록 시 `requestedAccessTokenVersion=2` 강제 (bootstrap.sh + 등록 후 manifest 검증 — Truvia 우회 = 3 MCP app 모두 manifest PATCH), (2) middleware 가 v1+v2 issuer 양쪽 수용 (Blueprint MCP 의 `config.py:76,81` + `auth_oidc.py:153` 패턴 mirror). 위치: `/Users/axe/frame/src/frame/mcp/http_server.py:141` + `/Users/axe/hive/src/hive/mcp/http_server.py:67`. 영구 fix = [B-trap-33-frame-hive-multi-issuer](/ops/backlog) (code portion 잔존, docs portion ✅ 2026-05-28) | 상세 troubleshooting: [/onboard/troubleshooting](/onboard/troubleshooting). ## OAuth scope 추가 시 운영자 절차 Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 의 `SCOPES` array (`src/lib/graph.ts`) 변경 ship 시 다음 순서를 그대로 따를 것. 누락 시 위 함정 표 마지막 row 의 3 가지 증상 발현 (2026-05-26 D-bp-mcp-calendar-1 ship 후 발현 사례 있음). ```bash # 1. SCOPES array 변경 commit + ship cd /Users/axe/blueprint && axe ship # 2. admin consent grant (운영자 = Global Admin 만 가능, soohun.kang) az login --allow-no-subscriptions az ad app permission admin-consent --id # (client_id = Blueprint Next.js app, 예: 2b222356-1c36-48e0-96a3-2c5e0ecbf937) # 3. blueprint-app 재시작 (MSAL in-memory 캐시 폐기) ssh axe-macmini "cd /Users/axe/blueprint && docker compose restart blueprint-app-blue blueprint-app-green" # 4. 검증 (token health) # - /api/admin/graph-tokens (또는 /settings) 에서 각 user 의 expiresAt 갱신 확인 # - 본인 계정으로 Graph API 호출 1회 (e.g. /api/admin/broadcast-dm test) # 5. user 측 reconnect 안내 (Teams DM) # - admin consent 후에도 옛 refresh token 의 grant 가 새 scope 안 가짐 # - 각 user 가 /api/graph/auth 재방문 → 새 scope 포함 consent → 새 refresh token 발급 ``` 본 절차 가운데 step 3 (MSAL in-memory cache 폐기) 의 근본 원인은 [/ops/known-gaps#msal-acquiretokenbyclientcredential-토큰-캐시](/ops/known-gaps) 참조. Blueprint 의 Azure App 2 개 (Next.js app `2b222356-...` vs MCP custom connector `482598f7-540c-462c-9dfd-b957651eb804`) 구분도 같은 페이지에 있음 — 본 SCOPES 변경 및 admin consent 는 **Next.js app 측에만** 적용. ## app-only Application permission (send-as) 위 [OAuth scope 추가 시 운영자 절차](#oauth-scope-추가-시-운영자-절차) 는 **delegated** scope (`SCOPES` in `src/lib/graph.ts`, caller 본인 토큰) 변경용이다. 그와 별개로, **admin send-as** 기능 (caller `role=admin` 이 타 사용자 리소스에 write) 은 app-only token (`getAppOnlyClient()` = `acquireTokenByClientCredential`) 을 쓰며, **Application permission** + tenant admin consent 를 요구한다. delegated 와 달리 사용자별 re-consent 가 아니라 테넌트 1회 admin consent 다. 대상 App = **Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`)** — MCP connector app (`482598f7-...`) 이 아니다 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)). | 기능 | Application permission | delegated 대응 (이미 SCOPES) | 결정 | |---|---|---|---| | calendar send-as (`create_event` 등 `as_user_email`) | `Calendars.ReadWrite` | `Calendars.ReadWrite` | D-bp-mcp-calendar-2 | | `send_mail` send-as (`as_user_email`) | `Mail.Send` | `Mail.Send` (→ self 발송은 추가 consent 불필요) | D-bp-mcp-mail-1 | ```bash # 1. permission 추가: Azure Portal → App registrations → Blueprint (Next.js) # → API permissions → Add → Microsoft Graph → Application permissions # → Mail.Send (well-known roleId b633e1c5-b582-4048-a93e-9f11b44c7e96) # 2. tenant admin consent (Global Admin 계정 — AXE 테넌트는 soohun.kang 만) az ad app permission admin-consent --id 2b222356-1c36-48e0-96a3-2c5e0ecbf937 # 3. MSAL client-credential in-memory 캐시 폐기 (옛 insufficient-scope 토큰 재사용 방지) docker restart blueprint-app-blue # 또는 -green ``` consent 누락 시 send-as 호출은 Graph 403 → 코드가 `mail_send_not_consented` (mail) / `app_only_token_unavailable` (calendar) 로 surface. self (delegated) 경로는 본 permission 없이도 동작하므로 **send-as 만** 영향. --- # @axe/ui 디자인 시스템 > AXE Labs 디자인 토큰 + 컴포넌트 라이브러리를 sync 스크립트 방식으로 소비. light/dark 토글, 5분 셋업, dogfood = axelabs.ai. URL: https://docs.axelabs.ai/architecture/axe-ui # @axe/ui 디자인 시스템 AXE Labs 의 단일 디자인 시스템 (`@axe/ui`). 색·간격·타이포·폰트 토큰 + 컴포넌트 클래스를 모든 React 소비자 (Blueprint · frame admin · hive admin · docs.axelabs.ai · axelabs.ai 회사 홈) 가 공유. npm registry 없이 **git+ssh 또는 sync 스크립트** 로 전파 ([D-axe-ui-1](/ops/decisions)). > **본 페이지는 새 프로젝트에 적용하는 절차서**. 운영 중 변경 흐름은 [/ops/backlog](/ops/backlog) + [/ops/updates](/ops/updates). ## SSOT 경로 `/Users/axe/axelabs/` (github: `soohunkang/axelabs`) — 디자인 시스템 본가: ``` src/lib/ ├── tokens/ colors · spacing · typography · fonts · index └── styles/ reset · components ``` Canonical sync 스크립트: `/Users/axe/axelabs-docs/scripts/sync-axe-ui.mjs` — 그대로 복사 가능. ## 적용 절차 (Next.js 기준 5분) ### 1. sync 스크립트 복사 ```bash cp /Users/axe/axelabs-docs/scripts/sync-axe-ui.mjs PROJECT/scripts/ ``` 스크립트 안의 `axeLabsRoot` 가 `join(repoRoot, "..", "axelabs")` 로 잡혀 있음. 새 프로젝트가 `axelabs/` 와 같은 부모 디렉토리 (`/Users/axe/`) 에 있으면 그대로 OK. 다르면 그 한 줄만 수정. ### 2. `package.json` 의 `scripts` 에 4 줄 추가 ```json "sync-axe-ui": "node scripts/sync-axe-ui.mjs", "check-axe-ui": "node scripts/sync-axe-ui.mjs && git --no-pager diff --stat -- app/_axe-ui/ ':!app/_axe-ui/VERSION' && git checkout -- app/_axe-ui/", "predev": "node scripts/sync-axe-ui.mjs", "prebuild": "node scripts/sync-axe-ui.mjs" ``` ### 3. 최초 sync 실행 ```bash npm run sync-axe-ui # → app/_axe-ui/{tokens,styles,VERSION} 생성됨 ``` ### 4. `app/globals.css` 첫 부분에 import Nextra/theme CSS **뒤에** 와야 override 됨: ```css @import "./_axe-ui/tokens/colors.css"; @import "./_axe-ui/tokens/spacing.css"; @import "./_axe-ui/tokens/typography.css"; @import "./_axe-ui/tokens/fonts.css"; ``` ### 5. (선택) 컴포넌트 클래스도 사용 ```css @import "./_axe-ui/styles/reset.css"; @import "./_axe-ui/styles/components.css"; ``` ### 6. `app/_axe-ui/` 전체를 git commit Docker/CI 빌드 안에서는 `axelabs/` 경로가 없음 — committed 산출물로 동작 (스크립트가 graceful skip). ## 테마 토글 — class · attribute 둘 다 지원 axelabs HEAD `ff6eb27` (2026-05-22) 이후 두 전략 모두 자동 매핑. ### A) class 전략 — next-themes 기본값 HTML 의 root element 에 `class="light"` 또는 `class="dark"`. Nextra docs 같이 next-themes 쓰면 추가 설정 불필요. ### B) attribute 전략 — root element 의 `data-theme` 속성 직접 토글 시: ```ts useEffect(() => { document.documentElement.setAttribute("data-theme", mode); }, [mode]); ``` 둘 중 어느 쪽이든 토큰 정확히 매핑. OS `prefers-color-scheme` 도 명시적 light 선택을 덮어쓰지 않음. ## 검증 | 모드 | body bg | accent | |---|---|---| | light | `#f5f1e8` (cream) | `#821f3b` (claret) | | dark | `#1a0610` (noir) | `#e3ff66` (neon) | 토글 시 즉시 반영. OS=dark + 사용자 "light" 선택해도 dark 로 안 어긋남. ## 최신 버전 체크 — 개발 중 SSOT 변경 감지 ```bash # 현재 sync 상태 cat app/_axe-ui/VERSION # @axe/ui 0.1.0 # synced-at: 2026-05-22T... # source: /Users/axe/axelabs # source-sha: ff6eb27 ← axelabs HEAD # source-lib-sha: ff6eb27 ← src/lib 마지막 변경 SHA # source-lib-date: 2026-05-22 10:41:53 +0900 # 받을 변경 있나 (dry-run, 파일 안 건드림) npm run check-axe-ui # 빈 출력 = 최신 # "N files changed" = 받을 변경 있음 # axelabs 측에서 놓친 commit 목록 git -C /Users/axe/axelabs log SOURCE-LIB-SHA..HEAD -- src/lib/ # 실제로 받기 npm run sync-axe-ui && git add app/_axe-ui/ && git diff --staged # 검토 후 commit: "sync(ui): @axe/ui bump to SHA" # axe ship SERVICE ``` **자동**: `npm run dev` / `npm run build` 시 `predev`/`prebuild` hook 이 매번 sync 돌림. diff 가 생기면 그때 commit + ship 결정. ## 적용된 소비자 | 프로젝트 | 전략 | 비고 | |---|---|---| | `/Users/axe/axelabs-docs` | class | Nextra docs (본 페이지 포함) | | `/Users/axe/axelabs/app/ui` | attribute | axelabs 메인 (디자인 시스템 자기 dogfood) | | Blueprint | _예정_ | [B-axe-ui-blueprint-migrate](/ops/backlog) | | frame admin / hive admin | _예정_ | 외부 customer rollout 전 | ## 관련 결정 - [D-axe-ui-1](/ops/decisions) — `@axe/ui` 단일 배포 채널 = git+ssh + axelabs.ai dogfood (Verdaccio / GitHub Packages / npm publish 모두 미사용 — 소비자 ≤3개 시점에서 부담만) - [B-axelabs-ai-live](/ops/backlog) — axelabs.ai 도메인 라이브 배포 (production tag 핀 가능 시점 prereq) - [B-axe-ui-v0.1-tag](/ops/backlog) — `v0.1.0` git tag (외부 소비자 stable 버전 시작점) ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | sync 스크립트가 axelabs 경로 못 찾음 (Docker/CI) | sync graceful skip, committed `app/_axe-ui/` 사용 | 본 디렉토리 항상 commit + Dockerfile 의 `COPY . .` 에 포함 | | Nextra theme CSS 가 토큰 override | 색 안 적용 | `@import` 순서 — `globals.css` 의 첫 부분 | | Fontshare `@import` 의 `f[]=` drop (Blueprint 사례) | font 로드 실패 | `` 방식 + Clash Display 패턴 일치 | | OS `prefers-color-scheme` 이 사용자 선택 override | 토글 후 OS 따라 깜빡 | axelabs `ff6eb27` 이후 자동 회피 (사용자 명시 우선) | --- # 백업 · DR > restic 3-tier (local · ring P2P · cold SSD), restore drill. URL: https://docs.axelabs.ai/architecture/backup # 백업 · DR ## 3-tier 전략 ``` Tier A — 로컬 (실시간 보호) restic repo /Users/axe/.axe/backups/local/ 매일 03:00 KST, com.axe.backup.local Tier B — P2P ring (cross-customer 양방향) axe-macmini ↔ realchoice-macmini (SSH key, Tailscale) 매일 03:30 KST, com.axe.ring.push Tier C — Cold SSD (offline 보호) 외장 SSD rotation /Volumes/axe-cold-{1,2,3} mount 시 자동 sync ``` ## Tier A — 로컬 (live) | 항목 | 값 | |---|---| | Repo path | `/Users/axe/.axe/backups/local/` | | Restic version | 0.18.1+ | | Password | macOS Keychain (`axe.backup.restic.local`) | | 백업 대상 | frame-postgres dump + **blueprint-postgres dump** (D-config-17 cutover 후, 2026-05-15~) + **hive-postgres dump** (D-hive-backup, 2026-05-21~ — Phase 1 조직/휴가 + Phase 3 payroll v2 실데이터) + **mysrt-postgres dump** (B-mysrt-backup-decision, 2026-06-06~ — users/jobs/push_subscriptions; SRT는 train/예약의 SoT일 뿐 계정·잡설정 SoT 아님) + **index-postgres dump** (D-index-51, 2026-06-06~ — `evidence_blob` = 죽은 딜 OneDrive/Blueprint 원본 삭제 후 **유일 사본**, 312MB bytea → ~633MB `pg_dumpall`, restic content-dedup 으로 ~274MB stored) + `.local/files/` (platform data) | | 빈도 | 매일 03:00 KST (`com.axe.backup.local` launchd) | | 현재 크기 | ~429 MiB raw-data (2026-06-06 — index-postgres 흡수 후, restic dedup 적용) | | Excludes | `/Users/axe/.axe/bin/restic-excludes` | ### 명령어 ```bash # 수동 백업 axe backup --local # 백업 상태 확인 restic -r /Users/axe/.axe/backups/local snapshots \ --password-file <(security find-generic-password -w -s axe.backup.restic.local) # 복원 (dry-run) axe restore --customer axe --from local --as-of 2026-05-20 --dry-run ``` ## Tier B — P2P ring axe ↔ realchoice 양방향 백업 (각자 다른 macmini 의 백업을 보관). ``` axe-macmini → realchoice-macmini 의 /Users/realchoice/peer-backups/axe/ realchoice-macmini → axe-macmini 의 /Users/axe/.axe/backups/peer/realchoice/ ``` | 항목 | 값 | |---|---| | 전송 | restic `sftp:` backend (SSH key) | | auth | SSH key (~/.ssh/id_ed25519, 비밀번호 없음) | | 빈도 | 매일 03:30 KST (`com.axe.ring.push` launchd) | | 검증 | bidirectional_ssh 2026-05-15 검증 완료 | `customers.yaml` 에 ring backup 메타 등록: ```yaml realchoice: ring_backup: ssh_user: "realchoice" ssh_fqdn: "realchoice-macmini.tail090015.ts.net" ssh_ip: "100.114.161.51" receive_dir_at_peer: "/Users/realchoice/peer-backups/axe/" receive_dir_at_self: "/Users/axe/.axe/backups/peer/realchoice/" restic_version: "0.18.1" disk_available_gib: 43 bidirectional_ssh: true ``` ### 함정 - **Tailscale alone 으로는 충분하지 않음** — SSH key 가 별도 필요. Tailscale 인증 없어도 작동. - **Cross-customer 백업 = 데이터 노출 risk** — 양측 운영자 모두 합의 + restic 암호화로 컨텐츠 보호. ## Tier C — Cold SSD (offline) 외장 SSD 를 분기별로 rotation. mount 가 감지되면 자동 sync. | 항목 | 값 | |---|---| | Mount path | `/Volumes/axe-cold-{1,2,3}` (rotation) | | Restic repo | `/Volumes/axe-cold-N/restic-repo/` (N = 1, 2, 3 — rotation index) | | Password | 종이 메모 (vault 안에 넣지 말 것 — closed-loop 방지) | | Rotation | 분기별 (Q1 = SSD #1, Q2 = SSD #2, ...) | | Drill | 분기마다 자동 restore drill (`com.axe.restore-drill`, Jan/Apr/Jul/Oct 15 03:00 KST) | ### 왜 종이? cold storage 의 password 를 self-host vault 에 넣으면 vault 가 사라졌을 때 cold storage 도 못 풉니다 (closed loop). 종이 + 운영자 머리 = 이중화. ## 복원 절차 ### 시나리오 1 — frame DB 손상 (오타 / migration 사고) ```bash # 1. 어제 03:00 KST 백업 복원 axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target frame-postgres # 2. frame 재시작 docker compose restart frame-mcp-blue frame-mcp-green # 3. 정합성 검사 docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec ``` ### 시나리오 1b — blueprint DB 손상 ```bash # 1. 어제 03:00 KST 백업 복원 axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target blueprint-postgres # 2. blueprint app + mcp 재시작 (PR #337 이후 blue/green pair) docker compose -f /Users/axe/blueprint/docker/docker-compose.yml restart app-green blueprint-mcp-blue blueprint-mcp-green # 3. 정합성 검사 (Prisma client round-trip) docker exec blueprint-app-green node -e "const {PrismaClient}=require('@prisma/client'); new PrismaClient().workspace.count().then(n=>console.log('workspaces:',n))" ``` ### 시나리오 1c — hive DB 손상 ```bash # 1. 어제 03:00 KST 백업 복원 axe restore --customer axe --tier local --as-of 2026-05-20T03:00 --target hive-postgres # 2. hive 재시작 (Phase 1 조직/휴가 + Phase 3 payroll v2) docker compose -f /Users/axe/hive/docker-compose.yml restart hive-postgres hive-mcp-blue hive-mcp-green # 3. 정합성 검사 (axec/axev 테넌트별 employee count round-trip) docker exec hive-postgres psql -U hive -d hive -c \ "SELECT 'axec' AS tenant, COUNT(*) FROM axec.employees UNION ALL SELECT 'axev', COUNT(*) FROM axev.employees;" ``` ### 시나리오 2 — macmini 자체 손실 (도난, 화재) ```bash # 1. 새 macmini 셋업 (Tailscale 설치, axe CLI install) # 2. ring peer 에서 받기 axe restore --customer axe --tier ring --from realchoice --as-of 2026-05-20 # 3. 또는 cold SSD 마운트 + restore axe restore --customer axe --tier cold --as-of 2026-05-20 ``` ### 시나리오 3 — 사일런트 corruption `frame integrity-check --entity axec` 가 detect 하면: ```bash # 가장 가까운 valid snapshot 으로 부분 복원 axe restore --customer axe --tier local --table journal_line --as-of <last-valid> ``` ## Restore drill (자동) 분기마다 자동으로: 1. 임시 staging container 에 어제 backup 복원 2. `frame integrity-check` 통과 여부 확인 3. 결과를 운영자 Slack 으로 보고 drill 자체가 실패하면 cold SSD rotation 시도 또는 ring peer 복원. ## 함정 모음 | 함정 | 결과 | 회피 | |---|---|---| | Vault 에 cold storage password 저장 | closed loop, vault 손실 시 cold 도 못 풂 | 종이 메모 | | restic password 분실 | 백업 영구 손실 | Keychain + 종이 이중화 | | backup excludes 누락 (`.local/files`) | 핵심 데이터 백업 안 됨 | restic-excludes 명시 | | `docker exec frame-postgres pg_dump` 만으로 신뢰 | container 죽으면 dump 못 함 | volume 자체 백업 병행 | | ring backup 단방향 | 한쪽 손실 시 복구 불가 | bidirectional_ssh 검증 필수 | | index-postgres 백업 누락 | `evidence_blob` = 죽은 딜 OneDrive/Blueprint 원본 삭제 후 **유일 사본** → 영구 소실 | `axe-backup` index 블록 (D-index-51) + `index export-evidence` round-trip drill | --- # 데이터 격리 모델 > schema-per-entity, customer 격리, audit_log, PII 암호화. URL: https://docs.axelabs.ai/architecture/data # 데이터 격리 모델 AXE Labs 는 **3-tier 데이터 격리**를 사용합니다: | Tier | 격리 단위 | 구현 | |---|---|---| | 1. OS-level | customer | macmini 1대 = customer 1개 | | 2. Database schema | entity (회사) | PostgreSQL schema-per-entity | | 3. Row-level (일부) | tenant_id | RLS (magnet 등에서) | ## Tier 1 — OS-level (customer) 각 customer 는 자기 macmini 위에서 frame · blueprint 등 동일 스택을 실행합니다. customer 간 cross-talk 없음. | customer | macmini | DB | schema 셋 | |---|---|---|---| | axe | `axe-macmini` | frame-postgres | shared + axec + axev + axtest | | realchoice | `realchoice-macmini` (예정) | (별도) | shared + realchoice | backup 도 macmini 별로 분리되며, customer 간 ring backup 은 **운영자 명시 승인 + 양방향 SSH 합의** 후에만. ## Tier 2 — Schema-per-entity (frame) frame 의 PostgreSQL 안에는: ``` frame DB ├── shared (cross-entity) │ ├── entity ← 등록된 entity 메타 (axec, axev, realchoice) │ ├── format_profile ← 파일 포맷 캐시 (vendor + column_signature) │ ├── account_template ← 표준 계정과목 (KSME) │ ├── idempotency_record ← 멱등 키 │ └── oauth_authorization_codes ← OAuth proxy (D-ops-15, 현재 dormant) │ ├── axec (개별 entity schema) │ ├── account, journal, journal_line │ ├── raw_transaction, bank_account, source_file │ ├── fiscal_period, open_item │ ├── evidence, audit_log │ └── ... (전체 16 개 migration) │ ├── axev (axec 와 동일 구조) │ └── ... │ └── axtest (테스트 전용) └── ... ``` **Cross-entity 쿼리 불가**: 각 MCP tool 호출이 `SET search_path TO {entity_id}, shared` 로 격리. 사용자가 `axec` 권한만 가지면 `axev` schema 접근 불가. ### Migration 패턴 Alembic 의 dual-env: | 디렉토리 | 적용 시점 | 대상 | |---|---|---| | `alembic/versions/` | `frame migrate` 시 1회 | `shared` schema | | `alembic/entity_versions/` | `frame register-entity` 시 + 매 migrate | 각 entity schema (axec, axev, ...) | `frame migrate` 실행 시: 1. `upgrade_shared()` → shared schema 마이그 2. 등록된 모든 entity 에 대해 `upgrade_entity(entity_id)` → entity schema 마이그 ## Tier 3 — Row-level (선택적) `magnet` 같이 multi-tenant 단일 schema 에서 RLS 사용: ```sql -- magnet sql/050_multi_tenant_baseline.sql ALTER TABLE campaign_metrics_daily ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON campaign_metrics_daily USING (tenant_id = current_setting('app.tenant_id')::bigint); ``` DB 연결 단계에서 `SET app.tenant_id = <id>` 주입. 사용자가 잘못된 tenant_id 로는 read/write 불가. frame 은 schema-per-entity 가 더 강한 격리 (cross-schema 권한 자체 분리), magnet 은 RLS 가 멀티 brand 통합 운영에 더 적합 → 서비스 별 적절한 격리 모델 선택. ## audit_log — 모든 쓰기 추적 각 entity schema 에 `audit_log` 테이블이 있으며, **모든 쓰기 (insert/update/delete)** 가 자동 기록됩니다. ```sql -- entity-side audit_log 의 컬럼 audit_log ( id bigserial, table_name text, op text, -- INSERT | UPDATE | DELETE old_data jsonb, new_data jsonb, actor text, -- frame.actor session var entity_context text, -- 'axec' ts timestamptz default now() ) ``` `frame.actor` 는 entity_session context manager 에서 설정되며, MCP tool 호출의 `TokenClaims.sub` 가 자동으로 들어갑니다. ### Append-only 강제 `raw_transaction`, `journal_line` 같은 핵심 테이블에는 DELETE/UPDATE 권한이 DB 사용자 단에서 REVOKE 되어 있습니다. 잘못된 분개는 `reverse_journal` 로 역분 (새 journal 생성), 절대 수정/삭제 안 함. ## PII 암호화 (pgcrypto) 개인정보 (security_holder, audit log 의 일부 raw_data) 는 `pgcrypto.pgp_sym_encrypt` 로 entity 별 별도 passphrase 로 암호화됩니다. ```yaml # docker-compose.yml env FRAME_PII_PASSPHRASE_AXEC: ${FRAME_PII_PASSPHRASE_AXEC:-} FRAME_PII_PASSPHRASE_AXEV: ${FRAME_PII_PASSPHRASE_AXEV:-} FRAME_PII_PASSPHRASE_REALCHOICE: ${FRAME_PII_PASSPHRASE_REALCHOICE:-} FRAME_PII_SALT_DIR: /root/.frame ``` 각 entity 의 passphrase 는 운영자 Keychain (`security add-generic-password`) 에 저장되고 컨테이너 launch wrapper 가 env 로 주입. **passphrase 분실 = PII 영구 손실** (이는 의도된 설계 — 운영자가 DB dump 만으로는 PII 복원 불가). ## Evidence blob 저장 (content-addressed file store, D-frame-2) 큰 파일 (evidence PDF·통장 xls·세금계산서 등) 은 Postgres bytea 가 아니라 **content-addressed file store** `.local/files///` 에 저장. DB 의 `evidence`/`source_file` row 는 `content_hash` (SHA-256) + **portable** `storage_url` (relative `//`) 만 보유. - **컨테이너 마운트**: host `.local/files` ↔ 컨테이너 `/app/.local/files` bind mount (`FRAME_FILE_STORAGE_PATH`). read 시 `frame.ingest.pipeline.resolve_storage_path()` 가 portable url 을 live store 기준 absolute 로 매핑 → host·컨테이너 어디서 실행해도 같은 blob 해석. - **금지**: `local://` (외부 OneDrive/Downloads 포인터 — 컨테이너 read 불가 + rename/move 시 깨짐), CWD 의존 absolute 경로 (host `/Users/...` vs 컨테이너 `/app/...` 불일치). 모든 ingest write-path (`pipeline`/`card`/`fund`/`hometax`/`entity_meta`/`resolution`/`ops.evidence`) 가 `_store_file_locally` 로 통일. - **무결성 probe**: `GET /frame/health/storage` 가 모든 entity schema 의 evidence storage_url 을 resolve 해 blob 존재 확인 — bind-mount 회귀로 blob 이 사라지면 (2026-05-14 incident) 503 + missing-list. blob 은 절대 삭제 안 함 (append-only 정신). ## 정합성 검사 `frame integrity-check --entity axec` 가 4가지 검사를 수행: 1. **balance** — `SUM(debit) == SUM(credit)` per journal 2. **journal** — 모든 journal_line 이 valid account 참조 3. **account** — chart-of-accounts 와 journal_line 의 일관성 4. **fiscal_period** — period 의 starting/ending balance 정합 cron 으로 매일 자동 실행 + Slack alert. --- # 배포 · 무중단 > blue/green frame swap, axe-frame-proxy, axe CLI deploy. URL: https://docs.axelabs.ai/architecture/deploy # 배포 · 무중단 ## Blue/green 패턴 (frame) frame 은 **두 개의 컨테이너가 항상 동시 실행**됩니다 — blue (3710) + green (3711). 한쪽이 active, 다른 한쪽이 passive. ``` ┌──── frame-mcp-blue :3710 ◀ alias `frame-mcp` (active) │ axe-frame-proxy ─── (Caddy upstream) :3712 │ └──── frame-mcp-green :3711 (passive, 새 코드 받는 곳) ``` 배포 시퀀스: ``` 1. green container 에 새 코드 build + start 2. green 헬스체크 통과 대기 (poll /health/ready) 3. docker network alias `frame-mcp` 를 green 으로 이동 4. axe-frame-proxy 가 upstream 자동 reload (Caddy graceful) 5. blue 의 in-flight 요청 완료 대기 (60s grace) 6. blue stop → 다음 배포 시점에 새 코드로 build ``` 다운타임: **0 초**. 한 시점에 < 1s 의 latency spike 는 수용. ## axe CLI deploy 운영자 머신에서: ```bash # dry-run (변경사항 미리 표시) axe deploy frame axe # 적용 axe deploy frame axe --apply ``` `axe deploy frame {customer}` 가 위 6 step 을 자동 실행. 진행 상황은 stderr 에 step 별 로그. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | `docker compose up --build` (다운타임) | 30-60s 다운타임 | `axe deploy` 사용 | | `docker compose restart` (코드 미변경 시) | 다운타임 + env 재로딩 X | 코드 변경 → `axe deploy` | | cloudflared 의 SIGHUP | process 죽음 | host-side proxy 뒤로 분리 | | blue/green 컨테이너 이름을 cloudflared 에 직접 명시 | swap 불가 | docker network alias 사용 | | `--no-build` 잊고 deploy | 새 코드 적용 안 됨 | `axe deploy` 가 자동 처리 | ## docker-compose 구조 `/Users/axe/frame/docker-compose.yml`: ```yaml services: postgres: image: postgres:16-alpine container_name: frame-postgres ports: ["3700:5432"] frame-mcp-blue: &frame-mcp-blue build: . (Dockerfile) container_name: frame-mcp-blue ports: ["3710:3710"] environment: FRAME_DEPLOY_COLOR: blue FRAME_MCP_PORT: 3710 AZURE_FRAME_MCP_CLIENT_SECRET: ${AZURE_FRAME_MCP_CLIENT_SECRET:-} # ... 기타 env networks: default: aliases: [frame-mcp] # initial: blue active artemis_default: aliases: [frame-mcp] frame-mcp-green: <<: *frame-mcp-blue # YAML anchor 로 동일 설정 상속 container_name: frame-mcp-green ports: ["3711:3710"] environment: FRAME_DEPLOY_COLOR: green # ... 동일하지만 color=green networks: default: {} # green 은 alias 없음 (passive) artemis_default: {} ``` ## 무엇이 다운타임 0 을 가능케 하는가 1. **컨테이너 1대 만 build 중** — 다른 1대가 traffic 흡수 2. **docker network alias** 가 instant DNS 갱신 (DNS TTL 없음, in-memory) 3. **axe-frame-proxy 의 Caddy** 가 upstream 변경에 graceful (SIGHUP) 4. **cloudflared 가 build 와 무관** — 평소 라우팅 유지 ## 다른 서비스 — blueprint blueprint 은 단일 컨테이너 (현재). Next.js dev/build 시간이 짧고 (수십 초), 사용자 traffic 이 frame 만큼 critical 하지 않아 단순 deploy 사용: ```bash cd /Users/axe/blueprint && pnpm build && pnpm start # (launchd 또는 docker compose restart) ``` 다운타임 발생 시 사용자가 보는 것은 "Connecting..." 메시지 수 초. 회계 작업과 같은 in-flight transaction 없음. 향후 blueprint 도 blue/green 으로 전환 예정 (D-config-13 패턴 확장). --- # 도메인 · DNS > Cloudflare zone, 1-level subdomain 결정, customer 도메인 매핑. URL: https://docs.axelabs.ai/architecture/domains # 도메인 · DNS ## 두 개의 zone | Zone | 용도 | 상태 | |---|---|---| | `axelabs.ai` | **플랫폼** (이 docs, 모든 customer 서비스) | 2026-05-14 등록 | | `axellc.com` | **corporate** (회사 이메일, 기업 홈페이지) | 기존 | 플랫폼 트래픽은 `axelabs.ai` 만 거치고, `axellc.com` 은 corporate 용도로 분리됩니다. ## URL 컨벤션 ``` {customer}.axelabs.ai ← apex, blueprint {customer}.axelabs.ai/frame ← frame MCP {customer}.axelabs.ai/frame/mcp ← MCP endpoint (canonical resource URI) {customer}.axelabs.ai/vault ← Vaultwarden {customer}.axelabs.ai/index ← (예정) admin.axelabs.ai ← 운영자 콘솔 (Microsoft SSO) docs.axelabs.ai ← 본 docs ``` ## 왜 1-level subdomain + path? 선택지 비교: | 옵션 | SSL | URL 모양 | 운영 부담 | |---|---|---|---| | 1-level + path | ✓ 무료 (Cloudflare Universal SSL) | `axe.axelabs.ai/frame` | cloudflared path-aware 라우팅 | | 2-level subdomain | ❌ 유료 ($10/cert/월) | `frame.axe.axelabs.ai` | 일관 wildcard 필요 | | Service-only domain | △ | `frame.axelabs.ai` | customer 격리 불가 | → 1-level + path 채택. cloudflared ingress 의 `path` 매칭으로 service 분기. ## 함정 — Universal SSL wildcard 의 1-level 한계 (D-ops-39) 무료 Universal SSL 의 wildcard cert (`*.axelabs.ai`) 는 **1단 서브도메인만 cover**. 2단 (e.g. `ssh.axe.axelabs.ai`, `.axe.axelabs.ai`) 은 cert SAN 미일치 → edge 가 default cert 로 fallback → 클라이언트에서 `tls: handshake failure` / `SEC_E_ILLEGAL_MESSAGE` / `sslv3 alert handshake failure`. **규칙**: HTTPS 노출되는 모든 hostname 은 `{name}.axelabs.ai` 의 **1단** 이어야 함. service 또는 sub-context 가 필요하면: - ✅ **flat hostname** — `ssh-axe.axelabs.ai`, `docs-axe.axelabs.ai` - ✅ **path under apex** — `axe.axelabs.ai/frame` - ❌ **2단 subdomain** — `ssh.axe.axelabs.ai`, `frame.axe.axelabs.ai` **증상이 보이면**: 클라이언트의 `cloudflared --version` 이 최신이고 시간 동기화도 정상인데 handshake failure 가 모든 클라이언트에서 일관되면 서버측 cert 문제. zone DNS records 에서 hostname dot count 확인. 함정 사례 → [/ops/known-gaps#cloudflare-universal-ssl-1-level](/ops/known-gaps#cloudflare-universal-ssl-1-level). **유료 회피 옵션 평가**: Advanced Certificate Manager ($10/cert/월) 또는 zone delegation (`axe.axelabs.ai` 별도 zone) 모두 미채택. 무료 plan + flat-hostname 으로 충분. ## 현재 customer 별 할당 | customer | apex 도메인 | corporate 도메인 (이메일) | |---|---|---| | axe | `axe.axelabs.ai` | `axellc.com` | | realchoice | `realchoice.axelabs.ai` | `realchoice.co.kr` | ## DNS records axelabs.ai zone (Cloudflare): ``` CNAME axe .cfargotunnel.com ; orange-cloud ON (axelabs tunnel) CNAME realchoice .cfargotunnel.com ; orange-cloud ON (realchoice tunnel) CNAME admin .cfargotunnel.com ; orange-cloud ON CNAME docs .cfargotunnel.com ; orange-cloud ON CNAME ssh-axe .cfargotunnel.com ; orange-cloud ON (operator SSH) CNAME www axelabs.ai TXT axe "MS=ms10433167" ; Microsoft Entra domain verify ``` > `ssh-axe.axelabs.ai` 는 운영자/엔지니어가 macmini 에 SSH 진입하기 위한 Cloudflare Access 보호 endpoint. 클라이언트는 `cloudflared access ssh --hostname ssh-axe.axelabs.ai ...` 또는 `ssh user@ssh-axe.axelabs.ai` (ProxyCommand 설정 시). > 이전 hostname `ssh.axe.axelabs.ai` 는 [위 함정](#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39) 으로 폐기 (D-ops-39, 2026-05-26). DNS record 와 `*.axe.axelabs.ai` 와일드카드 는 backlog 에서 일괄 정리. Microsoft 도메인 검증 TXT 는 Application ID URI 에 `https://axe.axelabs.ai/frame/mcp` 같은 형식을 등록하기 위해 필요. 자세히는 [인증 · 권한](/architecture/auth#azure-app-id-uri). ## Customer 추가 시 1. `customers.yaml` 에 새 entry (예: `realchoice`) 2. Cloudflare zone 에 `A {customer}` record 추가 (proxy ON) 3. cloudflared config 에 ingress 규칙 추가 4. cloudflared 재시작 (1회만, ingress 안정 후 추가 변경 없음) 상세 절차는 [신규 customer onboarding](/ops/runbook/customer-onboarding). --- # 결정 거버넌스 — gate (결재 + e-sign + 법무) > gate 서비스의 결정→실행 거버넌스 아키텍처. record≠process, 서명 1-primitive, actuation ledger, Blueprint mirror, 그리고 narrowed Phase-1(gate-native 직접-build 암호 e-sign[CAdES-T]·내부검색·AXE-internal) vs Phase-2(외부 본인확인기관·flywheel·공개 SaaS·결제·멀티테넌트) 경계. URL: https://docs.axelabs.ai/architecture/governance # 결정 거버넌스 — gate gate 는 한국형 **내부 전자결재 + e-sign + HTML 문서 + 법무검색**을 하나의 **AI-driven 결정→실행 파이프라인**으로 묶는 신규 **Rust + Postgres** 서비스다. [para-os §7](/architecture/para-os)(결정로그 ≠ 결재워크플로)의 물질화 — 성숙한 거버넌스 Area 가 독립 서비스로 졸업한 형태. > 본 페이지 = gate **거버넌스 아키텍처 설계** (착수 직전 설계 + e-sign 엔진 빌트, 2026-06-07). 결정 SSOT = [D-gate-1](/ops/decisions)(scope·Phase) · [D-gate-2](/ops/decisions)(record SoR) · [D-gate-3](/ops/decisions)(actuation·e-sign 경계, e-sign 부분 D-gate-5 가 supersede) · [D-gate-4](/ops/decisions)(법무) · **[D-gate-5](/ops/decisions)(e-sign 직접구현 = 암호등급 자체 서명엔진, 빌트+검증 — D-gate-3 integrator 입장 supersede)**. 마스터플랜·착수 큐 = [B-bp-decision-pipeline-esign](/ops/backlog) (service=gate, roadmap M9). 설계 전모·리서치 결론은 gate machine 메모리 2파일. ## 1. Phase 경계 — narrowed Phase-1 (경로 A) 7라운드 설계 후 7-agent red-team 이 **풀설계**(결재 + 직접 e-sign + 법무 RAG flywheel + 공개 SaaS + 결제 + 멀티테넌트 = 1인이 동시에 짓는 6~7 규제제품)에 **NO-GO**(11 P0 / 16 P1 / 8 누락) 판정했다. 결정적 사실 — **11 P0 가 전부 "야심 5종"에 거주**한다. 따라서 narrowed Phase-1 은 대부분의 P0 를 *해소가 아니라 defer 로* 회피한다 ([D-gate-1](/ops/decisions)). > **단, e-sign 은 예외 (2026-06-07 [D-gate-5](/ops/decisions))**: 운영자가 *"모두싸인의 기능을 직접, 암호등급으로 구현"* 지시 → e-sign 은 defer 가 아니라 **직접-build 로 빌트**됐다 (§6). red-team 이 직접-build 를 막았던 P0(HSM·HTML→PDF 결정성·custody)는 이제 **해소-또는-명시**: 결정성 = canonical decision bytes 로 *지금*(typst PDF render 는 later), HSM = trait abstraction *ready*(dev = software key), custody = `esign_seal` append-only + RFC3161 TSA. **진짜 잔여 Phase-2 = 외부 서명자 본인확인기관**(정보통신망법 §23-3, 자가구축 불가) — 그 외 직접 e-sign P0 는 닫혔다. | 영역 | **Phase-1 (확정·착수 가능)** | **Phase-2+ (각자 게이트, defer)** | |---|---|---| | 결정로그 | gate `decision` SoR (record≠process) | — | | 결재워크플로 | 기안→DOA→승인/반려/보류→시행 상태기계 | — | | 문서 | HTML spine → PDF 렌더 | — | | e-sign | **gate-native 직접-build 암호 서명엔진** (CAdES-T server-seal + RFC3161 + esign_seal evidence + verify_seal; 모두싸인 = optional alt provider) — [D-gate-5](/ops/decisions) | 외부 서명자 **본인확인기관** 통합(정보통신망법 §23-3) · *하드닝*: HSM·PAdES 서명-PDF·prod-key | | 실행 | actuation ledger (멱등 + reconcile) | saga 보상 (e-sign+결제+분개) | | 가로 거버넌스 | Blueprint `gate.decision` citation mirror | visibility-gated mirror 테이블 | | 배포 대상 | **AXE-internal only** (realchoice/Truvia table-stakes) | 공개 SaaS (+tenant_id RLS·self-IdP) | | 법무 | **pgvector-KR 내부검색만** (결재 자동주입 X) | Pinecone+Cohere+Gemini flywheel (+PIPA 자문) | | 결제 | 없음 | 포트원/토스/Paddle (+전자금융/전자상거래) | | 테넌시 | 없음 (owner_id RLS) | cross-tenant 축적 (+PIPA 가명정보) | 빌드게이트(코드보다 먼저) = [B-gate-bonin-id-contract](/ops/backlog)(본인확인기관 계약, 수주~수개월) · [B-gate-legal-counsel](/ops/backlog)(변호사법 §109 + PIPA 의견서) · [B-gate-phase2-gates](/ops/backlog). ## 2. record ≠ process — SoR = gate, Blueprint = mirror - **결정 로그 (record)** — 무엇이·누가·언제·무슨 권한으로·뭘 근거로 결정됐나. append-only, 불변(수정 아닌 **supersede**). **SoR = gate `decision` 테이블** (서비스가 SoR 소유, [D-gate-2](/ops/decisions)). - **결재 워크플로 (process)** — 기안 → 결재선(DOA) → 승인/반려/보류 → 시행. 그 위 policy-driven·agent-assisted 레이어. 워크플로 완료 → 결정로그 한 줄 방출. **왜 gate 가 SoR 인가**: round-1 은 결정로그를 *Blueprint core primitive(가로)* 로 뒀으나 = AXE-internal 전제. standalone 외부제품 요구가 refine — record 의 집을 gate 로 옮겼다. 근거(grounded): [/architecture/artifacts](/architecture/artifacts) "MCP backend = sole owner, data 중복 0" · [/architecture/data](/architecture/data) "cross-service = 직접 DB read 금지, 명시적 신호채널(pg_notify/outbox)만" · [D-cortex-9](/ops/decisions) = service-owns-SoR + Blueprint visibility-gated mirror 가 이미 빌트(template 기성). Blueprint = `gate.decision` citation mirror(optional consumer 1종, 외부고객 = OFF, gate 단독 완결). ## 3. 핵심 테이블 (Blueprint-agnostic) | 테이블 | 역할 | |---|---| | `document` | **HTML 작성 스파인**(풍성한 포맷) + 변수(AI/템플릿 fill) + rendered_pdf_ref(서명용) + version + kind(기안/계약/통지). 내부결재 + e-sign = 같은 document 의 두 phase. | | `workflow` | 내부결재 상태기계: draft→상신→in_review(결재선 step)→승인/반려/보류→시행→done. | | `approval_step` | 결재선 (kind: 승인/전결/협조/합의/대결 · state: pending/approved/rejected/held/skipped). **반려·철회·서명거절 역흐름 = 1급 시민**(red-team 누락 #8). | | `doa_policy` | 전결규정 (entity · decision_type · amount_min/max → line jsonb). | | `signature` | 내부+외부 서명 통합 primitive (provider ∈ `{internal, gate, modusign}` · signers · envelope_id · cert_ref · status). §4. | | `esign_seal` | **암호 봉인 evidence** (append-only, 전자문서법 §5 custodian) — decision_id · sealed_bytes_hash · **CMS/CAdES-T DER**(detached 서명 + RFC3161 토큰) · signer_cert_ref · tsa_url · sealed_at. 시행 시 `esign.gate` actuator 가 1행 append, `verify_seal` 가 읽음. §4·§6. | | `actuation` | 실행 ledger (actuator_kind · payload · idempotency_key · state · result_ref · reconciled_at). §5. | | `event` | append-only audit. | | `decision` | gate SoR terminal record (authority · actor · decided_at · basis citations · supersedes). canonical bytes = `esign_seal` 봉인 대상. | ## 4. 서명 = 한 primitive 내부 sign-off = 외부 e-sign = 같은 primitive `signature`. 누가·법적무게만 다르다. `provider ∈ {internal, gate, modusign}`: - **`internal`** — 결재선 step 의 인증된 결재 승인 (UI sign-off). gate identity = 실지명의. - **`gate`** (Phase-1, [D-gate-5](/ops/decisions)) — **gate-native 암호 봉인**. 시행 시 승인된 결정의 canonical bytes 를 단일 서버 서명 identity 로 **CAdES-T server-seal** → `esign_seal` 에 evidence 저장. 봉인 1건 = `signature(provider=gate)` + `esign_seal` 1행. §6. - **`modusign`** (optional alt provider) — 외부 카운터파티 라운드트립이 필요할 때의 대체 경로 (`esign.send`/EsignStub). Phase-1 의 *대안*이지 기본 아님. 내부 결재 승인도, gate 서버 봉인도, 외부 모두싸인 e-sign 도 "행위자가 결정을 시점에 (법적으로) 확정"하는 한 종류의 행위 — [para-os §84](/architecture/para-os) 와 정합. ## 5. actuation ledger + reconcile (분산 일관성) 승인된 결정 → **actuator 호출** (`frame.post_journal` / `index.register_deal` 등; Area 서비스 = 결정 파이프라인의 actuator). 결정→실행은 분산 트랜잭션이므로: - `actuation.idempotency_key = uuidv5(workflow + seq + actuator + payload_hash)` — gate 측 재시도 멱등. - **reconcile 워커** (frame-worker 패턴, pg_notify) — inflight→done/failed 정합. - ⚠️ **gate idempotency_key 는 *외부* 이중실행을 못 막는다** (red-team P0 #5) — `esign.gate` 봉인은 gate 내부 actuator라 `esign_seal`(decision_id 유일) append 가 멱등이지만, *외부* provider 경로(모두싸인 alt) 는 **provider-side idempotency 토큰** 필요. saga 보상(e-sign+결제+분개)은 결제 도입 Phase-2. ## 6. e-sign 경계 — Phase-1 = gate-native 직접-build 암호 서명엔진 > **2026-06-07 pivot ([D-gate-5](/ops/decisions), [D-gate-3](/ops/decisions) e-sign 경계 supersede)**: [D-gate-3](/ops/decisions) 은 e-sign 을 모두싸인 integrator 로 두고 직접-build 를 Phase-2 로 defer 했으나, 운영자가 *"모두싸인의 기능을 직접, 암호등급으로 구현"* 지시 → **Phase-1 이 직접-build 엔진**이다. 빌트+검증 완료 (이 세션). **Phase-1 = BUILD (직접 구현, 작동·검증됨)** — gate 가 자체 암호 서명엔진을 갖는다 ([para-os §84](/architecture/para-os) 와 정합: gate = 결정 파이프라인의 actuator, 봉인은 `esign.gate` actuator): - **① CMS/PKCS#7 CAdES-BES detached 서명** (RSA-2048 + SHA-256). `DocumentSigner` trait(**HSM-ready** abstraction) + `SoftwareSigner`(self-signed X.509). 적대적 리뷰(위조 도달경로 없음) + **openssl 양방향 interop**, 7 tests. - **② RFC3161 신뢰 타임스탬프 → CAdES-T** (hand-rolled ASN.1, 단일 clean dep line). default = DigiCert RSA TSA; 한국 prod = **Koscom/CrossCert** config-swap. 9 tests + live round-trip. - **⑤ 워크플로 배선**: **시행(execute)** 시 `esign.gate` actuator 가 승인된 결정을 **server-seal** — canonical decision bytes → CAdES-T → append-only **`esign_seal`** evidence 테이블 저장 (**전자문서법 §5 custodian**). 신규 **`verify_seal` MCP tool** = 서명 + 무결성 + 타임스탬프 검증. e2e: 결재→seal→verify valid · 저장 CMS openssl cross-verify 통과 · DB tamper→integrity fail. **법적 근거 — 전자서명법 (2020, 공인 폐지)**: 사서명(private/internal e-sign)은 **실지명의 + 서명의사**면 유효, 운영 라이선스 불요. gate 매핑 = **실지명의 = 인증된 gate identity**(내부 서명자) · **서명의사 = 결재 승인 행위**. → 내부/사내 e-sign 은 본 엔진으로 **합법 완결**. **모두싸인 = optional alternative provider** (기본 아님): 외부 카운터파티 라운드트립이 필요할 때 `signature.provider=modusign` 경로 (`esign.send`/EsignStub) 가 여전히 존재. gate-native 봉인이 Phase-1 기본, 모두싸인은 *대안*. **잔여 하드닝 / Phase-2**: - **외부 서명자 본인확인 = 진짜 Phase-2** — 방통위 지정 **본인확인기관** 통합([B-gate-bonin-id-contract](/ops/backlog), 정보통신망법 §23-3 자가구축 불가). 내부 서명자는 gate auth = 실지명의라 불요; 외부 신원 강증명만 게이트. - **HSM** (cryptoki/PKCS#11) — `DocumentSigner` trait 가 이미 **abstraction-ready**, dev 는 software key. prod = HSM-backed `DocumentSigner` 구현 + **서명키 DR/escrow**(분실 = 전 과거서명 검증불가). - **PAdES 서명-PDF 컨테이너** (③④) — typst **deterministic render** + pyHanko PAdES. 현재 봉인 대상 = decision canonical bytes(결정적); 사람이 읽는 sealed-PDF 는 later. - **production 키관리** — dev 는 ephemeral key. prod = 영속 서버 서명 identity + 회전·백업. **MUST NOT (Phase 무관, 불변)**: 자체 CA · per-user cert · 자체 TSA · "공인/인증" 주장. gate 는 **단일 서버 서명 identity** 로 봉인한다 (per-user cert 발급 없음) — 이것이 **ModuSign-recipe 모델**(provider 가 서버 서명으로 봉인, 사용자별 인증서 없이). 자체 TSA 도 안 만든다 — 외부 RFC3161 TSA(DigiCert/Koscom/CrossCert)에 위임. **환경 변수** (signing identity + TSA): `GATE_ESIGN_KEY_PEM` / `GATE_ESIGN_CERT_PEM` (서버 서명 키·인증서 PEM, 또는 `GATE_ESIGN_KEY_DIR` 로 디렉터리 지정) · `GATE_TSA_URL` (RFC3161 TSA 엔드포인트, default DigiCert / 한국 prod Koscom·CrossCert). prod 의 `GATE_ESIGN_KEY_PEM` = **비밀** → `customers.yaml services.gate.secrets[]` 에 등재(blueprint OIDC 서명키 `blueprint/axe/oidc-signing-key` 패턴, [/architecture/secrets](/architecture/secrets)). ## 7. 법무 모듈 가드레일 gate 법무모듈 = **검색·요약·번역·템플릿(로폼: user-blank, 기계적)만** ([D-gate-4](/ops/decisions)). **하드룰 — 변호사법 §109(형사)·§34(5)**: 법률상담/사안별 자문/대리/결과예측-as-advice **금지**. **공개 "AI 법률상담" 금지**(AI대륙아주 7개월 폐쇄·징계 선례). 변호사 노출 시 정액광고만(로톡 모델 — 알선료/성공보수/광고수익배분 금지). 모든 출력 "**법률 정보, 자문 아님**" disclaimer. cite = **retrieval-verified**(존재 + 시행일/선고일 temporal 강제). **결재 자동주입 차단** — AI 기안이 법령/판례를 auto-approve 경로에 주입하지 않음(red-team P0 #2). **Phase-1 data**: 국가법령정보 OPEN API(open.law.go.kr, 이용제한없음·무료) + data.go.kr 판례(상용 OK) + HF `korean_law_open_data_precedents`(openrail). 대법원/헌재 + 법령 grade(하급심 = 엘박스/케이스노트 상용 walled 라 제외). SoR = gate Postgres, index = **pgvector-KR**(Pinecone 아님). `lbox_open`/`kcl` = CC BY-NC = 유료제품 금지. **Phase-2 gate**: Pinecone + Cohere rerank + Gemini embed flywheel(LLM비용 사용자부과·DB 복리) + cross-tenant 축적 = **PIPA 처리근거·가명정보·국외이전(§28-8) 자문**([B-gate-legal-counsel](/ops/backlog)) 선결. B2B/internal-use 유지 = 최저위험. ## 8. 규제 경계 요약 | 법령 | 무엇을 강제하나 | Phase | |---|---|---| | 전자서명법 (2020, 공인폐지) | 사서명도 실지명의 + 서명의사면 유효 — 직접구현 합법, 운영 라이선스 불요. gate: 실지명의 = gate auth · 서명의사 = 결재 승인 | **Phase-1** (직접 e-sign 빌트, [D-gate-5](/ops/decisions)) | | 전자문서법 §5 | 서명문서 + 메타 무결성 보관 (gate = evidence custodian via `esign_seal` append-only + RFC3161 TSA) | **Phase-1** (gate-native 봉인) | | 정보통신망법 §23-3 | 본인확인 자가구축 불가 → 방통위 지정 본인확인기관 통합 필수 | Phase-2 (**외부 서명자 신원** 한정; 내부 서명자는 gate auth = 실지명의라 불요) | | 변호사법 §109/§34 | 법률사무 무면허 금지 — 검색·정보 한정, 자문/대리/auto-주입 금지 | **Phase-1** (법무 활성 시) | | 개인정보보호법 (PIPA) | cross-tenant 축적·국외이전·CI/DI = 처리근거·가명정보·DPIA | Phase-2 (flywheel/SaaS 전제) | | 전자상거래법 (2025 개정) | 정기결제 명시동의·셀프해지·사전고지·다크패턴 금지 | Phase-2 (결제 전제) | 대법원 2017도13263(해시 무결성 → 서명시점 해시 + custody chain) · 민사소송법 §358(실제 서명행위 → 진정성립)이 증거능력 타깃. 제외문서 = 유언·부동산등기·공정증서·일부보증(민법 §428-2). ## 9. Phase-2 게이트 (deferred) 각 야심은 자기 P0/규제 게이트를 동반한다 ([B-gate-phase2-gates](/ops/backlog)): 1. **e-sign — 엔진은 Phase-1 빌트 ([D-gate-5](/ops/decisions)), 잔여만 Phase-2/하드닝**: (Phase-2) 외부 서명자 **본인확인기관** 통합(정보통신망법 §23-3) · (하드닝) HSM/KMS `DocumentSigner`(trait ready) + 서명키 DR/escrow · PAdES 서명-PDF(typst deterministic render + pyHanko) · production 키관리. *닫힌 항목*: 자체 PKI 서버서명·CAdES-T·RFC3161 타임스탬프·custody(`esign_seal`)·canonical-bytes 결정성·para-os §84 재결정 = §6 참조. 2. **Pinecone 법무 flywheel** — PIPA 자문 · 비식별/가명정보 · namespace 테넌트격리 · PII 임베딩 전 제거 코드강제. 3. **공개 SaaS** — tenant_id RLS substrate(owner_id RLS 재설계, red-team P0 #8) · self-IdP tenant바인딩/iss/aud/per-tenant 시크릿/MFA(P0 #9) · rate-limit·abuse · 서브도메인 격리. 4. **결제** — 전자금융/전자상거래법(동의·셀프해지·사전고지·다크패턴) · LLM 미터링 과금 · Paddle MoR 영세율. (Stripe = 한국법인 불가.) 5. **cross-tenant 축적** — PIPA 근거 확정 후. ## 10. 착수 순서 docs-first(본 페이지 + D-gate-1~5) → `cp -R cortex gate` scaffold(auth 3-branch ladder[HS256 / Blueprint OIDC / Entra] · `gate_app` NOSUPERUSER + `gate.actor` GUC RLS · append-only audit · blue/green caddy 상속, index 도메인 미상속) → 포트 41xx 배선(pg4100 / blue4110 / green4111 / proxy4112) → 7테이블 migration(6 + `esign_seal`) → 상태기계 → **`esign.gate` actuator**(gate-native CAdES-T server-seal + `esign_seal` + `verify_seal`, [D-gate-5](/ops/decisions) — 빌트) + 모두싸인 alt-provider actuator(Actuator trait + EsignStub → 모두싸인 sandbox, optional) → `customers.yaml services.gate.secrets[]`(+ `GATE_ESIGN_KEY_PEM` 서명키) · cloudflared · pre-push guard. ## 관련 - [/architecture/para-os](/architecture/para-os) §7 — 결정로그 ≠ 결재워크플로 (본 페이지의 상위 철학) - [/architecture/artifacts](/architecture/artifacts) — Artifact · Citation 저장/스키마 (Blueprint mirror 토대) - [/architecture/data](/architecture/data) — 데이터 격리 · cross-service 신호채널 규율 - [D-gate-1](/ops/decisions) · [D-gate-2](/ops/decisions) · [D-gate-3](/ops/decisions) · [D-gate-4](/ops/decisions) · [D-gate-5](/ops/decisions)(e-sign 직접구현) — 본 페이지의 결정 등재 - [B-bp-decision-pipeline-esign](/ops/backlog) — 마스터플랜 + 착수 큐 (service=gate, M9) --- # MCP 서버 개발 체크리스트 > frame / hive / blueprint 누적 lesson — 양파껍질 5층 + Azure manifest 9 항목 + 운영 21 항목 (skill-integration 5 포함). URL: https://docs.axelabs.ai/architecture/mcp-server-checklist # MCP 서버 개발 체크리스트 claude.ai Custom Connector + Claude Code 가 받는 표준 MCP 서버를 추가할 때, **frame · hive 가 검증한 byte-by-byte 패턴을 1:1 으로 복사**해야 한다. 비슷하게 만들면 5층 양파껍질이 차례로 폭로된다 (Blueprint 2026-05-21 실제 사고). > **핵심 원칙**: 새 MCP 서버는 `cp -R /Users/axe/frame/src/frame /tmp/...` 부터 시작하라. "유사하게" 가 아니라 "diff 가 없을 때까지". 단 4 단어 (`audience=[client_id, app_id_uri]`) 빠뜨려도 OAuth 통과 후 401 무한루프. --- ## 0. 가기 전 — 결정 (D-bp-mcp-1, 2026-05-21) | 결정 | 이유 | |---|---| | **Python + FastMCP + Starlette + uvicorn** 으로 통일 | claude.ai 의 Streamable HTTP 가 frame 의 wire-level response 만 수용. Node port 시도 (visible response 동일) 도 silent reject. | | DB 는 **기존 Prisma/Drizzle/SQLAlchemy schema 를 raw SQL 로 read-only access** | 별도 schema 필요 없음. SQLAlchemy `text("SELECT ... FROM \"User\" WHERE ...")` 패턴. | | **OAuth Resource Server (RP)** 직접 — 자체 AS 구축 금지 | D-ops-15 dead-end: claude.ai 는 외부 AS 의 PKCE 우회 못함. 무조건 Microsoft Entra ID 직결. | | **standalone 컨테이너** — main service 컨테이너에 mount X | claude.ai connector probe 가 Next.js 의 `vary: rsc, ...` auto-header 거부. 최소 header 의 standalone Python 필요. | --- ## 1. Azure Entra ID app registration — 9 항목 `az ad app create` 한 줄로 끝나지 않는다. 각 항목 확인. ```bash # 1. app + sp 동시 생성 az ad app create --display-name "Blueprint MCP" --sign-in-audience AzureADMyOrg az ad sp create --id APP_ID # ← 명시적 SP 생성, 자동 생성 안 됨 # 2. identifierUris (= App ID URI) az ad app update --id APP_ID --identifier-uris "https://axe.axelabs.ai/blueprint/mcp" # 3. accessTokenAcceptedVersion = 2 (v2 endpoint 가 표준) az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/OBJECT_ID" \ --body '{"api":{"requestedAccessTokenVersion":2}}' # 4. oauth2PermissionScopes (delegated permission) az rest --method PATCH ... --body '{ "api": {"oauth2PermissionScopes":[{ "id":"RANDOM_UUID", "value":"mcp.access", "type":"User", "isEnabled":true, "adminConsentDisplayName":"Access Blueprint MCP", "userConsentDisplayName":"Access Blueprint MCP" }]} }' # 5. redirectUris (claude.ai 의 양쪽 도메인) az rest --method PATCH ... --body '{"web":{"redirectUris":[ "https://claude.ai/api/mcp/auth_callback", "https://claude.com/api/mcp/auth_callback" ]}}' # 6. requiredResourceAccess (Microsoft Graph User.Read — id_token 의 email claim 발급) az rest --method PATCH ... --body '{"requiredResourceAccess":[{ "resourceAppId":"00000003-0000-0000-c000-000000000000", "resourceAccess":[{"id":"e1fe6dd8-ba31-4d61-89e7-88639da4683d","type":"Scope"}] }]}' # 7. implicitGrantSettings.enableIdTokenIssuance = true az rest --method PATCH ... --body '{"web":{"implicitGrantSettings":{"enableIdTokenIssuance":true}}}' # 8. client_secret 생성 + vault push (stderr 분리 필수) az ad app credential reset --id APP_ID --years 2 --query password -o tsv 2>/dev/null \ | axe secret push CUSTOMER/SERVICE/mcp-client-secret - # 9. tenant admin consent (필요 시 Azure Portal 에서 1 클릭) # User.Read + mcp.access 둘 다 grant ``` **함정 — `az` stderr 가 password 와 합쳐져 vault corrupt (Blueprint 270자 사건 2026-05-21)**: 반드시 `2>/dev/null` 로 stderr 분리. **검증**: ```bash az ad app show --id APP_ID --query "{accessTokenAcceptedVersion:api.requestedAccessTokenVersion, identifierUris:identifierUris, signInAudience:signInAudience}" -o json ``` frame · hive · blueprint 비교 시 차이 0 이어야 한다. 차이 있으면 fix. **별개 — app-only write 기능의 Graph Application permission**: 위 9 항목은 **MCP connector app** (token 검증 + id_token email claim 용 `User.Read`) 한정이다. Blueprint 의 admin send-as write 기능은 app-only token (`getAppOnlyClient()`, client-credentials)을 쓰므로 **Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`)** 에 Application permission 을 부여해야 한다 — connector app (`482598f7-...`) 이 아니다 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)): | 기능 | Application permission | 결정 | |---|---|---| | calendar send-as (`as_user_email`) | `Calendars.ReadWrite` | D-bp-mcp-calendar-2 | | `send_mail` send-as (`as_user_email`) | `Mail.Send` | D-bp-mcp-mail-1 | 부여 후 `az ad app permission admin-consent --id 2b222356-1c36-48e0-96a3-2c5e0ecbf937` (Global Admin) → `docker restart blueprint-app-blue` (또는 -green; MSAL client-credential in-memory 캐시 폐기). 상세 = [/architecture/auth § app-only Application permission (send-as)](../architecture/auth). 비-admin caller 의 send-as 시도는 코드가 403 `send_as_forbidden` 로 차단하므로 self (delegated) 경로는 이 permission 없이도 동작. --- ## 2. cloudflared ingress — 1 줄 (path-based) `/Users/axe/.axe/tunnels/axelabs/config.yml` 의 `ingress:` 에 prepend: ```yaml - hostname: axe.axelabs.ai path: ^/SERVICE/mcp(/.*)?$ service: http://host.docker.internal:PROXY_PORT - hostname: axe.axelabs.ai path: ^/SERVICE/\.well-known/oauth-protected-resource(/.*)?$ service: http://host.docker.internal:PROXY_PORT ``` **함정**: cloudflared 는 **path prefix 를 strip 하지 않는다**. 컨테이너가 `/SERVICE/mcp` 를 그대로 받음. 따라서 Starlette 라우트도 `/SERVICE/mcp` 에 mount. --- ## 3. Caddy reverse proxy (axe-SERVICE-mcp-proxy) frame-proxy / hive-proxy 미러. cloudflared 가 graceful reload 미지원이라 사이에 끼움 (blue/green swap 시 sub-second). `/Users/axe/.axe/SERVICE-mcp-proxy/Caddyfile`: ``` :80 { reverse_proxy SERVICE-mcp:3000 { lb_try_duration 5s lb_try_interval 250ms header_up Host {http.request.host} } encode gzip log { output stdout; format console; level INFO } } ``` **함정**: nested `handle_path` syntax 쓰지 마. 단순 `reverse_proxy` 만 (Blueprint Caddyfile syntax error 2026-05-21). --- ## 4. Python 코드 — frame `auth_oidc.py` + `mcp/http_server.py` 1:1 미러 ### 4.1 `auth_oidc.py` — JWKS + 토큰 검증 (∼180 줄) **필수 4 가지 (Blueprint 양파껍질 1·3 사건)**: ```python # ① httpx.Timeout — 4-arg 또는 default 명시. partial kwargs 는 0.28 부터 ValueError. _JWKS_HTTP_TIMEOUT = httpx.Timeout(connect=2.0, read=3.0, write=2.0, pool=5.0) # ^^^^^^^^^^^^^^^^^^^^ 빠뜨리면 500 # ② audience = [client_id GUID, application_id_uri] # v2 endpoint 의 access_token aud = client_id GUID (NOT App ID URI). # PyJWT 가 list 의 any 매칭. 둘 다 줘야 v1/v2 양립. expected_audiences: list[str] = [get_client_id(), get_app_id_uri()] payload = pyjwt.decode( token, public_key, algorithms=["RS256"], audience=expected_audiences, # ← list, 단일 string 금지 issuer=[get_microsoft_issuer(), get_microsoft_issuer_v1()], options={"require": ["exp","iat","iss","aud","sub"]}, leeway=60, ) # ③ JWKS cache + fail cooldown + kid-miss force refetch # Microsoft 의 key rotation 대응. frame/auth_oidc.py:47-78 그대로. # ④ email claim fallback 순서 email = (payload.get("email") or payload.get("preferred_username") or payload.get("upn") or "").strip().lower() ``` ### 4.2 `mcp/http_server.py` — Starlette + FastMCP wiring (∼270 줄) **필수 5 가지**: ```python # ① lifespan forward — Starlette Mount 는 child lifespan 미전파 def build_app() -> Starlette: mcp_app = mcp.streamable_http_app() @asynccontextmanager async def _lifespan(app: Starlette): async with mcp_app.router.lifespan_context(app): # ← 안 부르면 try: yield # "Task group not initialized" finally: await db_shutdown() # ② 401 reason 노출 (body + log) def _unauthorized(request, request_id, reason): log.warning("401 %s reason=%s", request.url.path, reason) body = {"error": { "code": _map_code(reason), # MISSING_TOKEN / INVALID_TOKEN / USER_NOT_PROVISIONED / UNAUTHORIZED "message": reason, # ← 고정 string 금지. reason 그대로. "context": {"request_id": request_id, "reason": reason}, ... }} # WWW-Authenticate resource_metadata = APP_ID_URI 에서 /mcp suffix strip + /.well-known/... # ③ FastMCP DNS-rebinding guard 우회 — main() 안에서 build_app() 전에: allowed_hosts = os.environ.get("SERVICE_MCP_ALLOWED_HOSTS") if allowed_hosts: mcp.settings.transport_security.allowed_hosts = [h.strip() for h in allowed_hosts.split(",") if h.strip()] # 안 하면 axe.axelabs.ai 가 421 Misdirected Request # ④ Route mount: cloudflared 가 path 그대로 보내므로 `/SERVICE/` mount + 화이트리스트 routes = [ Route("/health", health), Route("/SERVICE/health", health), Route("/SERVICE/.well-known/oauth-protected-resource", oauth_protected_resource), Mount("/SERVICE", app=mcp_app), ] # ⑤ JWTAuthMiddleware 의 public-path 화이트리스트도 `/SERVICE/` prefix 포함 if path in ("/health", "/SERVICE/health", "/SERVICE/.well-known/oauth-protected-resource", ...): return await call_next(request) ``` --- ## 5. docker-compose.yml — 5 항목 ```yaml mcp: build: { context: .., dockerfile: mcp/Dockerfile } container_name: SERVICE-mcp env_file: ../.env environment: - SERVICE_MCP_PORT=3000 - SERVICE_MCP_APP_ID_URI=${SERVICE_MCP_APP_ID_URI:-https://axe.axelabs.ai/SERVICE/mcp} - AZURE_AD_TENANT_ID=${AZURE_AD_TENANT_ID:-122fb574-...} # ① allowed_hosts (axe.axelabs.ai + 컨테이너 DNS + localhost) - SERVICE_MCP_ALLOWED_HOSTS=SERVICE-mcp:*,localhost:*,127.0.0.1:*,host.docker.internal:*,axe.axelabs.ai - SERVICE_MCP_ALLOWED_ORIGINS=https://axe.axelabs.ai,https://claude.ai,https://claude.com networks: default: aliases: - SERVICE-mcp # ② artemis network 도 참여 (axelabs-tunnel 같은 네트워크여야 cloudflared 가 닿음) artemis: # ③ Python 이미지면 curl healthcheck (Node 잔재 `node -e fetch(...)` 금지) healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"] interval: 30s timeout: 5s retries: 3 start_period: 10s depends_on: postgres: condition: service_healthy ``` **함정 (Blueprint 양파껍질 5 사건)**: healthcheck 가 Node 잔재면 컨테이너 영원히 `unhealthy`. Python 이미지면 curl 만. --- ## 6. customers.yaml entry — D-ops-17 매니페스트 ```yaml azure_apps: CUSTOMER: SERVICE_mcp: # D-SERVICE-mcp-1 client_id: "GUID" application_id_uri: "https://axe.axelabs.ai/SERVICE/mcp" scopes: - "https://axe.axelabs.ai/SERVICE/mcp/mcp.access" # client_secret 는 vault 에서 pull (env 명시) client_secret_env: "AZURE_SERVICE_MCP_CLIENT_SECRET" services: SERVICE: env_file: "/Users/axe/SERVICE/.env" secrets: - { env: AZURE_SERVICE_MCP_CLIENT_SECRET, vault: "SERVICE/CUSTOMER/mcp-client-secret", rotation_external: azure } # 기타 비밀들 ... ``` `axe secret push PATH -` 로 push, `axe secret pull SERVICE` 로 deploy-time pull. --- ## 7. 등록 절차 — 사용자 화면 (claude.ai) 1. Settings → Connectors → **+ Add custom connector** 2. URL: `https://axe.axelabs.ai/SERVICE/mcp` 3. Client ID + Client Secret 입력 (vault 에서 `axe secret get SERVICE/CUSTOMER/mcp-client-secret -n`) 4. Microsoft 로그인 (axellc.com 계정) → 동의 → claude.ai 복귀 5. **성공 신호**: connector 상세에 tool 목록이 노출됨 (whoami, list_..., 등) **실패 시 사용자 화면 메시지 → 진짜 원인 매핑** (Blueprint 5 fix 회고): | claude.ai 메시지 | 실제 원인 | 진단 path | |---|---|---| | "the integration may not be available right now due to a temporary error" | server 가 500 응답 (verifier 자체 ValueError 등) | `docker logs SERVICE-mcp` traceback | | "the integration rejected the credentials, so the connection was reverted" | server 가 401 응답 | log.warning `reason=...` 확인 (`§ 4.2 ②` fix 필수) | | "Audience doesn't match" (응답 body) | `audience=` 가 list 아님 (`§ 4.1 ②`) | aud claim 확인 — v2 면 client_id GUID | | "Task group is not initialized" (응답 body 또는 컨테이너 로그) | lifespan forward 누락 (`§ 4.2 ①`) | boot log 에 "StreamableHTTP session manager started" 있어야 함 | | "Invalid Host header" + 421 | allowed_hosts 미설정 (`§ 4.2 ③`) | `BLUEPRINT_MCP_ALLOWED_HOSTS` env 확인 | --- ## 8. 운영 — 21 가지 체크포인트 배포 후 다음을 모두 확인. 어느 하나 실패 시 production 으로 안 보냄. - **#1–#16 = 전송·인증·부팅 레이어** (frame · hive · blueprint 가 검증한 wire/OAuth/DB 항목). - **#17–#21 = skill-integration 레이어** ([index](../services/index) 가 도입). MCP 서버가 *판단 도구* (propose_deal_closure 류 write tool + ic / pmc skill 호출) 를 노출하면, 도구가 idempotent 한지 · citation 이 roundtrip 하는지 · skill 끼리 충돌 시 audit 에 남는지 · 의존 down 시 graceful 한지 · schema 가 진화해도 옛 artifact 가 읽히는지를 추가로 검증한다. write tool 없는 read-only MCP (matrix 류) 는 #17–#21 면제. | # | 검증 | 명령 | 정답 | |---|---|---|---| | 1 | 컨테이너 healthy | `docker ps \| grep SERVICE-mcp` | `Up X (healthy)` | | 2 | health endpoint | `curl https://axe.axelabs.ai/SERVICE/health` | 200 OK | | 3 | OAuth metadata | `curl https://axe.axelabs.ai/SERVICE/.well-known/oauth-protected-resource` | 200 + 정확한 issuer/scopes | | 4 | Bearer 없음 → 401 | `curl -X POST https://axe.axelabs.ai/SERVICE/mcp` | 401 + `WWW-Authenticate: Bearer ... resource_metadata="..."` | | 5 | fake Bearer → 401 with reason | `curl -X POST ... -H "Authorization: Bearer xxx"` | 401 + body.error.context.reason 노출 | | 6 | boot log: session manager | `docker logs SERVICE-mcp` | `StreamableHTTP session manager started` | | 7 | JWKS fetch 성공 | (첫 인증 호출 후) `docker logs` | `GET .../discovery/v2.0/keys "HTTP/1.1 200 OK"` | | 8 | 컨테이너 DNS alias | `docker exec axelabs-tunnel ping -c1 SERVICE-mcp` | 응답 | | 9 | Caddy → backend | `docker logs axe-SERVICE-mcp-proxy` | `upstream "SERVICE-mcp:3000"` | | 10 | cloudflared ingress | `cloudflared tunnel info axelabs` | hostname 포함 | | 11 | Azure app manifest | `az ad app show --id APP_ID` | accessTokenAcceptedVersion=2, identifierUris=URL 형식 | | 12 | SP 존재 | `az ad sp list --filter "appId eq 'APP_ID'"` | non-empty | | 13 | vault secret 유효 | `axe secret get PATH -n \| wc -c` | 40~50 자 (270 자면 stderr 섞임) | | 14 | claude.ai 직접 등록 | 위 § 7 의 1~5 단계 | tool 목록 노출 | | 15 | **DATABASE_URL 유효** (D-bp-mcp-3) | `docker exec SERVICE-mcp printenv DATABASE_URL` | `postgresql://...` 또는 `postgresql+asyncpg://...` (SQLite `file:./...` 면 = .env 잔재 → 제거) | | 16 | **startup probe 통과** (D-bp-mcp-3) | `docker logs SERVICE-mcp \| grep "startup probe"` | `startup probe: DB reachable, schema accessible` (없으면 lifespan fail = DB 미접속). ⚠️ `/health/ready` endpoint 만 가진 service (frame · hive 2026-05-22 상태) 는 broken DB 로 부팅 가능 — first request 가 와야 fail → blue/green swap promote 위험. **lifespan 안에서 SELECT 1 1 블록 호출** 패턴 강제. `blueprint/mcp/src/blueprint_mcp/mcp/http_server.py:238-260` 1:1 미러. | | 17 | **skill write tool idempotency** (D-index-13) | 동일 input 으로 write tool 2회 호출 (예: `propose_deal_closure(deal_id=X, idempotency_key=K)` 두 번) | 1회차 = 적용 + `(artifact_id, was_inserted=true)`, 2회차 = **`409` / `IdempotencyConflict`** (또는 `was_inserted=false` idempotent skip) — **두 번째 호출이 중복 ledger row 를 만들면 FAIL**. index 는 `idempotency_record` 테이블 (hive 패턴 mirror, 24h TTL) 로 atomic propose 의 race 차단 — `index/migrations/20260528000001_shared_schema.up.sql` + `error.rs::IndexError::IdempotencyConflict` (→ `StatusCode::CONFLICT`). agent 가 retry/재호출해도 안전해야 함 (네트워크 timeout 후 재시도 = 흔함). | | 18 | **citation resolver roundtrip** (durable ≠ fragile) | propose 한 artifact 의 citation 을 다시 resolve → 원본 anchor 와 대조 | resolve 한 값이 원본과 **상대오차 ±0.001% 이내** (재무 수치는 bit-exact 권장). `durable` citation (anchor = sha256 / drive_item_id / frame journal_id 같은 stable ref) 은 100% 재현, `fragile` citation (텍스트 anchor·페이지 추정) 은 별도 카운트되어 노출되어야 함 — `index/src/html.rs` 의 `citation_durable_total` / `citation_fragile_total`. fragile 비율이 0 이 아니면 status board 에 표시 (숨기면 FAIL). | | 19 | **cross-skill conflict → audit_trail** (silent resolve 금지) | 두 skill (예: ic 재무모델 vs frame 실적분개) 이 같은 metric 에 다른 값을 propose → conflict 감지 | 충돌이 **자동으로 한쪽을 골라 덮어쓰지 않고**, `artifact_event` append-only 감사 로그 (op=propose) + reconcile 레이어에 `{min, max, relative_gap, [{value, source_file, location}]}` 로 노출되어야 함. index 는 상대갭 **10% 초과** 시 metric group 을 conflict 로 flag (`artifact.rs` conflict guard) — "conflict 는 metric 레벨에서 표면화, 절대 silent 해결 안 함". 판단 도구가 충돌을 조용히 삼키면 FAIL. | | 20 | **graceful degradation** (의존 down 시 markdown-only / warn) | 의존 서비스 1개 강제 정지 후 도구 호출 (예: skill 의 humanize sub-agent OR frame MCP upstream 차단) | 전체 pipeline 이 **panic / 502 로 죽지 않고** degrade — index ic skill 은 humanize 2회 실패 시 `degrade-to-warn` (결과 폐기 → `pre_humanize.attempt1` backup 으로 PDF 렌더 + 워터마크, 진행은 막지 않음, `skills/ic/SKILL.md`). blue/green Caddy 는 한 컬러 down 시 다음 요청을 살아있는 컬러로 자동 라우팅 (graceful reload). 의존 부재가 hard-fail 을 일으키면 FAIL. | | 21 | **schema evolution 하위호환** (옛 artifact 읽힘) | `schema_version` 을 올린 새 코드로 **이전 버전 artifact** 를 read/list | 옛 row 가 그대로 역직렬화되어야 함 — index artifact 는 `schema_version integer NOT NULL DEFAULT 1` (`migrations/20260526210000_initial_artifact_schema.up.sql`) + back-compat shim (`artifact.rs`: 기존 caller 는 default provenance `skill="manual"`) + seed 가 "schema 가 실데이터와 backward-compatible" 임을 증명 (`seed.rs`). migration 후 옛 artifact 가 깨지면 FAIL. **acceptance gate**: index 는 `cargo test` (seed roundtrip + 23 live seed 역직렬화) 를 배포 게이트로 사용 — read tool 변경 시 이 gate 통과 필수. | > **#17–#21 운영 모니터 (배포 후 24h)**: idempotency 위반 (중복 ledger) · fragile citation 급증 · 미해결 conflict · degrade-to-warn 발동 빈도 · schema 역직렬화 실패는 **status board 지표**다 (index `/index` status page 의 `artifact_event` append-only count + `citation_durable/fragile` + `conflict` 카운트). 첫 24h 동안 fragile 비율 · conflict 건수 · degrade 발동을 관찰하고, 비정상 spike 면 promote 보류. read-only MCP (matrix) 는 write tool 이 없어 #17·#19 비해당, #18·#20·#21 도 노출 도구 한정으로 N/A 처리 가능. --- ## 9. 디버깅 — 첫 번째 행동 증상이 무엇이든 **이것부터**: ```bash docker logs SERVICE-mcp --since 5m 2>&1 | grep -iE "401|reason|warning|error|exception|task group|invalid" | head -30 ``` 그 다음 proxy: ```bash docker logs axe-SERVICE-mcp-proxy --since 5m 2>&1 | grep -E '"status": [45]' | tail -20 ``` 응답 body 가 진단 정보 들어있는지 확인: ```bash curl -X POST https://axe.axelabs.ai/SERVICE/mcp \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxfQ.invalid" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' | jq .error ``` `.error.message` 와 `.error.context.reason` 이 모두 노출되어야 정상 (`§ 4.2 ②` 통과). --- ## 10. 양파껍질 회고 (Blueprint MCP, 2026-05-21) 5 단계 모두 frame 코드에 byte-by-byte 미러했더라면 0 단계. | 차단 | 메시지 | 원인 | Fix | |---|---|---|---| | 1 | "integration may not be available" (500) | `httpx.Timeout(connect, read)` 2-arg | 4-arg form | | 2 | (모든 401 이 같은 메시지) | `_unauthorized` body 가 reason 무시 | reason → body + log | | 3 | "rejected the credentials" (401) | `audience=app_id_uri` 단일 | `audience=[client_id, app_id_uri]` | | 4 | (auth 통과 후 첫 호출) `Task group not initialized` | Starlette Mount 가 FastMCP lifespan 미전파 | `lifespan=inner.router.lifespan_context` wrap | | 5 | 421 `Invalid Host header: axe.axelabs.ai` | DNS rebinding guard | `mcp.settings.transport_security.allowed_hosts` env | | 6 | "Authorization with the MCP server failed" (claude.ai UI, 종일 broken, D-bp-mcp-3 2026-05-22) | D-config-17 Postgres cutover 가 `.env` 의 `DATABASE_URL="file:./data/blueprint.db"` SQLite legacy line 안 지움 → mcp 가 받음 → SQLAlchemy `ArgumentError` deep in ASGI → 모든 Bearer 요청 silent 500 → claude.ai 는 일반적 OAuth fail 로 표시 → 운영자가 secret 만 의심 | (a) `.env` 의 stale `DATABASE_URL` line 제거 (compose default `${DATABASE_URL:-postgresql://...}` 적용), (b) `config.get_database_url()` fail-fast on non-postgres URL, (c) Starlette lifespan 에 `SELECT 1` startup probe → DB 미접속 시 healthcheck fail → blue/green swap 거부 | **총 6 PR (#331~#335, #346)**, 약 2 시간. 같은 시간에 frame 1:1 copy + Postgres cutover env audit 했다면 30 분. **Lesson 항구화**: 1. 새 MCP 서버 = `cp -R frame/src/frame /tmp/NEW` 부터. 2. 첫 PR 부터 `_unauthorized` reason 노출 — 진단 도구가 우선. 3. 등록 시도 시 claude.ai 의 generic 메시지 ↔ 컨테이너 로그 매핑 표 (`§ 7`) 참조. 4. **DB 종속 service 는 startup probe 필수** — lifespan 에서 `SELECT 1` 호출, 실패 시 raise → healthcheck fail → blue/green swap 거부. silent runtime 500 보다 명시적 부팅 실패가 훨씬 안전. 5. **env_file 잔재 검사** — 매 service launch 전 `docker exec SERVICE printenv DATABASE_URL` 가 정확한 형식인지 § 8 #15~16 으로 확인. --- ## 참고 - frame 구현: `/Users/axe/frame/src/frame/{auth_oidc.py, mcp/http_server.py}` - hive 구현: `/Users/axe/hive/src/hive/{auth_oidc.py, mcp/http_server.py}` - blueprint 구현: `/Users/axe/blueprint/mcp/src/blueprint_mcp/{auth_oidc.py, mcp/http_server.py}` (2026-05-21 standalone) - index 구현 (skill-integration 레이어 #17–#21): `/Users/axe/index/src/{artifact.rs, error.rs, html.rs, seed.rs}` + `migrations/20260528000001_shared_schema.up.sql` (idempotency_record) — [/services/index](../services/index) - 인증 흐름: [/architecture/auth](../architecture/auth) - 비밀 매니페스트: [/architecture/secrets](../architecture/secrets) (D-ops-17) - 결정 기록: [/ops/decisions](../ops/decisions) --- # PARA OS — 일하는 방식과 지식의 구조 > Blueprint 의 PARA 는 파일 정리법이 아니라 조직론. Area(영속 기능) × Project(임시 TFT) matrix org 를 substrate 로, 지식이 claim·judgment·document 로 흐른다. dispatch="집"(link) 모델, governance 결정로그+결재워크플로, 죽은 딜 본질. URL: https://docs.axelabs.ai/architecture/para-os # PARA OS — 일하는 방식과 지식의 구조 Blueprint 의 PARA 는 파일 정리법이 아니라 **조직론**이다. Area(영속 기능) × Project(임시 TFT) 의 matrix org 를 substrate 로 삼고, 그 위에 지식이 흐른다. 도메인 서비스(frame/hive/index)는 성숙한 Area 가 물질화한 것, Blueprint 는 모든 Area 의 기본 substrate + 가로 결정로그. > 본 페이지 = 설계 **철학·원칙** (방향). 저장·citation·schema 구현 세부는 [/architecture/artifacts](/architecture/artifacts), 결정 이력은 [D-bp-para-1](/ops/decisions). 일부 항목은 **열린 질문** (§10) — 확정 시 decisions 등재. SoT 메모 = `project_para_ai_os_philosophy.md` (Blueprint global memory). ## 1. PARA = 조직론 | Layer | 조직론적 정체 | 시간성 | 예 | |---|---|---|---| | **Project** | cross-functional TFT (동적 조직) — 목표 향해 모였다 끝나면 흩어짐 | 임시 | 딜, 펀드 결성, 유상증자, 분쟁 조정 | | **Area** | 영속 기능 (정적 조직) — 통상의 부서 | 영속 | Finance · HR · Investment · Legal · IR · BOD | | **Resource** | 일하는 방법·틀 — Project·Area 수행의 지식 기반 | 재사용 | DD 체크리스트, IC 템플릿, 섹터 프레임, 회사소개서 | | **Archive** | 식은 이력 — 적극 탐색 불요 | 냉동 → 폐기 | 죽은 딜, 종료 프로젝트 | Project 와 Area 는 matrix org 의 두 축이다 — **정적 기능 × 동적 태스크포스**. Project 가 전사에 던져지면 참여자가 emerge 하므로 "살아있는 팀"이자 "컨테이너"다 (둘 다). ## 2. 기하학 — Area 가 척추, R/A 는 Area 안 PARA 는 4 개 평등한 형제가 아니다. **Area 가 척추**이고, Resource 와 Archive 는 *각 Area 안에* 매달린다. Project 만 Area 들을 가로지른다. ```text Area (영속 기능) ── 척추 ├─ Live state 서비스화(frame/hive/index) | 미졸업(Blueprint artifact) ├─ Resource 그 기능의 방법·틀 (hot) └─ Archive 그 기능의 식은 이력 (cold) Project (TFT) ── Area 들을 가로지르는 동적 레이어 ``` ## 3. MCP = 물질화된 Area + 졸업 경로 도메인 서비스는 추상 서비스가 아니라 **Area 를 AI-native 로 물질화한 것**이다. frame = Finance Area, hive = HR Area, index = Investment Area. "폴더+artifact 만으로 Area 관점이 부족 → 개별 서비스화 + DB 적재로 UX 극대화" 가 그 동기. - **기본 = Blueprint artifact** (인큐베이터). 새 테넌트·새 Area 는 전부 여기서 시작. - **졸업 = 전용 MCP 서비스**. 트리거 = 저장이 아니라 **도메인 로직/operation + AI 상호작용** (frame=복식부기·마감, hive=급여계산, index=IRR). 단순 저장이면 Blueprint 로 충분. ## 4. Blueprint = 모든 Area 의 기본 substrate | 층위 | 미졸업 / 무서비스 Area | 졸업 Area (frame/hive/index/gate) | |---|---|---| | Live state | Blueprint artifact | 서비스가 소유 | | Resource | Blueprint | **서비스가 자기 도메인 Resource 소유** (index = DD 체크리스트·섹터 프레임·IC 템플릿) | | Archive | Blueprint (cold) | **서비스가 자기 도메인 Archive 소유** (index = 죽은 딜) | | 결정 로그 | gate SoR · Blueprint citation mirror (가로) | gate SoR · Blueprint citation mirror (가로) | → **Blueprint = 도메인 서비스가 *안 덮는* 것의 substrate**: (a) 미졸업·전용 서비스 없는 Area 의 live + R/A, (b) **cross-service 종합물**(보드팩·cross-domain 메모 — 각 서비스로 citation), (c) PARA fabric(workspace/dispatch/home·link) + **gate.decision mirror**(가로). 졸업한 서비스(index 등)는 자기 도메인 지식(**죽은 딜·섹터 프레임 포함**)을 *가져간다* — moat(죽은 딜 복리) = index, Blueprint = 토대 + 연결 조직. ## 5. 저장 substrate — 저장소(SoR) vs 검색 index 구분 벡터 DB·임베딩·rerank 은 **저장소가 아니라 검색 index**다. 진실 바이트가 아니라 원본 가리키는 임베딩만 둔다. 그래서 "OneDrive 냐 Pinecone 이냐" 는 양자택일이 아니라 "OneDrive(SoR) + 벡터 index". | 대상 | 저장 (SoR) | 검색 | |---|---|---| | typed Area live (예: frame 잔액) | 서비스 DB | typed 쿼리 (벡터 불요) | | Resource (hot) | 파일 / artifact | **벡터 index** (의미검색) | | Archive (cold) | 싼 cold 보관 | 거의 안 함 — 폐기 전 *추출 지식만* 영구·queryable | ## 6. Dispatch = "집(home)" 모델 > **move 냐 copy 냐 link 냐 — 이 고민은 폴더가 강요한 가짜 3지선다다.** 폴더는 파일을 물리적으로 한 곳에만 둔다. 그래서 "투자계약서가 딜 소속이냐 Legal 소속이냐" 에 좋은 답이 없다. 전환: **artifact 는 몸통이 하나, 집은 여럿(link).** dispatch = 바이트 이동이 아니라 *집 배정*이다. | 산출 종류 | 동작 | 예 (실제 워크스페이스) | |---|---|---| | 소유·소모성 | **move → Archive → 폐기** | LoI 초안 v1~v5, dataroom 원본, 회의 스크래치 | | Area 영구 귀속 | **link / 정착** (canonical 은 Area, Project 는 참조) | 인감·고유번호증·통장(펀드 entity), 투자계약서(Legal), 서명 NDA, 최종 LoI, IRR claim(index) | | 재사용 지식 | **copy-curate → Resource** (복제 아닌 재사용본 저작) | 섹터 경쟁분석, 펀드 결성 템플릿(1호→2호), 리턴모델 방법론 | 즉 **link 가 기본**(다중 집), move 는 소모성 working 파일만, copy 는 재사용본을 *새로 저작*할 때만. 이는 [D-bp-entity-2](/ops/decisions) 의 copy-with-provenance default 를 **link-default 로 revise** 한 것. cross-functional 분배는 별도 동작이 아니다 — Project 가 가로질러서 산출이 *원래부터* 여러 Area 소속이다. ## 7. Governance — 결정로그(record) vs 결재워크플로(process) 지식뿐 아니라 **결정**도 1급이다. 둘을 가른다: - **결정 로그 (record)** — 무엇이·누가·언제·무슨 권한으로·뭘 근거로 결정됐나. append-only, 불변(수정 아닌 **supersede**), 모든 Area 가 cite. **record SoR = gate `decision` 테이블**(서비스가 SoR 소유) · **Blueprint = `gate.decision` citation mirror(가로 거버넌스)** — [D-gate-2](/ops/decisions) 로 refine(원래 "Blueprint core primitive" → service-owns-SoR + visibility-gated mirror, standalone 가능). frame 의 `register_resolution` / `query_effective_resolutions` / `link_journal_to_resolution` 이 회계 범위 결정로그 — 이를 가로로 일반화. - **결재 워크플로 (process)** — 기안 → 결재선(DOA) → 승인/반려 → 시행 = 한국형 전자결재. 그 위 policy-driven·agent-assisted 레이어. 워크플로 완료 → 결정로그 한 줄 방출. **서명은 한 종류**다: 내부 sign-off = 외부 e-sign = "행위자가 결정을 시점에 (법적으로) 확정". 서명은 결정 파이프라인의 **actuator** 로 실행되고, 봉인 결과를 결정에 link 한다. **Area 서비스 = 결정 파이프라인의 actuator** — 승인된 결정 → `frame.post_journal`/`index.register_deal` 호출, 결과를 결정에 link(앞으로) + 도메인 변경이 승인 결정을 cite(뒤로). 통합 구현 = [backlog B-bp-decision-pipeline-esign](/ops/backlog). > > **(2026-06-07 갱신 · [D-gate-5](/ops/decisions), [D-gate-3](/ops/decisions) e-sign 입장 supersede)**: 직접 서명엔진 build 는 *defer 되지 않고 당겨져 **구현**됐다* (암호등급). gate Phase-1 = **gate-native `esign.gate` actuator** — 시행 시 승인된 결정의 canonical bytes 를 **CAdES-BES → RFC3161 → CAdES-T** 로 server-seal(단일 서버 서명 identity, RSA-2048+SHA-256), append-only `esign_seal` evidence(전자문서법 §5 custodian) 저장 + `verify_seal` 검증. 법적 근거 = 전자서명법 2020 사서명(실지명의 = gate auth · 서명의사 = 결재 승인). 모두싸인 = optional alt provider 로 강등. **잔여**: 외부 서명자 본인확인기관(정보통신망법 §23-3) = 진짜 Phase-2; HSM(trait ready)·PAdES-PDF·prod-key = 하드닝. red-team 이 직접-build 를 막았던 P0(HSM·HTML→PDF 결정성·custody)는 해소-또는-명시(canonical bytes 결정성 now / HSM abstraction ready / custody = esign_seal + TSA). 상세 = [/architecture/governance](/architecture/governance) §6. > 한국 시장에서 전자결재 + e-sign 은 **table-stakes** — 테넌트가 당연히 기대한다. internal 편의가 아니라 product requirement. ## 8. 본질 — 죽은 딜을 저장한다 live deal 관리는 누구나 한다 (moat 아님). **VC 지식의 압도적 대부분은 죽은 딜에 있다** — 시장맵·경쟁분석·섹터 thesis·valuation, 그리고 무엇보다 **judgment("왜 패스했나")**. 통상 이게 Archive 에 묻혀 증발한다. AI-native fund 가 **죽은 딜 지식을 재사용 Resource + queryable judgment corpus 로 harvest** 하면 복리로 쌓인다 ("이 섹터 딜 N 건 봤고 패턴은 이렇다"). 그래서 Archive 모델이 정밀해진다: **raw 파일은 싸게 폐기, 추출 지식(judgment + 큐레이션된 섹터 Resource)은 영구·queryable.** 폐기 전 harvest 가 §6 dispatch 의 copy-curate 가 존재하는 *이유*다. 이것이 Investment Area 서비스(index)가 invested 딜뿐 아니라 **passed/dead 딜을 1급으로 저장**해야 하는 근거. ## 9. 관통 원칙 - **종류별로 가른다** — 한 상자에 여러 종류를 욱여넣으면 탁해진다. copy/link, Archive/Area, dispatch 가 다 그 증상이었다. 조직론 축(Area/Project/Resource/Archive) + 인식론 축(claim/judgment/document)으로 분해하면 녹는다. - **claim · judgment · document** — claim(관측: source-cited, live, 갱신) · judgment(판단: author, 불변·supersede) · document(산출: 둘의 구성). - **link 가 기본, 단일 출처** — copy 는 재사용본 저작에만. - **결정은 불변 로그** — process(결재)가 record(결정로그)를 낳는다. - **신규 코드 = Rust** (2026-06-06 사용자 지침). ## 10. 열린 질문 — 처리 현황 | # | 질문 | 상태 (2026-06-06) | |---|---|---| | 1 | 2-level scope (personal efforts + shared Areas) | **확정: personal + shared** ([D-bp-rust-1](/ops/decisions) — artifact `scope` 컬럼) | | 2 | Governance servicization — Blueprint core 결정로그 vs 독립 서비스 | **확정: 독립 `gate` 서비스 + Blueprint citation mirror** ([D-gate-1](/ops/decisions)·[D-gate-2](/ops/decisions), §7) | | 3 | Project staffing ("던지면 emerge") 메커니즘 | **후속 defer** ([D-bp-rust-1](/ops/decisions) — Project=컨테이너 + WorkspaceMember, emerge UX 는 후속) | | 4 | Rust 적용 범위 | **확정: Blueprint 백엔드 점진 Rust 전환(strangler-fig), substrate=첫 organ, 프론트는 TS 유지** ([D-bp-rust-1](/ops/decisions)) | **구현 토폴로지** (§4 substrate 가 *어떻게* 사느냐): PARA substrate 는 Blueprint 의 **첫 Rust organ** 으로, `blueprint-postgres` 의 `substrate` schema 를 소유하는 sidecar(axum)다 — frame/hive 식 독립 product-service 가 아니라 `axe ship blueprint` 한 경로 안의 내부 organ ([D-bp-artifact-4](/ops/decisions) "monolith first·신규 인프라 0" 을 *언어만 Rust* 로 refine). 상세 = [D-bp-rust-1](/ops/decisions) + ADR `docs/adr/blueprint-rust-migration.md`. ## 관련 - [/architecture/artifacts](/architecture/artifacts) — Artifact · Citation 저장/스키마 구현 세부 (본 페이지의 field-level 토대) - [/architecture/governance](/architecture/governance) — gate 결정 거버넌스(결재 + e-sign + 법무) 상세 + Phase-1/2 경계 ([D-gate-1](/ops/decisions)) - [D-bp-para-1](/ops/decisions) — 본 페이지의 결정 등재 ([D-bp-entity-2](/ops/decisions) copy→link revise) - [backlog B-bp-decision-pipeline-esign](/ops/backlog) — 결재 + e-sign 통합 서비스화 --- # 플랫폼 신원 (Blueprint = OIDC Provider) > Blueprint 가 Entra 를 federate 하고 플랫폼 토큰을 발행 — 로그인 1회로 전 서비스. (D-axe-idp-1 — Phase 1+2 LIVE) URL: https://docs.axelabs.ai/architecture/platform-identity # 플랫폼 신원 — Blueprint = OIDC Provider > **상태**: **Phase 1+2 LIVE** (2026-06-04). Blueprint OP(discovery·jwks·authorize·token) + RS256 서명키(vault `blueprint/axe/oidc-signing-key`) + `axe login` loopback-PKCE + **frame·hive·cortex·index·matrix + Blueprint 자체 MCP — 6개 서비스 전부 Blueprint 토큰 신뢰** (영속 설정, e2e 검증). `axe login` → `axe tools` GREEN. 본 페이지는 [D-axe-idp-1](/ops/decisions) 설계 SSOT 이자 현행 구현 기준. > ⚠️ **발행자 자신이 마지막 resource server였다** (2026-06-04 fix): Blueprint MCP 는 토큰을 *발행*하면서도 *검증*은 Microsoft Entra 경로만 알아, 자기 플랫폼 토큰을 `unknown_kid` 로 401 했다 (kid = Blueprint OIDC 서명키, Microsoft JWKS 에 부재). frame 의 `auth_blueprint.py` + iss-dispatch 를 미러해 Blueprint MCP 도 자기 issuer 의 resource server 로 배선 (`BLUEPRINT_ISSUER=https://blueprint.axellc.com`). 교훈: OP 를 세울 때 **그 OP 의 자체 MCP 도 resource-server 목록에 포함**해야 한다. > 비파괴 cutover 유지 (`BLUEPRINT_ISSUER` 미설정 시 서비스는 종전 Microsoft 경로). **인가 중앙화**(entity grant 토큰 삽입)는 여전히 Phase 3 — 현재는 인증만 Blueprint, 인가는 각 서비스 customers.yaml. ## 본질 오늘 외부/멀티에이전트 접근의 마찰은 **서비스마다 따로 인증**한다는 데서 온다. 직원·에이전트가 frame·hive·index·cortex·matrix 를 쓰려면 서비스별 MCP OAuth 를 각각 통과하고, frame·cortex 는 각자 [OAuth-RP 프록시](/architecture/auth) (D-ops-14/15) 를 따로 운영한다. 신원은 **N 곳에 흩어져** 있다. **목표 한 줄**: 사람은 **한 번** SSO 로그인하고, 그 결과로 받은 **하나의 플랫폼 토큰**으로 **모든 서비스**를 쓴다. 거버넌스(누가·어떤 스코프·취소·감사)는 **Blueprint 한 곳**에 모인다. 그 한 곳이 **Blueprint** 인 이유: - Blueprint 는 **이미 Entra 를 federate** 한다 (NextAuth Azure AD provider, [`src/lib/auth.ts`](/architecture/auth)). 신규 IdP 를 세우는 게 아니라 **기존 세션 위에 토큰 발행만 얹는다**. - Blueprint 는 **이미 per-user 권한의 권위자**다 — `entityScopes` · `EntityRole` · `getEntityRolesForUser` 를 들고 있고, frame/hive 가 `/api/internal/entity-roles` 로 **이미 그걸 물어본다**. 토큰 거버넌스의 자연스러운 자리. - Blueprint 는 **control plane** (구동 시스템). 서비스가 신뢰할 단일 발행자로 토폴로지상 맞다. ## 현재 상태 — 검증된 인증 표면 (2026-06-03 실측) 각 서비스가 incoming 토큰을 검증하는 방식. "Blueprint 신뢰" 추가 시 바꿀 지점과 난이도를 함께 표기. | 서비스 | 스택 | validator (file) | iss-dispatch seam | HS256 경로 | "Blueprint 신뢰" 난이도 | |---|---|---|---|---|---| | **frame** | Python FastMCP | `src/frame/mcp/http_server.py:438` | ✅ `_is_microsoft_iss` → else HS256 | ✅ `FRAME_JWT_SECRET` | **낮음** — `elif _is_blueprint_iss` 한 가지 추가 | | **hive** | Python FastMCP (frame fork) | `src/hive/mcp/http_server.py:250` | ✅ `_is_microsoft_iss` → else HS256 | ✅ `HIVE_JWT_SECRET` | **낮음** — frame 과 1:1 동일 패턴 | | **cortex** | Rust axum | `src/auth.rs:214` | △ HS256 short-circuit → RS256 (MS hardcoded) | ✅ `CORTEX_JWT_SECRET` (배선됨) | **중간** — issuer 파라미터화 | | **index** | Rust axum | `src/auth.rs:214` | ✗ RS256(MS) hardcoded, fallback 없음 | △ `INDEX_JWT_SECRET` 정의만·**미배선** | **중간** — issuer 파라미터화 + HS256 배선 | | **matrix** | Rust axum (custom) | `src/auth.rs:17` | ✗ HS256 단일, iss 미검증 | ✅ `MATRIX_JWT_SECRET` 만 | **높음** — async refactor + RS256 + JWKS | | **blueprint (자체 MCP)** | Python FastMCP | `mcp/src/blueprint_mcp/mcp/http_server.py` | ✅ iss-dispatch 추가됨 (2026-06-04) — Blueprint vs Microsoft | ✗ HS256 경로 없음 (Microsoft + Blueprint 만) | **낮음 (완료)** — frame `auth_blueprint.py` 미러. *발행자 본인이라 누락됐던 행* | **핵심 발견**: - frame·hive 는 이미 **iss 로 verifier 를 고르는 dispatch seam** 이 있다 — Blueprint 분기는 한 줄. Phase 1 첫 타깃 = **frame** (seam + `auth_oidc.py` JWKS 머신 재사용 + 이미 CLI GREEN 검증됨). - Blueprint 측 OP 머신(`SignJWT`/JWKS endpoint/authorize/token)은 **전무** (greenfield). 단 `jose` 는 이미 dependency 이고 검증측 `createRemoteJWKSet` 패턴이 `src/lib/mcp-microsoft-rp.ts` 에 존재. - customers.yaml 의 tenant 는 **고객사별로 다름** (realchoice = 별도 Entra tenant). → **OIDC-OP 는 per-deployment**: 각 고객 Blueprint = 자기 Entra 를 federate 하는 자기 issuer. AXE 자체 issuer = `https://blueprint.axellc.com`. ## 목표 아키텍처 ``` ┌─────────────────────── 사람 SSO 1회 ───────────────────────┐ │ ▼ axe login (CLI) ┌──────────────────────┐ loopback PKCE │ Microsoft Entra ID │ │ │ (customer tenant) │ │ 1. open browser /oauth/authorize └──────────┬───────────┘ ▼ │ NextAuth (기존) ┌───────────────────────── Blueprint = OIDC Provider ─────────────┴──┐ │ GET /.well-known/openid-configuration (discovery) │ │ GET /.well-known/jwks.json (공개키 — 서비스가 fetch) │ │ GET /oauth/authorize getServerSession 재사용 → code (PKCE) │ │ POST /oauth/token code+verifier → 플랫폼 JWT(RS256)+refresh │ │ POST /oauth/register DCR (loopback + claude.ai allowlist) │ │ POST /oauth/revoke refresh 취소 │ │ 거버넌스: scope · EntityRole · 발행 audit · per-user/tenant │ └───────────────────────────────┬───────────────────────────────────┘ 2. 플랫폼 토큰 (keychain) │ iss=blueprint, aud=platform, RS256 ▼ ▼ (서비스는 Blueprint JWKS 로 검증) ┌──────────┐ Bearer ┌────────┬────────┬────────┬────────┬────────┐ │ axe CLI │ ───────► │ frame │ hive │ index │ cortex │ matrix │ └──────────┘ 하나의 └────────┴────────┴────────┴────────┴────────┘ 토큰으로 전 서비스 iss=blueprint 분기 → Blueprint JWKS RS256 검증 ``` **원칙**: 1. **Entra federation 은 재사용** — `/oauth/authorize` 가 `getServerSession(authOptions)` 로 기존 NextAuth 세션을 확인. 세션 있으면(=이미 Entra SSO 됨) code 발행, 없으면 `/login` 으로 bounce 후 복귀. Blueprint 가 Entra 와 직접 서버-투-서버 code 교환을 다시 짤 필요 없음 (frame `oauth.py` 의 `/oauth/callback` 단계가 Blueprint 에선 불필요 — 이게 frame 대비 단순화). 2. **하나의 토큰, 전 서비스** — `aud` = 플랫폼 audience 단일값. 서비스별 audience juggling 없음. 서비스 권한은 **scope** 가 가른다. 3. **서비스는 서명만 신뢰** — 각 서비스는 `iss=blueprint` 분기에서 Blueprint JWKS 로 RS256 검증. Phase 1 에선 **인증**만 Blueprint 로 (email vouch); **인가**(email→entity) 는 각 서비스 customers.yaml 그대로 → 비파괴. (entity grant 를 토큰에 심는 **인가 중앙화**는 Phase 3.) ## OIDC-OP 엔드포인트 (Blueprint Next.js 에 신설) 모두 `https://blueprint.axellc.com` (= `BLUEPRINT_OIDC_ISSUER`, 기본 `NEXTAUTH_URL`) origin 에 App Router route handler 로 추가. 전부 **공개**(인증 면제) — 단 authorize 는 세션을 요구. | 엔드포인트 | 메서드 | 신규 파일 (제안) | 역할 | |---|---|---|---| | `/.well-known/openid-configuration` | GET | `src/app/.well-known/openid-configuration/route.ts` | RFC 8414 metadata (issuer, endpoints, jwks_uri, S256, RS256) | | `/.well-known/jwks.json` | GET | `src/app/.well-known/jwks.json/route.ts` | 공개키 (서비스·CLI 가 RS256 검증) | | `/oauth/authorize` | GET | `src/app/oauth/authorize/route.ts` | 세션 확인(getServerSession) → 단발 code 발행 (PKCE S256 필수) | | `/oauth/token` | POST | `src/app/oauth/token/route.ts` | `authorization_code` + `refresh_token` grant → 플랫폼 JWT | | `/oauth/register` | POST | `src/app/oauth/register/route.ts` | RFC 7591 DCR (loopback + claude.ai redirect allowlist) | | `/oauth/revoke` | POST | `src/app/oauth/revoke/route.ts` | refresh token 취소 (governance) | | `/oauth/device_authorization` | POST | (Phase 3) | RFC 8628 device-code (headless) | frame [`oauth.py`](/services/frame) 가 **검증된 레퍼런스**다 (PKCE 단발 code·atomic 단일소비·S256 constant-time·DCR allowlist 전부 구현). Blueprint OP 는 그것을 그대로 따르되 **차이점 3 가지**: 1. HS256 대신 **RS256 서명** + JWKS 공개 (서비스가 secret 공유 없이 검증). 2. Microsoft 서버-투-서버 교환 단계 **제거** → 기존 **NextAuth 세션** 재사용. 3. **refresh token** 추가 (CLI UX "로그인 1회 후 한동안 유지" + 취소 지점). ## 플랫폼 JWT (access token) shape RS256, `kid` 헤더. 클레임: ```jsonc { "iss": "https://blueprint.axellc.com", // BLUEPRINT_OIDC_ISSUER (per-deployment) "sub": "", // User.entraOid — 안정적 cross-app 식별자 "email": "ai@axellc.com", // 소문자 정규화 "aud": "https://axe.axelabs.ai", // 플랫폼 audience (단일; 전 서비스 공통) "scope": "openid profile email frame hive index cortex matrix", "azp": "axe-cli", // 토큰을 받은 client_id (감사) "jti": "", // 감사 + (선택) 취소 denylist "iat": 1730000000, "exp": 1730003600, // 1h "ent": { "axec": ["read","write"], "axev": ["read"] } // Phase 3 (선택) — 현재는 서비스가 무시 } ``` - **access token**: 짧음 (1h). 만료 시 refresh 로 무중단 갱신. - **refresh token**: opaque 랜덤 (JWT 아님), 서버측 저장(Prisma), **회전**(rotating — 사용 시 새 발급+구 폐기), 30d. → 취소·도난탐지 지점. - **scope**: Phase 1 = 서비스 단위 grant (존재 = 그 서비스 접근). frame 의 entity별 read/write 인가는 customers.yaml email→entity 가 그대로 담당. (`frame:read` 같은 fine scope + `ent` 클레임 인가중앙화 = Phase 3.) - **aud 단일값의 트레이드오프**: 한 토큰이 전 서비스 → 토큰 유출 시 blast radius 가 플랫폼 전체. 완화 = 짧은 exp(1h) + scope + refresh 취소 + loopback-only public client + PKCE 필수. (per-service aud 격리보다 편의를 택한 D-axe-idp-1 의 의도적 선택.) ## 서비스별 신뢰 이전 (trust migration) 각 서비스는 **feature flag** 로 Blueprint 신뢰를 켠다 — `BLUEPRINT_ISSUER` + `BLUEPRINT_JWKS_URL` (+ `BLUEPRINT_AUDIENCE`) **unset = 현행 동작 그대로** (비파괴·롤백 = env 제거). ### frame · hive (Python — 낮음, Phase 1·2) `http_server.py` 의 dispatch 에 분기 한 줄 + verifier 모듈 신설: ```python # http_server.py — 기존 if _is_microsoft_iss(iss): ... else: (HS256) 사이에 삽입 elif _is_blueprint_iss(iss): # iss == BLUEPRINT_ISSUER payload = await verify_blueprint_token( # 신규 auth_blueprint.py agent_token, jwks_url=BLUEPRINT_JWKS_URL, issuer=BLUEPRINT_ISSUER, audience=BLUEPRINT_AUDIENCE, ) email = extract_email(payload) agent_claims = resolve_subject_to_claims(payload, email, customer_id=cid) # 기존 재사용 ``` `verify_blueprint_token` = `auth_oidc.py` 의 JWKS fetch/cache(`createRemoteJWKSet` 대응, 1h TTL·kid-miss 강제갱신)를 그대로 미러, issuer/aud/exp 검증 후 payload 반환. email→entity 는 **기존 `resolve_subject_to_claims` 무수정 재사용** → 인가 모델 불변. ### cortex · index (Rust — 중간, Phase 2) `auth.rs:verify_token` 의 단일 MS issuer hardcode 를 issuer-dispatch 로 리팩터: ```rust let iss = peek_iss(token)?; // unverified payload iss match trusted_issuer(&iss, &state.settings) { Issuer::Microsoft => verify_rs256(token, ms_jwks, ms_iss, ms_aud).await, Issuer::Blueprint => verify_rs256(token, bp_jwks, bp_iss, bp_aud).await, // 신규 Issuer::Self_ => verify_hs256(token, secret), // cortex 기존 / index 는 여기서 배선 _ => Err(Unknown), } ``` `verify_rs256(token, jwks, iss, aud)` 헬퍼로 추출하면 MS·Blueprint 가 같은 코드 재사용. index 는 이참에 `INDEX_JWT_SECRET` HS256 도 배선(cortex `oauth_as.rs` 패턴 복사). ### matrix (Rust custom — 높음, Phase 2 후) HS256 단일·동기 미들웨어 → ① async 화 ② iss peek ③ Blueprint RS256 분기 ④ JWKS fetch(reqwest 이미 보유) ⑤ `Claims` 에 `iss`/`aud` 필드 추가. ~80–120 LOC. seam 없음 → 마지막. | 서비스 | flag env | 작업량 | Phase | |---|---|---|---| | frame | `BLUEPRINT_ISSUER`,`BLUEPRINT_JWKS_URL`,`BLUEPRINT_AUDIENCE` | ~80 LOC + tests | **1** (모델 증명) | | hive | 동일 | ~60 LOC (frame 복사) | 2 | | cortex | `BLUEPRINT_*` (settings) | refactor + ~80 LOC | 2 | | index | `BLUEPRINT_*` + `INDEX_JWT_SECRET` 배선 | refactor + ~100 LOC | 2 | | matrix | `MATRIX_BLUEPRINT_*` | async refactor ~120 LOC | 2 | | blueprint (자체 MCP) | `BLUEPRINT_ISSUER`,`BLUEPRINT_AUDIENCE` (JWKS 파생) | ~150 LOC + 10 tests (frame 미러) | **완료 (2026-06-04)** | > 위 표가 frame·hive·cortex·index·matrix 만 세고 **Blueprint 자체 MCP 를 빠뜨린 것**이 2026-06-04 의 401 버그 근원이었다 — issuer 가 곧 resource server 라는 사실이 자명해 보여 목록에서 누락됐다. 새 OP 를 세울 땐 *그 OP 의 MCP 도* 이 표의 한 행이다. ## `axe login` — loopback PKCE (CLI) [AXE CLI](/services) 에 `axe login` (인자 없음) 추가. stdlib 만 (`http.server`·`webbrowser`·`hashlib`·`secrets`·`urllib`): ``` 1. CLI: 랜덤 loopback 서버 기동 http://127.0.0.1:/callback 2. CLI: POST /oauth/register {redirect_uris:[loopback]} → client_id (또는 정적 axe-cli) 3. CLI: code_verifier 생성, challenge=S256, state 생성 → 브라우저 open /oauth/authorize?response_type=code&client_id=…&redirect_uri=loopback &code_challenge=…&code_challenge_method=S256&scope=…&state=… 4. 브라우저: (NextAuth 세션 없으면) Entra SSO 1회 → Blueprint 가 loopback 으로 302 ?code=…&state=… 5. CLI(loopback): code 수신, state 검증 → POST /oauth/token grant_type=authorization_code&code=…&code_verifier=…&redirect_uri=loopback 6. CLI: {access_token, refresh_token, expires_in} → keychain 저장. 브라우저엔 "닫아도 됨". ``` - **헤드리스(Codex/CI)**: `axe login --token ` / `AXE_TOKEN` env 공존 (gh 모델) — 변경 없음. 또는 Phase 3 **device-code**. - **검증은 사람 브라우저 로그인 필요** → 단독 e2e 불가, 운영자 SSO 1회로 마무리. - 토큰 자동 refresh: access 만료 시 CLI 가 저장된 refresh 로 `/oauth/token` (`grant_type=refresh_token`) 조용히 갱신. ## 거버넌스 (Blueprint 중앙) - **scope**: `openid profile email` + 서비스 grant. 발급 시 Blueprint 가 user 의 `entityScopes`/`EntityRole` 로 허용 scope 를 결정 (consent UI 는 first-party CLI 라 Phase 1 생략 가능, 외부 third-party client 도입 시 추가). - **revoke**: refresh token 서버측 저장 → `/oauth/revoke` 또는 admin UI 에서 폐기. access 는 짧아(1h) revoke latency = exp. 더 강한 즉시취소 필요 시 `jti` denylist (introspection 없이). - **audit**: 모든 발행(누가·언제·client·scope·jti)을 append-only 로그. Blueprint 의 기존 로깅/DB 재사용. - **per-tenant**: issuer 가 per-deployment 이므로 고객사 토큰은 그 고객 Blueprint 가 발행·관리 (sovereignty 정합). ## 키 관리 - RS256 keypair. private key = **vault** (`blueprint/axe/oidc-signing-key`, PKCS8 PEM 또는 JWK) → `BLUEPRINT_OIDC_PRIVATE_KEY` 로 주입. 공개키만 `/.well-known/jwks.json` 에 `kid` 와 함께. - **회전**: JWKS 에 2 키(old+new) 동시 게시 → 신규 서명은 new `kid`, 검증은 둘 다 수용 → old 만료 후 제거. 서비스 JWKS 캐시 TTL(1h) 고려. - `jose` (`generateKeyPair`/`importPKCS8`/`SignJWT`/`exportJWK`) 사용 — 이미 dependency. ## 데이터 모델 (Prisma 신설) ```prisma model OAuthAuthCode { // authorize→token 단발 code (frame oauth_authorization_codes 대응) code String @id clientId String userId String codeChallenge String redirectUri String scope String expiresAt DateTime consumedAt DateTime? } model OAuthRefreshToken { // refresh + revoke + 회전 id String @id @default(cuid()) tokenHash String @unique // 평문 저장 안 함 userId String clientId String scope String expiresAt DateTime revokedAt DateTime? rotatedTo String? // 회전 추적 (도난 재사용 탐지) createdAt DateTime @default(now()) } model OAuthClient { // 클라이언트 레지스트리 (axe-cli 정적 seed + DCR) clientId String @id name String redirectUris String // JSON 배열 type String @default("public") // PKCE-only createdAt DateTime @default(now()) } ``` ## 비파괴 cutover (신·구 병행) 보안 핵심 → **점진·가역**. 어느 단계도 기존 claude.ai MS-OAuth 흐름과 frame/cortex 프록시를 **깨지 않는다**. 1. **Blueprint OP 추가** = 전부 신규 route. 기존 동작 0 변경. 2. **서비스별 Blueprint 신뢰 추가** = iss 분기 1개 ADD. MS·HS256 경로 불변. flag(`BLUEPRINT_ISSUER`) unset 이면 무시 = 현행. → **서비스 단위로 독립 ship·롤백**. 3. **per-service 프록시 폐기는 나중** — frame/cortex 의 claude.ai DCR 프록시(D-ops-14/15)는 Blueprint OP 가 충분히 증명될 때까지 **공존**. claude.ai 커넥터 흐름 내내 유지. 통합(claude.ai 를 Blueprint OP 로 이전 + 프록시 제거)은 Phase 2 말~3. 롤백 = 해당 서비스 `BLUEPRINT_*` env 제거 후 recreate. 토큰 발행 중단 = Blueprint route 비활성. ## Phase 계획 - **Phase 1 (모델 증명) ✅ 2026-06-04**: Blueprint OP(discovery·jwks·authorize·token·register·revoke) + RS256 키(vault) + Prisma 모델 + `axe-cli` 정적 client + `axe login` loopback PKCE + **frame** Blueprint 신뢰. → 운영자 브라우저 SSO 1회로 `axe login` → `axe frame tools` GREEN 증명 완료. - **Phase 2 ✅ 2026-06-04**: hive·cortex·index·matrix trust 이전 — 5개 서비스 전부 LIVE + 영속(`BLUEPRINT_ISSUER` compose/.env.local 기본값) + e2e(`axe tools`). claude.ai → Blueprint OP 이전 + frame/cortex 프록시 폐기는 미평가(잔여). - **Phase 3**: 인가 중앙화(`ent` 클레임 + fine scope, 서비스가 customers.yaml 대신 토큰 grant 사용) + 감사 UI + headless device-code(RFC 8628, `B-axe-cli-device-code` — 미구현) + 키 회전 자동화. ## 미해결 / 결정 필요 - **issuer 도메인**: 현재 `blueprint.axellc.com` (NEXTAUTH_URL). axelabs.ai 도메인 이전 시 issuer 변경 = 발행된 토큰·서비스 신뢰설정 일괄 영향 → 이전 **전에** 확정하거나 `auth.axelabs.ai` 안정 alias 고정. - **claude.ai 통합 시점**: claude.ai 커넥터를 Blueprint OP 뒤로 옮기면 per-service 프록시 폐기 가능. 단 claude.ai DCR redirect allowlist·Mcp-Session-Id 흐름 재검증 필요 (B-axe-cli 의 public 멀티스텝 403 함정과 연동). - **인가 중앙화 범위**: entity grant 를 토큰에 심는 순간 customers.yaml 의 email→entity 가 Blueprint 로 이동 → 큰 마이그레이션. Phase 1 은 인증만, 인가는 그대로 두는 게 안전. ## 관련 - 결정: [D-axe-idp-1](/ops/decisions) (본 설계) · [D-axe-cli-1](/ops/decisions) (CLI·토큰모델) · [D-ops-14/15](/ops/decisions) (per-service 프록시 — 통합 대상) - 현행 인증: [/architecture/auth](/architecture/auth) (3 경로 — Blueprint = 4번째 trusted issuer 로 합류) - 레퍼런스 구현: frame `src/frame/mcp/oauth.py` (검증된 OAuth 2.1 AS + PKCE + DCR) - 백로그: [B-axe-idp-1](/ops/backlog) --- # Playbook 패턴 (AI session 던지기 표준 형식) > docs.axelabs.ai 의 행위 단위 페이지 (셋업/배포/공지/복구) 가 사람과 AI 양쪽의 단일 entry point 가 되도록 하는 맨 위 prompt block + 본문 구조 표준. 새 playbook 작성 시 본 SSOT 따른다. URL: https://docs.axelabs.ai/architecture/playbooks # Playbook 패턴 docs.axelabs.ai 의 일부 페이지는 단순 reference 가 아니라 **행위 단위** (셋업 / 배포 / 공지 / 복구) 다. 이런 페이지는 사람이 직접 read 해서 따라가는 것 외에, **사용자가 자기 AI session 에 URL + 짧은 prompt 를 던져 step-by-step interactive 로 진행**시키는 방식이 표준이다 ([D-docs-playbook](../ops/decisions)). > 본 페이지 = 새 playbook 작성 시 따를 형식의 SSOT. 형식 drift 가 곧 페이지마다 사용자 mental model 재학습 비용 = 패턴 가치 훼손이라 박제한다. ## 무엇이 playbook 이고 무엇이 아닌가 | 페이지 성격 | playbook? | 이유 | |---|---|---| | **셋업** (SSH 진입, vault setup, MCP connector 추가) | ✅ | 사용자가 단계 따라가야 끝남 | | **배포** (release-flow, customer onboarding D-day) | ✅ | 운영자 손에서 N 명령 순차 실행 | | **운영자 공지** (Teams DM broadcast, mail broadcast) | ✅ | 한 번에 N 사람 대상으로 행위 | | **복구** (vault recovery, postgres restore) | ✅ | 사고 시 명령 순서 + 검증 매 step | | **decision / 결정 기록** (D-ops-*) | ❌ | 사실 기록. 행위 트리거 아님 | | **아키텍처/모델** (domains, data, artifacts) | ❌ | reference. 사용자가 행위로 변환 안 함 | | **backlog / known-gaps / updates** | ❌ | 메타 운영 — 페이지가 entry point 지만 행위 단위 X | 판단 기준: "본 페이지를 끝까지 따라가면 사용자 환경 또는 외부 시스템 상태가 변하는가?" → 변하면 playbook. ## 표준 형식 ### Frontmatter ```yaml --- title: <행위 명사구 또는 짧은 페이지명> description: <한 줄 — 행위 + 외부 dependency + 완료 기준. 본 페이지 fetch 한 AI 가 prereq 판단할 수 있게 명시> playbook: true --- ``` `playbook: true` 는 machine-readable marker — 후속 build-time catalog 자동 생성 / search filter / 외부 agent 가 "행위 단위 페이지만" 골라낼 때 anchor. 본 SSOT 의 "현 playbook 목록" 표가 수동 인덱스, frontmatter 가 자동 인덱스의 단일 진실. ### Sidebar marker 해당 페이지의 `_meta.js` entry title 앞에 `⚡ ` prefix 추가 — Nextra sidebar 에서 사람이 페이지 열기 전 "이 페이지는 AI 던지기 가능한 행위 단위" 식별: ```js // content/onboard/_meta.js (예) export default { index: '신규 직원 가이드', // 안내, marker 없음 'ssh-access': '⚡ SSH 로컬 작업', // playbook 'vault-setup': '⚡ Vault setup (KDF + 4 client)', // playbook troubleshooting: '문제 해결', // reference, marker 없음 } ``` ### 본문 구조 (필수 순서) ```mdx # <행위 명사구> (선택) 1-2 문장 페이지 정체성 — 무엇을 하는 페이지인지, legacy 페이지가 있다면 cross-link. ## AI 요청 프롬프트 ​``` <표준 prompt block — 다음 절 참고> ​``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## Prereq - <권한 / 자격> - <환경 / 머신 / 네트워크> - <사전 자료 (email, key, manifest 등)> ## Step 1: <첫 단계> ## Step 2: ... (또는 단계가 크면 `## Phase 1 (...)` / `## Phase 2 (...)` 패턴) ## 함정 정리 | # | 함정 | 우회 | |---|---|---| | 1 | <증상> | <대응> | ## 완료 후 (선택) - - <후속 회신 / Ship Log / 운영자 통보> ``` ### 표준 prompt block 템플릿 코드 블록 안의 내용: ``` https://docs.axelabs.ai/ 따라 <행위 한 줄 요약> 해줘. 진행: 1. <환경/머신 진단 — OS, 기존 설치 여부, 권한> 2. <페이지 Prereq 확인 + 조건 분기 (해당 시 skip)> 3. 페이지의 각 Step 명령 실행 + 검증, 매 step 결과 받고 다음 4. 함정 발생 시 페이지 "함정 정리" 표 따라 우회 5. <완료 후 done criteria — 운영자 회신 / Ship Log / 검증 산출물> ``` **고정 element** (모든 playbook 동일): - 첫 줄 = 페이지 URL + "따라" + 행위 + "해줘" - step 3 = "페이지의 각 Step 명령 실행 + 검증, 매 step 결과 받고 다음" (낱말 그대로) - step 4 = "함정 발생 시 페이지 '함정 정리' 표 따라 우회" (낱말 그대로) **가변 element**: - step 1-2 = 행위에 따른 사전 진단 (운영자 broadcast 는 "Blueprint LIVE 확인" / SSH 셋업은 "OS 진단" 등) - step 5 = 완료 기준 (Teams 회신 / Ship Log / 운영자 통보 등) ### 표준 꼬리 (prompt block 다음 2줄) ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ``` **낱말 그대로** 복붙. AI session 후보 4 개 순서 고정 (Claude Code · Cursor · ChatGPT 데스크탑 · Claude.app · 기타). ## 현 playbook 목록 표 = [`scripts/generate-playbook-catalog.mjs`](https://github.com/axelabs-ai/axelabs-docs/blob/main/scripts/generate-playbook-catalog.mjs) 가 frontmatter `playbook: true` 페이지 인덱싱해서 marker 사이 자동 채움. `package.json` 의 `prebuild` 가 자동 hook — `npm run build` 마다 갱신. ad-hoc 호출은 `npm run catalog`. **script idempotent** (변경 없을 때 file 안 만짐) 라 매 build 마다 working tree dirty 0. **본 표 직접 편집 X** — 새 playbook 추가 = 페이지 frontmatter 만, build (또는 catalog) 한 번 → 표 + JSON 갱신 → 결과 commit. machine catalog = [public/playbooks.json](/playbooks.json) (count + URL + title + action + audience, deterministic, no timestamp). {/* playbook-catalog:start */} | Playbook | 행위 | 대상 | |---|---|---| | [AXE MCP connectors (claude.ai + Claude Code 자동 sync)](/onboard/claude-connectors) | claude.ai 의 Custom Connector 등록에 Bitwarden 브라우저 확장 + AXE Vault `MCP Connectors` collection 활용 | 직원 / customer 직원 | | [Microsoft 365 connector](/onboard/m365-connector) | OneDrive · Outlook · Teams 통합 (Anthropic 제공) | 직원 / customer 직원 | | [SSH 로컬 작업 (Claude Code / Cursor / 기타 AI session)](/onboard/ssh-access) | AXE macmini 에 SSH 진입해 로컬 작업하는 표준 절차 | 직원 / customer 직원 | | [Vault setup — KDF rotation + 4 client + Bitwarden Authenticator](/onboard/vault-setup) | AXE Vaultwarden 의 SSO→MP unlock 정상화 (KDF rotation, 옛 user 만) + 데스크톱/Chrome/Mobile 3 client + Bitwarden Authenticator 표준 setup | 직원 / customer 직원 | | [Cloudflared 재기동](/ops/runbook/cloudflared) | cloudflared 가 죽었거나 config 변경 시 5초 다운타임 절차 | 운영자 | | [신규 Customer Onboarding](/ops/runbook/customer-onboarding) | 운영자 측 자동화 + 수동 touchpoints | 운영자 | | [Frame DB 복구](/ops/runbook/db-recovery) | frame-postgres 손상/migration 사고/silent corruption 대응 | 운영자 | | [Blue/Green Deploy](/ops/runbook/deploy) | frame 무중단 배포 절차 | 운영자 | | [직원 퇴사](/ops/runbook/employee-offboarding) | 퇴사한 직원의 모든 access 차단 절차 | 운영자 | | [신규 직원 등록](/ops/runbook/employee-onboarding) | customer admin 으로부터 신규 직원 추가 요청 받았을 때 운영자 절차 | 운영자 | | [macmini 손실](/ops/runbook/macmini-loss) | customer macmini 도난/화재/완전 손실 시 1-day 복구 | 운영자 | | [운영자 broadcast — Teams DM 으로 임직원 1:N 공지](/ops/runbook/operator-broadcast) | Blueprint `/api/admin/broadcast-dm` REST 로 bot identity (ai@axellc.com) → AXE 임직원 1:N Teams DM | 운영자 | | [Release flow (axe ship)](/ops/runbook/release-flow) | 코드 변경 → 운영 반영까지의 release-gate | 운영자 | | [Secret Rotation](/ops/runbook/secret-rotation) | 모든 비밀 회전의 단일 명령 — axe secret rotate | 운영자 | | [Vaultwarden 복구](/ops/runbook/vault-recovery) | self-host vault 복구, OIDC 깨짐 대응, sso_nonce 수동 패치 | 운영자 | {/* playbook-catalog:end */} 신규 playbook 추가 흐름 = (1) 페이지 frontmatter `playbook: true` + 본문 표준 형식 + sidebar `⚡ ` prefix → (2) `npm run build` (또는 `npm run catalog`) 한 번 실행 → 본 표 + JSON 자동 갱신 → (3) git commit 묶음. ## 함정 / drift 회피 | 함정 | 우회 | |---|---| | AI session 후보 나열 순서를 자기 임의로 바꿈 (예: Claude.app 을 첫 자리) | 본 SSOT 의 4 후보 순서 (Claude Code · Cursor · ChatGPT 데스크탑 · Claude.app) 낱말 그대로 | | 꼬리 2줄을 1줄로 압축 (vault-setup·operator-broadcast 의 초기 drift) | 2줄 분리 유지 — 읽기 부담 ↓ + AI 가 첫줄 (후보 식별) 과 둘째줄 (행위 지시) 별도 cue 로 파싱 | | `## AI 요청 프롬프트` 헤딩 변형 (옛 `🤖 AI 에 던질 prompt (...)` 잔존 / 다른 단어로 paraphrase) | 낱말 그대로. 사람 + AI 둘 다 visual anchor 로 사용 — 일치 안 하면 "이 페이지 playbook 인가?" 재판단 비용 | | prompt block 안 첫 줄에 URL 안 적고 페이지 안에서 "위 페이지" 식 indirect 참조 | URL 명시 — AI 가 본 prompt 만 보고 페이지 fetch 가능해야 self-contained | | step 3-4 의 표준 문구를 paraphrase ("각 step 따라가 줘" / "함정 표 보고" 등) | 낱말 그대로 — AI 가 페이지의 `## 함정 정리` 표를 anchor 로 매핑하는 cue | | 페이지 본문에 prompt 안 step 과 실제 `## Step N` heading 의 번호 불일치 | prompt step 5 = 페이지의 마지막 행위, prompt 안 step N = 페이지 Step N 으로 1:1 매핑 권장 | ## 새 playbook 추가 체크리스트 신규 페이지가 위 "무엇이 playbook 인가" 판단으로 playbook 이면: 1. Frontmatter title + description + **`playbook: true`** 작성 (description 한 줄에 행위 + dependency + 완료 기준) 2. `## AI 요청 프롬프트` 헤딩 + code block + 표준 꼬리 2줄 복붙 — code block 안만 행위에 맞게 가변 3. `## Prereq` / `## Step N` 또는 `## Phase N` / `## 함정 정리` / `## 완료 후` 순서 — 누락 시 본 SSOT 표 한 줄로 reject 4. 본 페이지 (`architecture/playbooks`) 의 "현 playbook 목록" 표 = **script 자동 생성, 수동 편집 X** — `scripts/generate-playbook-catalog.mjs` 가 frontmatter `playbook: true` 페이지 인덱싱해서 marker 사이 표 + `public/playbooks.json` 갱신. `prebuild` 자동 hook 또는 `npm run catalog` (ad-hoc) → 결과 commit. script idempotent 라 build 마다 dirty 0 5. 해당 폴더의 `_meta.js` entry title 앞에 `⚡ ` prefix 추가 (sidebar marker) 6. 페이지가 운영자 행위면 `content/ops/runbook/`, 직원/customer 행위면 `content/onboard/` — 위치 혼동 시 sidebar 카테고리 가시성 ↓ + catalog audience 매핑 잘못 ## 관련 결정 - 본 SSOT — [D-docs-playbook](../ops/decisions) - backlog ritual — [/ops/backlog](/ops/backlog) (다른 entry point. playbook 페이지는 backlog 와 별 channel — playbook = 행위 트리거, backlog = 행위 큐) --- # 비밀 관리 — vault → 서비스 흐름 > 모든 서비스 비밀의 SoT 는 Vaultwarden. customers.yaml 매니페스트가 매핑 SSOT. axe ship 가 배포 직전 vault → env_file 동기화 강제. URL: https://docs.axelabs.ai/architecture/secrets # 비밀 관리 (D-ops-17) > AXE 플랫폼의 모든 서비스 비밀 (DB password, OAuth client_secret, JWT signing key, 외부 API token) 은 **Vaultwarden 이 canonical store**. 서비스 컨테이너는 vault 를 직접 모르고, **`axe ship` 가 배포 직전 vault → env_file 로 동기화** 한 뒤 docker compose 가 env_file 을 읽음. ## 핵심 결정 | # | 결정 | 이유 | |---|---|---| | D-ops-9 | Vaultwarden = canonical secret store | 기존 결정 — Tier-1 vault | | D-ops-17 | Deploy-time pull (runtime X) | runtime vault 호출은 blast radius + bootstrap 문제. deploy-time 만 의존성 발생, 서비스 코드는 vault 모름 | ## 데이터 흐름 ``` ┌──────────────┐ ┌─────────────────────────────────────────┐ │ Vaultwarden │ ← canonical SoT │ /Users/axe/.axe/customers.yaml │ │ (axe.axelabs │ │ services..secrets[]: │ │ .ai/vault) │ │ - env: FRAME_DB_PASSWORD │ └──────┬───────┘ │ vault: "frame/axe/db-password" │ │ │ - env: AZURE_FRAME_MCP_CLIENT_SECRET│ │ bw CLI │ vault: "frame/axe/oauth-..." │ │ (BW_SESSION │ rotation_external: azure │ │ from Keychain) └─────────────────────┬───────────────────┘ │ │ (manifest read) ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ axe ship (release-gate) │ │ step 1: docs-check │ │ step 2: branch + clean │ │ step 3: commits to push │ │ step 4: confirm │ │ step 5: git push │ │ step 6: deploy │ │ ├─ pre-deploy [a]: axe secret check ← abort 시 stop│ │ ├─ pre-deploy [b]: axe secret pull ← env_file write│ │ └─ docker compose up -d ... (env_file = pulled content) │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────┐ │ /Users/axe//.env(.local) │ ← AUTO-GENERATED, mode 600, │ FRAME_DB_PASSWORD="..." │ git-ignored │ AZURE_FRAME_MCP_CLIENT_SECRET="..." │ │ ... │ └────────────────────────────────────────┘ │ ▼ docker container env (frame-mcp-blue, etc.) ``` ## 매니페스트 스키마 `customers.yaml` 의 `customers.CUSTOMER.services.SERVICE.secrets[]`: ```yaml services: frame: env_file: "/Users/axe/frame/.env.local" # axe secret pull 의 write target secrets: - env: FRAME_DB_PASSWORD # 환경변수 이름 (docker-compose 가 보는) vault: "frame/axe/db-password" # Bitwarden item name (axe secret get 가 보는) - env: AZURE_FRAME_MCP_CLIENT_SECRET vault: "frame/axe/oauth-client-secret" rotation_external: azure # 회전 시 Azure portal 갱신 필요 # ... ``` | 필드 | 필수 | 의미 | |---|---|---| | `env_file` | ✓ | `axe secret pull` 가 작성할 절대 경로. docker-compose.yml 의 `env_file:` 지시자와 일치 | | `secrets[].env` | ✓ | 환경변수 이름 (서비스 코드가 `os.getenv("...")` 로 보는 이름) | | `secrets[].vault` | ✓ | Vaultwarden item 이름. 컨벤션: `SERVICE/CUSTOMER/SHORT` (예: `frame/axe/db_password`) | | `secrets[].rotation_external` | ✗ | 외부 시스템 식별자 (`azure`/`github`/`meta`/`anthropic`/`naver`/`slack`). 설정 시 `axe secret rotate` 가 portal URL 안내 | | `bootstrap_only` | ✗ | `true` 면 service 자체가 vault 인 경우 (vault 가 자기 자신 못 읽음 — chicken-and-egg). check/pull skip | ## CLI 명령 | 명령 | 동작 | |---|---| | `axe secret check SERVICE` | 매니페스트 vs vault 보유 비교 → 누락 출력. 누락 시 exit 1 | | `axe secret pull SERVICE` | 매니페스트 순회 → bw 로 fetch → `env_file` atomic write (mode 600) | | `axe secret push ENV_NAME --service SVC --value VALUE` | 매니페스트로 vault 경로 lookup → 신규 생성 or password 필드 갱신. 대형/PEM/파일-소스 비밀은 `--value-stdin` 으로 stdin 주입 (값이 argv/ps 에 안 뜸): `grep ^KEY= .env \| cut -d= -f2- \| axe secret push KEY --service SVC --value-stdin` | | `axe secret send ENV_NAME --service SVC [--to RECIP]` | 매니페스트 lookup → vault GET → `bw send` 로 1회용 링크 생성 → URL stdout. 기본 `-d 1 -a 1 --hidden`. 사람에게 전달 전용 | | `axe secret rotate ENV_NAME --service SVC` | 새 값 (외부 provider 입력 or 자동 생성) → vault PUT → pull → ship 트리거 | | `axe secret get VAULT_NAME` | 단건 조회 (저레벨, 기존 명령) | | `axe secret list` | vault 전체 item 표 (값 출력 X) | | `axe secret status` | bw session 상태 확인 | `--customer` 기본값 `axe`. realchoice 합류 시 명시. ## 매니페스트 — 현재 상태 (axe customer) | service | env_file | 비밀 개수 | |---|---|---| | frame | `/Users/axe/frame/.env.local` | 9 (DB password, JWT, OAuth client_secret, PII passphrase × 2, Claude OAuth, 롯데카드 포털 ID/PW — host-only 스크래퍼 [D-frame-lottecard-scraper](/ops/decisions)) | | hive | `/Users/axe/hive/.env.local` | 5 (DB password, JWT, PII passphrase × 2, OAuth client_secret) — frame 동일. confidential client (PKCE + secret, accessTokenVersion=2, isFallbackPublicClient=true) | | blueprint | `/Users/axe/blueprint/.env` | 14 (DB, agent secret, Anthropic admin, OAuth, NextAuth, GH token, cron, frame token, **OIDC 서명키 `blueprint/axe/oidc-signing-key` — D-axe-idp-1 RS256 base64 PKCS8**, ...) | | stream | `/Users/axe/stream/.env` | 2 (DB, Truvia master key) | | magnet | `/Users/axe/magnet/.env` | 10 (DB, HMAC, Meta × 3, Naver × 3, Slack, Threads × 2) | | matrix | `/Users/axe/matrix/.env.local` | 3 (DB password, JWT secret, console API token) — [D-matrix-1](/ops/decisions) | | vault | `/Users/axe/.axe/vault/.env` | 1 (Vaultwarden 자기 자신 OAuth client_secret) — `bootstrap_only: true` | 합계 **42 개 비밀** 매니페스트 등재. (현재 vault 안 실제 item 수와 매니페스트 양은 별개 — `axe secret check SVC` 로 갭 확인.) > **gate (착수 예정, 아직 미배포 — 위 표 미포함)**: gate 가 배포되면 `services.gate.secrets[]` 에 **e-sign 서버 서명키** `GATE_ESIGN_KEY_PEM`(vault `gate/axe/esign-signing-key`, RSA-2048 PKCS8 PEM — blueprint OIDC 서명키 패턴과 동형) + 짝 인증서 `GATE_ESIGN_CERT_PEM`(공개 X.509, 비밀 아님이라 env/파일이면 충분) 등재 필요 ([D-gate-5](/ops/decisions) gate-native CAdES-T 서명엔진, [/architecture/governance](/architecture/governance) §6). 키 디렉터리 지정 변형 = `GATE_ESIGN_KEY_DIR`. `GATE_TSA_URL`(RFC3161 TSA, default DigiCert / 한국 prod Koscom·CrossCert) = 비밀 아님(공개 엔드포인트). ⚠️ 서명키 분실 = 전 과거 봉인 검증불가 → **DR/escrow** 필수(하드닝, [/architecture/governance](/architecture/governance) §9). dev = ephemeral key (vault 불요). ## 신규 customer 매니페스트 추가 (realchoice 등) > **현재 상태 (2026-05-23)**: `customers.yaml` 의 `services:` 섹션은 **`axe` customer 만 등재**. realchoice 는 customer 메타블록 (legal_name, tailscale_host, sso.apps 등) 만 있고 secrets 매니페스트 부재. 즉 `axe secret push --customer realchoice` 가 manifest lookup 실패. 이 갭이 D-day "1-shot onboard" 의 가장 큰 막힘 → [B-onboard-customers-add](/ops/backlog) + [B-onboard-azure-pack](/ops/backlog). 신규 customer 합류 시 `customers.yaml` 의 `services:` 섹션에 customer 별 슬롯을 미리 등재해야 `axe secret push` 가 동작합니다. realchoice 템플릿: ```yaml services: frame: realchoice: env_file: "/Users/realchoice/frame/.env.local" secrets: - env: FRAME_DB_PASSWORD vault: "frame/realchoice/db-password" - env: FRAME_JWT_SECRET vault: "frame/realchoice/jwt-secret" - env: AZURE_FRAME_MCP_CLIENT_SECRET vault: "frame/realchoice/oauth-client-secret" rotation_external: azure - env: FRAME_PII_PASSPHRASE_REALCHOICE vault: "frame/realchoice/pii-passphrase-realchoice" - env: CLAUDE_CODE_OAUTH_TOKEN vault: "frame/realchoice/claude-oauth" blueprint: realchoice: env_file: "/Users/realchoice/blueprint/.env" secrets: - env: AZURE_AD_CLIENT_SECRET vault: "blueprint/realchoice/azure-client-secret" rotation_external: azure - env: NEXTAUTH_SECRET vault: "blueprint/realchoice/nextauth-secret" # ... (axe customer 의 12 개 패턴 동일 복제) vaultwarden: realchoice: env_file: "/Users/realchoice/.axe/vault/.env" bootstrap_only: true secrets: - env: AZURE_VAULTWARDEN_CLIENT_SECRET vault: "vaultwarden/realchoice/oauth-client-secret" rotation_external: azure hive: realchoice: env_file: "/Users/realchoice/hive/.env.local" secrets: - env: HIVE_DB_PASSWORD vault: "hive/realchoice/db-password" # ... axe 동일 패턴 ``` > **`axe customers add {customer}` 명령** ([B-onboard-customers-add](/ops/backlog)) 가 이 템플릿을 자동 생성하도록 만들면 운영자 손작업 단계 2 제거 가능. customer IT 회신 (8 개 값) JSON 한 덩어리를 받아 `axe customer ingest {customer} azure-pack.json` 한 줄에 (a) customers.yaml 슬롯 작성 + (b) 3 개 client_secret 을 vault 로 push 까지 묶는 게 [B-onboard-azure-pack](/ops/backlog) 의 본질. ## 회전 (rotation) 흐름 ``` axe secret rotate AZURE_FRAME_MCP_CLIENT_SECRET --service frame [1/4] external rotation required (provider: azure) portal: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/... action: rotate secret in the provider, then paste the NEW value below. the OLD value will remain valid until step 4 (rotation overlap window). new value: ******** [2/4] writing new value to vault item 'frame/axe/oauth-client-secret' ✓ updated existing vault item [3/4] axe secret pull frame ✓ frame: pulled 7 secrets → /Users/axe/frame/.env.local (mode 600) [4/4] deploy with new secret run `axe ship frame` now? (yes/no): yes → frame blue/green swap with new secret Note: in the azure portal, REVOKE the OLD secret value now that the new one is verified working in production. Both are valid until you delete the old. ``` 회전 순서가 중요: 1. **외부 provider 먼저** — Azure 가 새 값 발급 → 운영자가 복사 2. **vault PUT** — Bitwarden 에 새 값 저장 (이전 값 덮어쓰기) 3. **env_file refresh** — `axe secret pull` 4. **service redeploy** — `axe ship` (blue/green = 무중단) 5. **OLD secret revoke** — Azure portal 에서 이전 값 삭제 (overlap window 닫기) ## Pull 동작 — merge-mode (D-ops-18 의 결과) `axe secret pull SERVICE` 는 env_file 을 **머지** 합니다: - 매니페스트의 키 → vault 값으로 **in-place update** - 매니페스트 외 키 / 주석 / 빈 줄 → **그대로 보존** - 매니페스트엔 있는데 파일엔 없는 키 → 파일 끝에 append → 비밀 외 config (예: `AZURE_AD_CLIENT_ID`, `NEXTAUTH_URL`, `DATABASE_URL` 등 운영자가 hand-maintain 하는 항목) 가 pull 호출로 사라지지 않음. **초기 구현은 전체 덮어쓰기였고 2026-05-21 첫 blueprint pull 에서 9개 config 가 wipe 되는 사고 발생 → 코드 패치 + D-ops-18 등재**. 출력 예: ``` ✓ blueprint: merged 13 secrets into /Users/axe/blueprint/.env (13 updated, 0 added, non-managed lines preserved) ``` ## Rotation — az CLI 기반 (D-ops-19) Azure 의 `client_secret` 회전은 6 단계 az cli 자동화. Portal UI 0 회 (단, app 의 **Owner** 가 운영자로 박혀 있어야 함): ```bash APP_ID= # e.g. Frame MCP: 137fc0ef-... OLD_KEY_ID=$(az ad app credential list --id $APP_ID --query "[?starts_with(hint,'')].keyId | [0]" -o tsv) # 1. Azure 새 secret 추가 (--append, OLD 유지 = overlap window) NEW=$(az ad app credential reset --id $APP_ID \ --display-name "rotated " --years 2 --append \ --query password -o tsv) # 2. vault PUT axe secret push --service --value "$NEW" # 3. pull (merge — config 보존) axe secret pull # 4. 무중단 swap axe deploy axe --apply # frame / hive # 또는 axe blueprint upgrade axe --apply # blueprint # 5. 검증 — 컨테이너 env 의 새 secret prefix 가 OLD 와 다른지 docker exec env | grep # 6. 운영 안정 확인 후 OLD revoke az ad app credential delete --id $APP_ID --key-id $OLD_KEY_ID ``` **App Owner 전제조건**: portal-등록 app 은 default 로 owner 없음 → az cli 권한 X. 신규 az cli 등록 app 은 호출자가 owner 자동 박힘. 기존 portal-등록 app 들은 portal Owners 탭에서 운영자 (`ai@axellc.com`) 명시 추가 필요. 본 플랫폼 현황: | App | appId | Owner | |---|---|---| | frame_mcp | `137fc0ef-...` | ai@axellc.com (2026-05-21 추가) | | hive_mcp | `b7ead15d-...` | ai@axellc.com (등록 시 자동) | | blueprint_mcp | `482598f7-...` | ai@axellc.com (등록 시 자동) | | Blueprint Graph (better-auth) | `2b222356-...` | ai@axellc.com (2026-05-21 추가) | | vaultwarden | `9d0dc49b-...` | ai@axellc.com (2026-05-21 추가) | ## 사람에게 전달 — Bitwarden Send `axe secret get` 의 결과는 vault → 디스크/컨테이너 경로용. **사람 (신규 직원, customer admin) 에게 보낼 때는 평문 절대 금지** — Teams DM, 이메일, 위키 모두 영구 보존되는 채널. Vaultwarden 의 1회용 링크 (Bitwarden Send) 로 전달한다. ### `axe secret send` (권장) ```bash axe secret send --service [--to ] ``` 매니페스트 lookup (`customers.yaml services..secrets[].env → .vault`) → bw GET → bw send 파이프. stdout 에 Send URL 만 (상태 메시지는 stderr) — `axe secret send ... | pbcopy` 로 클립보드 직행. 기본값: | 플래그 | 의미 | 기본값 | |---|---|---| | `-d N` / `--days N` | N일 후 자동 삭제 | `1` | | `-a N` / `--access N` | 최대 N회 열람 후 무효 | `1` | | `--to <라벨>` | Send name 에 `(라벨)` 추가 (운영자 추적용, 실 ACL 아님) | 없음 | | `--password

` | 추가 비밀번호 잠금 | 없음 — 외부 customer 대상 시 권장 | | `--customer ` | customer 식별 (기본 `axe`) | `axe` | stdin 으로 값 주입 → argv 에 secret 노출 0 (ps, shell history 안 박힘). ```bash # 예: 한진우에게 Blueprint MCP secret 전달 axe secret send AZURE_BLUEPRINT_MCP_CLIENT_SECRET --service blueprint --to jinwoo # ✓ Send created: blueprint AZURE_BLUEPRINT_MCP_CLIENT_SECRET (jinwoo) # expires in 1d / max 1 access(es) # https://vault.axelabs.ai/#/send/abc123.../def456... ``` ### 다인 × 다 secret 배치 `-a 1` 이면 한 사람이 열면 끝나므로 **(수신자, secret) 1쌍당 링크 1개**: ```bash for who in taehun jinwoo soohun; do for env in AZURE_BLUEPRINT_MCP_CLIENT_SECRET AZURE_HIVE_MCP_CLIENT_SECRET; do svc=$(echo $env | awk -F_ '{print tolower($2)}') echo "=== $who / $env ===" axe secret send $env --service $svc --to $who done done ``` 위 6 링크가 stdout 에, 각 헤더는 stderr 로 분리 출력. 운영자 본인 (soohun) 도 동일하게 새 Custom Connector 등록 흐름이 필요하면 포함. ### 자동 발사 — `/api/admin/broadcast-dm` `axe secret send` 출력 URL 을 운영자가 Teams 에 손으로 paste 하지 말고 **Blueprint bot (ai@axellc.com) 이 1:1 DM 으로 직접 발사**. 엔드포인트 [`POST /api/admin/broadcast-dm`](https://blueprint.axellc.com/api/admin/broadcast-dm) 이 each email → AAD lookup → `POST /chats` (oneOnOne, Graph idempotent) → `POST /chats/\{id\}/messages` 수행. ```bash # 1. URL 발급 (이미 `/tmp/sends.txt` 로 캐시했다고 가정) # 2. 수신자별 메시지 합성 + DM 발사 (예: 강태훈 한 명) CRON=$(grep -E "^CRON_SECRET=" /Users/axe/blueprint/.env | cut -d= -f2-) URL_BP=$(grep "^taehun|blueprint|" /tmp/sends.txt | cut -d'|' -f3) URL_HV=$(grep "^taehun|hive|" /tmp/sends.txt | cut -d'|' -f3) TEXT="태훈님, Blueprint + Hive Custom Connector 등록 부탁드립니다 ... Blueprint Secret: $URL_BP Hive Secret: $URL_HV" curl -s -X POST http://localhost:3100/api/admin/broadcast-dm \ -H "Authorization: Bearer $CRON" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg e taehun.kang@axellc.com --arg t "$TEXT" \ '{emails:[$e], text:$t, contentType:"text"}')" # → {"summary":{"total":1,"sent":1,...}, "results":[{...,"chatId":"19:...","messageId":"..."}]} ``` **같은 텍스트를 N명에게 broadcast**: `emails:[a,b,c]` 한 호출. 다른 텍스트면 N 호출 (위 패턴 반복). | 필드 | 의미 | |---|---| | **Endpoint** | `POST /api/admin/broadcast-dm` (Blueprint, host `:3100` 또는 `blueprint.axellc.com`) | | **Auth** | `Authorization: Bearer $CRON_SECRET` (Blueprint `.env`). NextAuth 세션 불필요 | | **Body** | `\{ emails: string[], text: string, contentType?: "text" \| "html" \}` 기본 `"text"` (HTML escape 안전) | | **AAD lookup** | `User.aadObjectId` (NextAuth 첫 sign-in 시 자동 채워짐). 없으면 `skipped` | | **Idempotent** | Graph `POST /chats` 가 같은 (bot, target) 쌍이면 같은 chat id 반환 → 재호출해도 새 채팅방 안 생김 | | **Self-DM 차단** | target AAD = bot AAD 면 `skipped` (bot=ai@axellc.com 본인은 호출 불가) | 상세 코드: `/Users/axe/blueprint/src/app/api/admin/broadcast-dm/route.ts`. #### 함정 | 함정 | 결과 | 회피 | |---|---|---| | 수신자가 메시지 보면 **sender = AI (ai@axellc.com)** 이라 사람-요청 같지 않음 | "왜 AI 가 connector 등록 시킴" 혼동 | 메시지 본문 첫 줄에 운영자명 명시 ("수훈 대표 요청으로 ...") | | `User.aadObjectId` 비어 있는 신규 직원 → `skipped` | DM 미발사, 운영자 로그 확인 안 하면 모름 | NextAuth Blueprint 첫 sign-in 1회 선행. 또는 응답 JSON 의 `summary.skipped > 0` 가드 | | `CRON_SECRET` env 미설정 | 503 응답 | Blueprint `.env` 의 `CRON_SECRET` 확인. Phase 2 deploy 에서 자동 생성됨 | ### 웹 UI 대안 `https://vault.axelabs.ai` 로그인 → 좌측 **Send** → **+ New Send** → Type: Text, Text: secret 붙여넣기, Deletion: 1 day, Maximum Access Count: 1, Hide text by default ✓ → Save → **Copy link**. CLI 가 막혔거나 매니페스트 외 임시 비밀에만 사용. ### 함정 | 함정 | 결과 | 회피 | |---|---|---| | `bw send` 를 raw 로 호출하면서 `axe secret get` 에 `-n` 누락 | trailing `\n` 이 base64/URL 인코딩 시 secret 변형 → OAuth 거부 | `axe secret send` 사용 (내부에서 stdin 으로 처리) | | Send URL 자체를 다시 위키/메일에 영구 보존 | 1회 사용 후 URL 무효지만 메타데이터 (라벨) 노출 | URL 은 1회용 채널 (DM, SMS) 만 사용 | | `-d` 너무 길게 (예: `-d 30`) | 수신자가 잊고 안 받으면 secret 이 30일간 노출면 유지 | `-d 1` 기본, 수신 확인 후 `bw send delete ` | | 수신자가 받기 전에 운영자 본인이 열어버림 | `-a 1` 소진 → 수신자 재발급 필요 | URL 검증 X — 발급 후 바로 송부, 수신자 동의 확인 후 만 | | Send 만든 직후 `bw sync` 안 한 두 번째 macmini 에서 안 보임 | revoke 가 한쪽에만 적용 | 운영자는 한 기기에서만 Send 관리, 또는 매 작업 후 `bw sync` | | vault locked 상태 (timeout) | `axe secret send` 가 시작 시 status 검사 → "vault is locked" 명시 + unlock 명령 안내 | `axe vault unlock` → Keychain 갱신. **TTY 없는 에이전트/cron 컨텍스트면 osascript 다이얼로그 자동 팝업** (운영자가 master password 입력) — 더 이상 "운영자가 터미널에서 직접" punting 불필요 (2026-06-04 root-cause fix). `--gui` 강제 / `--tty` 레거시 프롬프트 | | bw locked 상태에서 raw `bw get password X` 직접 호출 | interactive prompt 시도 → `ERR_USE_AFTER_CLOSE` crash + **exit 0 + 빈 stdout** (misleading) | `axe secret send`/`get` 만 사용 — pre-check 가 status 가드 | | **bw 2025.7.0 (D-ops-28 다운그레이드 line) 의 `bw send` 가 옵션 (`-d` / `-a` / `-n` / `--hidden`) 함께 주면 stdin pipe 거부** — `echo X \| bw send -d 1 -a 1 -n "..."` → `error: missing required argument 'data'`. `--help` 의 `echo "text" \| bw send` 예시는 **옵션 0 인 호출에만** 동작 | raw `bw send` 사용한 1세대 `axe secret send` 가 exit 1 로 죽음 (2026-05-23 발견, 1세대 라이브 첫 호출 시) | 2세대 구현 = `bw send create` 서브커맨드 + base64-encoded JSON template 을 stdin 으로 (`\{type:0, name, deletionDate, maxAccessCount, text:\{text, hidden\}, password\}`) — encoded blob 도 stdin 이라 argv 노출 0. `accessUrl` 필드 parse 해서 반환 | ## 운영자 → vault 비밀 입력 패턴 (osascript hidden-dialog) 운영자가 **외부에서 발급받은 비밀 (PAT, API key, OAuth client_secret, SaaS token)** 을 vault 에 한 번에 영구 저장해야 할 때의 표준 패턴. [D-ops-40](/ops/decisions) axe.3 release (2026-05-26) GHCR PAT 보관 시 정착. > **같은 osascript 다이얼로그 기법이 vault *unlock* 에도 적용됨** (2026-06-04): `axe vault unlock` 은 TTY 가 없으면 (Claude Code / cron / CI) 자동으로 hidden-text 다이얼로그를 띄워 master password 를 받아 `bw unlock --passwordenv` 로 unlock → BW_SESSION 을 Keychain 에 저장. 덕분에 **어느 에이전트 세션이든 한 명령으로 vault unlock 을 트리거**할 수 있고 운영자는 팝업에만 답하면 됨 (master password 는 argv/ps/history 어디에도 안 남음). 근인: 기존 `bw unlock --raw` 는 프롬프트에 TTY 가 필요해 non-TTY 호출자가 unlock 불가 → 매번 운영자에게 punting 했음. > > **파일/대형/PEM 비밀**(예: 플랫폼 OIDC RS256 서명키)은 다이얼로그(single-line) 대상이 아니므로 `... | axe secret push --service --value-stdin` 으로 stdin 주입 (값이 argv/ps 에 안 뜸). ### 원칙 | 원칙 | 구현 | |---|---| | 비밀 값 = AI context 미진입 | bash subprocess 안에서만 env var, AI 가 받는 output 은 `✓ saved/✗ failed` 만 | | shell history 미기록 | `osascript display dialog` (zsh history 우회) | | 화면 / 스크롤백 미노출 | `hidden answer` 옵션 | | clipboard 미사용 | dialog → 직접 env var (clipboard hop 없음) | | TTY 불필요 | macOS Apple Events 로 GUI dialog — `forkpty: Device not configured` 환경에서도 동작 | | 저장처 단일 SoT | AXE Vaultwarden organization collection (keychain 금지 — [D-ops-ops-17](/ops/decisions) + [B-frame-keychain-to-vault](/ops/backlog)) | | 메모리 즉시 정리 | bash subprocess 종료 시 env var 소멸 + 명시 `unset` | | idempotent | 같은 name 으로 재실행 시 UPDATE (overwrite), 없으면 CREATE | ### 한 블록 (복붙 → argument-pack 만 수정) ```bash # === argument-pack: 이 부분만 task 별로 갱신 === ITEM_NAME='' # 예: ghcr-axelabs-ai-pull-pat COLLECTION_ID='' # bw list collections --organizationid | jq 로 확인 ORG_ID='' # 예: 0c5d8bbd-ad85-42b4-8b8a-2849031981b1 (AXE) LOGIN_USERNAME='' # 예: axe-labs-ai LOGIN_URI='' # 예: https://ghcr.io DIALOG_PROMPT='<운영자에게 보여줄 한 줄 설명>' DIALOG_TITLE='

' NOTES_HINT='<짧은 용도 + rotation 주기>' # ============================================ SECRET=$(osascript \ -e "display dialog \"${DIALOG_PROMPT}\" default answer \"\" with hidden answer with title \"${DIALOG_TITLE}\" buttons {\"Cancel\",\"OK\"} default button \"OK\"" \ -e 'text returned of result' 2>&1) RC=$? if [ $RC -ne 0 ] || [ -z "$SECRET" ]; then echo "DIALOG_CANCELLED_OR_EMPTY rc=$RC"; exit 1; fi export BW_SESSION="$(security find-generic-password -s 'axe.vault.session' -w 2>/dev/null)" if [ -z "$BW_SESSION" ]; then echo "NO_SESSION_IN_KEYCHAIN"; exit 1; fi STATUS=$(bw status --session "$BW_SESSION" --raw 2>&1 | jq -r '.status' 2>/dev/null) if [ "$STATUS" != "unlocked" ]; then echo "SESSION_NOT_UNLOCKED (run: bw unlock --raw)"; exit 1; fi bw sync --session "$BW_SESSION" >/dev/null 2>&1 EXISTING=$(bw list items --organizationid "$ORG_ID" --session "$BW_SESSION" 2>/dev/null \ | jq -r --arg n "$ITEM_NAME" '.[] | select(.name==$n) | .id' | head -1) if [ -n "$EXISTING" ]; then bw get item "$EXISTING" --session "$BW_SESSION" 2>/dev/null | jq \ --arg secret "$SECRET" --arg today "$(date +%Y-%m-%d)" --arg hint "$NOTES_HINT" \ '.notes="\($hint) Updated: \($today)." | .login.password=$secret' \ | bw encode | bw edit item "$EXISTING" --session "$BW_SESSION" >/dev/null \ && echo "✓ UPDATED $ITEM_NAME ($EXISTING)" else bw get template item | jq \ --arg name "$ITEM_NAME" --arg org "$ORG_ID" --arg col "$COLLECTION_ID" \ --arg secret "$SECRET" --arg today "$(date +%Y-%m-%d)" --arg hint "$NOTES_HINT" \ --arg user "$LOGIN_USERNAME" --arg uri "$LOGIN_URI" \ '.name=$name | .notes="\($hint) Issued: \($today)." | .organizationId=$org | .collectionIds=[$col] | .login.username=$user | .login.password=$secret | (if $uri == "" then . else .login.uris=[{"uri":$uri}] end)' \ | bw encode | bw create item --session "$BW_SESSION" 2>/dev/null | jq -r '.id' \ | xargs -I{} echo "✓ CREATED $ITEM_NAME ({})" fi unset SECRET ``` ### 호출 시 운영자 액션 (5 단계) 1. 비밀 발급처 (예: https://github.com/settings/tokens) 에서 비밀 생성 + 화면에 표시된 값 복사 2. 위 블록 실행 (운영자 본인 터미널 OR Claude Code 의 Bash tool 둘 다 가능) 3. macOS native dialog 팝업 → hidden-text 필드에 비밀 붙여넣기 → OK 4. 결과 = `✓ CREATED ()` 또는 `✓ UPDATED ()` 5. `pbcopy < /dev/null` 로 clipboard 클리어 이후 자동화는 `bw get password --session "$BW_SESSION"` 한 줄로 꺼내씀. ### AI agent 호출 Claude Code 세션은 `vault-secret-capture` skill 로 같은 패턴 자동 적용 (description 매칭 기반 자동 발견). 운영자가 "vault 에 PAT 저장해줘" 만 해도 위 블록을 argument-pack 채워서 실행 — 비밀 값은 dialog 통해서만 받음 (AI context 미통과). ### 실 적용 예 — D-ops-40 GHCR PAT ```text ITEM_NAME = ghcr-axelabs-ai-pull-pat COLLECTION_ID = 1d794d29-f127-4602-b8cb-5ce8a731cf7f (Platform — Service Secrets) ORG_ID = 0c5d8bbd-ad85-42b4-8b8a-2849031981b1 (AXE) LOGIN_USERNAME = axe-labs-ai LOGIN_URI = https://ghcr.io NOTES_HINT = GHCR push/pull. axe-macmini + customer macmini docker login. Rotate 90d. ``` 이후 사용: `bw get password ghcr-axelabs-ai-pull-pat | docker login ghcr.io -u axe-labs-ai --password-stdin` ### 함정 (이 패턴 한정) | 함정 | 결과 | 회피 | |---|---|---| | collection name 의 em-dash (`Platform — Service Secrets`) 잘못 hyphen 으로 lookup | name 검색 실패 | ID 직접 사용 (`COLLECTION_ID=...uuid...`) | | `axe.vault.session` Keychain entry 부재 | NO_SESSION_IN_KEYCHAIN | `bw unlock --raw \| security add-generic-password -s 'axe.vault.session' -a "$(whoami)" -w "$(cat)" -U` | | 줄바꿈 포함 secret (PEM, RSA private key) | dialog 가 single-line | 다른 패턴 필요 (file path → vault attachment, 별 skill) | | dialog OK 가 아닌 `Cancel` / 빈 입력 | DIALOG_CANCELLED_OR_EMPTY | 정상 처리, exit 1 로 정지 (재실행 가능) | | customer-side 비밀을 AXE vault 에 저장 | sovereignty 위반 | customer 측이 자기 vault 에 자기가 저장 (같은 패턴 customer macmini 에서 실행) | ### 향후 확장 - `axe secret prompt-store --collection X --uri ... --username ...` CLI subcommand 로 한 줄 명령화 (backlog: B-axe-secret-prompt-store-cli) - 다중 secret 동시 입력 (예: client_id + client_secret 쌍) — 현재는 별 호출 2회 - 줄바꿈 포함 secret — file path 받아서 vault attachment 으로 저장하는 별 skill ## SSH 환경에서 vault 비밀 주입 (keychain-free raw-bw) > 위 osascript hidden-dialog 패턴은 **GUI 세션**(콘솔/원격 데스크톱)을 전제로 한다. 그러나 이 머신 = AXE 의 Mac mini 이고 **Claude Code 세션은 SSH(loopback) 위에서** 돈다 — 운영자는 Windows 에서 `ssh axe@100.127.210.30` (Tailscale) 로 접속한다. macOS 는 **SSH 세션이 GUI login keychain 에 쓰는 것을 거부**한다 ("User interaction is not allowed"). 그래서 SSH 컨텍스트에서 vault 에 비밀을 *넣을* 때는 **keychain 을 전혀 안 쓰는 raw-bw 경로**를 쓴다. ([D-ops-44](/ops/decisions), `/Users/axe/CLAUDE.md` 강제 규칙.) ### 금지 (SSH 에서 전부 실패) | 금지 | 이유 (SSH 에서 깨지는 메커니즘) | |---|---| | 운영자에게 `axe vault unlock` 시키기 | unlock 의 keychain-cache 단계가 SSH 세션에서 crash | | `axe secret push` / `pull` / `check` 로 비밀 PUT | 내부 `_vault_env` 가 keychain 세션만 읽음 (`security find-generic-password -s axe.vault.session -a ai@axellc.com`) → SSH 에선 "vault session not found" | | osascript `display dialog` | GUI 다이얼로그 = 원격/Windows 운영자에게 안 보임. 운영자-타이핑 비밀은 `read -rs` 로 받을 것 | ### 정답 = keychain-free raw-bw (운영자가 자기 셸에서 직접) 운영자가 **자기 인터랙티브 셸** (Windows `ssh axe@100.127.210.30` 또는 Mac 터미널) 에서 직접 실행한다. 에이전트는 스크립트를 **ASCII-only · 주석 없음 · `bw unlock` 을 단독 한 줄**로 공급한다 — 한 블록으로 붙여넣으면 unlock 프롬프트가 바로 다음 줄을 비밀번호로 삼켜버리기 때문이다. 1. **private CA 지정** (필수): ```bash export NODE_EXTRA_CA_CERTS=/Users/axe/.axe/vault/certs/rootCA.pem ``` 2. **unlock — 반드시 자기 단독 줄에서** (master-pw 프롬프트): ```bash export BW_SESSION="$(bw unlock --raw)" ``` TTY-flaky 환경 폴백 (프롬프트가 안 뜨거나 깨질 때): ```bash read -rs PW; export BW_SESSION="$(BW_PASSWORD="$PW" bw unlock --passwordenv BW_PASSWORD --raw)"; unset PW ``` 3. **Login item 생성/갱신** (raw `bw create` / `bw edit`), 그다음 `bw sync` → `unset BW_SESSION`. 항목 매핑: | bw 필드 | 값 | |---|---| | **name** | `customers.yaml` 의 `services..secrets[].vault` 경로 (예 `gate/axe/jwt-secret`) | | **username** | env var 이름 (예 `GATE_JWT_SECRET`) | | **password** | 스크립트 안에서 생성/읽은 값 — `$(openssl rand -hex 32)` / `$(cat key.pem)` / 상수. **echo·shell history·argv·agent-context 어디에도 안 남김** | ### Windows 운영자 접속 운영자(Windows)는 Tailscale 로 `ssh axe@100.127.210.30` 한 뒤 위 3 단계를 **자기 SSH 셸 안에서** 실행한다. BW_SESSION 은 그 셸에만 존재한다. ### 검증 caveat — 에이전트는 검증 불가 BW_SESSION 이 운영자 셸 안에만 살아 있으므로 에이전트(Claude)는 결과를 직접 확인할 수 없다. **운영자의 `created`/`updated` + `synced` 출력이 곧 확인**이다. `axe secret check` 호출 금지 (keychain 필요 → SSH 에서 실패). ### keychain 세션이 필요한 곳 = deploy/ship 시점 한정 비밀을 *넣는* 작업은 위 raw-bw 로 keychain 없이 끝난다. 단 **`axe ship` / `axe secret pull` 은 배포 시점에 keychain 세션을 읽는다**. SSH 에서 GUI 없이 그 세션을 채우는 법 (headless unlock): ```bash read -rs KCPW; security unlock-keychain -p "$KCPW" ~/Library/Keychains/login.keychain-db; unset KCPW ``` 그다음 `axe vault unlock` → `axe ship`. 이 단계는 **배포/ship 할 때만** 필요하고, 비밀 주입 자체에는 불요하다. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | 서비스 코드에서 vault REST 직접 호출 | vault 다운 = 전 서비스 startup fail | **금지**. 매니페스트 등록 후 deploy-time pull 만 | | `git push origin main` 직접 호출 (axe ship 우회) | docs drift + secret 미동기화 | release-worthy push 는 `axe ship` 만 (D-ops-16) | | `.env(.local)` git commit | secret 노출 | `.gitignore` 에 `.env*` (각 repo 확인 필요) | | vault item name 오타 | check 가 missing 으로 표시 | 매니페스트 yaml lint + vault item 정확 복사 | | bw session 만료 (Keychain) | `axe secret get/check/pull` 무응답 | `bw unlock --raw` 재실행 + Keychain 업데이트 | | 매니페스트에는 있는데 vault item 누락 | check 실패 → ship abort | Vaultwarden web UI 에서 Login item 생성 (name = vault path, password 필드 = 값) | | `bootstrap_only` 인 vault 자체 secret | pull 실패 (rightly) | `/Users/axe/.axe/vault/.env` 수동 유지 + 종이 메모 fallback | | 운영자 1명 → service account 부재 | 모든 bw 호출이 운영자 개인 token | `axe-cli@axellc.com` service account 분리 (Phase 6) | | **compose `environment:` 블록의 default-empty substitution (`$VAR` 변수 치환 형식) 이 env_file 값을 shadow** | 컨테이너에 secret 빈 값 도달 (frame 2026-05-21 dogfood 시 발견) | compose `environment:` 블록에 **비밀 vars 중복 금지** — env_file 만 사용 (D-ops-18) | | **compose 파일이 subdir 에 있고 Docker auto-load `.env` 가 project_dir 의 `.env` 만 찾음** | `$VAR` 변수 substitution 실패 → 컨테이너 미작동 (blueprint `/docker/` subdir 시 발견) | `ln -sf ../.env SUBDIR/.env` symlink (D-ops-18) | | **portal-등록 app 에 owner 없음 → az cli 회전 권한 X** | `Insufficient privileges` | portal Owners 탭에서 운영자 명시 추가 (D-ops-19) | | **az 의 `credential reset` (no `--append`) 가 기존 secrets 전체 삭제** | overlap window 없이 즉시 cutover → in-flight 인증 실패 | 항상 `--append` (D-ops-19) | | 매니페스트 env_file 이 compose 가 실제 읽는 파일과 불일치 (frame `.env` vs `.env.local`) | pull 가 .env.local 에 써도 compose 가 .env 에서 substitution → 새 값 안 닿음 | compose 의 모든 비밀 var 가 env_file 단일 경로에서 읽혀야 함. environment block 에서 비밀 substitution 제거 (D-ops-18) | | **bw CLI `2026.4+` 또는 Bitwarden Chrome extension `>= 2026.4`** + Vaultwarden Timshel fork SSO user | `TypeError: toWrappedAccountCryptographicState` / "Master password unlock data was not found" crash | bw `2025.7.0` pin (`npm install -g @bitwarden/cli@2025.7.0`). 서버 측 axe.2 patch ([D-ops-37](/ops/decisions)) 가 영구 fix — patch 적용된 image (`axe.3` release) 사용 시 bw 2026.x 도 호환되지만 운영 표준은 2025.7.0 통일 | | **bw `data.json` cache stale (server patch deploy 후)** — axe.2 / axe.3 등 server patch deploy 후 local cache 가 옛 schema 의 wrapped key 보유 → bw 가 decrypt 시도 시 MAC mismatch. patch deploy 가 client-side 무효화 신호 안 보냄 = 운영자 본인이 logout/login 1 회 필수 | 매 `bw unlock` 또는 `bw get ...` 시 `[Encrypt service] MAC comparison failed` → bw 명령 전체 실패 | (1) `bw logout` (2) `bw config server https://axe.axelabs.ai/vault` (3) `bw login` interactive (4) `bw unlock` 재시도. frequency 추적: 2026-05-22, 2026-05-26 — server patch deploy 후 매번 1회. 상세 = [`/ops/runbook/vault-recovery#bw-cli-data-json-cache-stale-recovery`](/ops/runbook/vault-recovery#bw-cli-data-json-cache-stale-recovery) | ## Bootstrap (현재 상태) ### bw CLI 표준 버전 — `2025.7.0` ([D-ops-28](/ops/decisions)) `axe secret *` + `bw-bootstrap.sh` + customer-side wrapper 가 **모두 bw CLI `2025.7.0`** 가정. 신규 머신 / customer macmini 설치: ```bash npm install -g @bitwarden/cli@2025.7.0 bw --version # 2025.7.0 확인 ``` **다운그레이드 사유**: bw `2026.4+` 와 Bitwarden Chrome extension `>= 2026.4` 가 Vaultwarden Timshel fork 의 SSO user 와 호환 깨짐 — `TypeError: toWrappedAccountCryptographicState` / "Master password unlock data was not found" crash. 서버 측 axe.2 patch ([D-ops-37](/ops/decisions)) 가 영구 fix 라 본 patch 이미지 사용 시 bw 2026.x 도 호환되지만, **운영 표준은 2025.7.0 로 통일** (회귀 차단 + customer macmini 일관성). axe CLI 의 `_svc_step_customer_vault_check` 가 version mismatch 발견 시 warning. `bw-bootstrap.sh` 가 install hint + warning gate. ### 초기 채우기 `axe secret push/pull/rotate` 코드는 동작 — `bw` CLI 가 설치돼 있고 `BW_SESSION` 이 Keychain 에 있으면 즉시 사용 가능. 다만 **vault 안에 실제 item 들이 아직 만들어지지 않음**. 초기 채우기: ```bash # vault 잠금 풀기 (필요 시) bw unlock --raw # 출력값 복사 security add-generic-password -s axe.vault.session -a ai@axellc.com -w "" -U # 누락 확인 axe secret check frame # 각 누락 item 에 대해, 현재 디스크의 값을 vault 로 push axe secret push FRAME_DB_PASSWORD --service frame # (interactive: value 입력) # 또는 --value '' 로 한 줄에 # 모두 채운 후 재확인 axe secret check frame # exit 0 = ✓ # 이후로는 axe ship 가 자동으로 check + pull ``` ## MCP Connectors catalog (D-vault-mcp-catalog, 2026-05-26) 운영자/직원/customer 가 `claude.ai/customize/connectors` (또는 다른 MCP host) 에 axelabs 자체 MCP 를 custom connector 로 등록할 때, 4 조각 (이름, MCP URL, client_id, client_secret) 을 매번 4 곳에서 찾는 비효율 해소. Vaultwarden 의 **`MCP Connectors` org collection** 이 catalog view — Bitwarden 브라우저 확장이 URI 매칭으로 자동 suggest. ### 구조 (데이터 중복 0) ``` SoT (변경 금지) View (자동 생성) ───────────────── ───────────────────────────────── customers.yaml ─┐ .sso.apps.frame_mcp │ .client_id ├──→ axe mcp publish ──→ Vaultwarden .application_id_uri │ (idempotent upsert) "MCP Connectors" collection .client_secret_env │ ├─ "Frame MCP (axe)" │ │ URIs: claude.ai/.com/customize/connectors Vaultwarden ─┤ │ + https://axe.axelabs.ai/frame/mcp frame/axe/ │ │ username: 137fc0ef-... (client_id) oauth-client-secret ─┘ │ password: │ field "MCP URL": application_id_uri │ field "Tenant ID" / "Scopes" / "Vault secret path" ├─ "Hive MCP (axe)" └─ "Blueprint MCP (axe)" ``` ### CLI ```bash axe mcp list # 미리보기 — vault 미접근 axe mcp publish # 실 publish — 매니페스트 순회 + vault fetch + upsert axe mcp publish --dry-run # plan only axe mcp publish --allow-missing # public client (secret 없는 항목) 도 publish ``` 매 호출 idempotent — item 이름 (`{Svc} MCP ({cust})`) 으로 lookup 하여 존재 시 edit, 없으면 create. ### 사용자 흐름 (target audience) 1. `https://claude.ai/customize/connectors` 접속 + "Custom Connector" → "Add" 2. Bitwarden 브라우저 확장 아이콘 클릭 — URI 매칭으로 3 개 MCP 항목 자동 suggest 3. 원하는 항목 클릭 → username (= client_id) / password (= client_secret) 자동 입력 4. Name + MCP URL 은 custom field 옆 "copy" 한 번씩 → 폼에 붙여넣기 5. Save → connector 등록 완료 ### 자동 갱신 hook `axe secret rotate --service ` 가 MCP `client_secret` 회전 시 `axe mcp publish` 자동 호출 → catalog 의 password 즉시 갱신. 사용자가 옛 secret 으로 등록하다 OAuth 거부 받는 함정 차단. ### 신규 MCP 추가 절차 (표준) 1. Entra app 등록 (또는 az CLI) — application_id_uri = `https://axe.axelabs.ai/{svc}/mcp` + redirect_uris (claude.ai/.com 양쪽) + mcp.access scope 2. `customers.yaml customers.axe.sso.apps._mcp` 4-key 추가: `client_id`, `application_id_uri`, `scopes[]`, `client_secret_env` 3. `customers.yaml customers.axe.services..secrets[]` 에 `{env: AZURE__MCP_CLIENT_SECRET, vault: "/axe/oauth-client-secret", rotation_external: azure}` 추가 4. `axe secret push AZURE__MCP_CLIENT_SECRET --service ` (interactive) 5. `axe mcp publish` → catalog 에 새 item 자동 등재 ### 함정 | 함정 | 결과 | 회피 | |---|---|---| | `customers.yaml` 의 `client_secret_env` line 누락 (예: 5/21 hive 등재 후에도 frame_mcp 의 본 line 이 "proxy-only" 주석 형태로 stale, 5/26 정정) | catalog 의 frame_mcp 가 (public) 으로 잘못 publish — claude.ai 등록 시 secret 칸 비어 OAuth 거부 | `axe mcp list` 출력의 "vault item" 칸이 `(public)` 인 항목 점검. claude.ai 가 secret 요구하는 MCP 면 customers.yaml 의 `client_secret_env` line 확인 | | MCP URL (custom field) 의 Bitwarden 확장 자동입력 불가 | claude.ai 폼의 "Server URL" 필드는 표준 username/password 가 아니라 자동입력 안 됨 — 사용자가 "copy" → "paste" 한 번 필요 | 표준 한계. 4 곳 찾는 것보다 4 클릭 이 빠름 | | customer (realchoice 등) 의 MCP secret 을 operator vault 에 publish | sovereignty 위반 ([B-customer-sovereignty-architecture](/ops/backlog) Q3) | `axe mcp publish` 는 `--customer` 명시 + 명시되지 않으면 axe 만 publish. customer 측 운영자가 customer macmini 에서 customer 자체 vault 에 publish | | Vaultwarden org timeout 으로 catalog 가 stale 보일 가능성 | Bitwarden 확장에 새 item 안 보임 | 클라이언트 측 `bw sync` 또는 web UI 새로고침. catalog 변경 빈도 낮아 일상 운영엔 영향 없음 | ## 관련 - [`/ops/runbook/secret-rotation`](/ops/runbook/secret-rotation) — 회전 실전 절차 - [`/ops/runbook/release-flow`](/ops/runbook/release-flow) — `axe ship` 전체 흐름 - [`/ops/decisions`](/ops/decisions) — D-ops-9 (vault SoT), D-ops-17 (deploy-time pull), D-vault-mcp-catalog (본 catalog) - [`/services/index`](/services) — Vaultwarden 운영 정보 --- # Topology · 네트워크 > Cloudflare Tunnel, Docker network, host-side proxy 패턴. URL: https://docs.axelabs.ai/architecture/topology # Topology · 네트워크 ## 외부 경계 — Cloudflare 모든 외부 트래픽은 Cloudflare 의 anycast 망을 통해 도달합니다. | 구간 | 도메인 / 호스트 | 처리 | |---|---|---| | DNS | `axelabs.ai` zone (Cloudflare) | A/CNAME → Cloudflare proxy IPs | | 전송 보안 | Cloudflare → cloudflared (HTTPS) | mTLS connector | | 터널 | `axelabs-tunnel` (Docker, ID `d8efecdd-2c3f-42de-9925-501433e21394`) | tunnel ingress | | origin | `host.docker.internal:{port}` | per-service routing | cloudflared config 위치: `/Users/axe/.axe/tunnels/axelabs/config.yml` ### Tunnel ingress 규칙 ```yaml tunnel: d8efecdd-2c3f-42de-9925-501433e21394 ingress: - hostname: axe.axelabs.ai path: ^/frame(/.*)?$ service: http://host.docker.internal:3712 # axe-frame-proxy - hostname: axe.axelabs.ai path: ^/vault(/.*)?$ service: https://host.docker.internal:8222 # axe-vault-caddy - hostname: admin.axelabs.ai service: http://axe-caddy:80 # operator console - hostname: axe.axelabs.ai service: http://host.docker.internal:3100 # blueprint apex - service: http_status:404 # default deny ``` ## ⚠️ 함정 — cloudflared SIGHUP 미지원 ```ansi [1m[31mcloudflared 는 SIGHUP graceful reload 를 지원하지 않습니다.[0m ``` **검증된 사실 (2026-05-15)**: `docker kill -s HUP axelabs-tunnel` 실행 시 프로세스가 종료됩니다. config 변경 = 컨테이너 재시작 = **5초 다운타임**. **대응**: 변경 잦은 부분 (frame upstream, blue/green swap) 을 cloudflared config 에서 분리하고 **host-side proxy 뒤로 이전**. → 결과: cloudflared 는 `host.docker.internal:3712` 한 줄만 알고, blue/green swap 은 그 proxy 내부에서 처리. cloudflared 재시작 없음. ## Host-side proxy 패턴 (axe-frame-proxy) **axe-frame-proxy** = Caddy reverse-proxy 컨테이너. host port `3712` 노출. 단일 책임: `/frame/*` 트래픽을 docker network 위의 `frame-mcp` alias (blue 또는 green 중 active 쪽) 로 라우팅. blue/green swap = alias 이동 + Caddy graceful reload (SIGHUP 지원, cloudflared 와 달리). frame 컨테이너의 build/recreate 와 무관하게 cloudflared 안정. ``` cloudflared (axelabs-tunnel) │ ↓ HTTP /frame/* → host.docker.internal:3712 │ ↓ ┌──── axe-frame-proxy (Caddy) ────┐ │ │ │ /frame/* → upstream (선택) │ │ │ │ │ ↓ │ │ frame-mcp-blue :3710 │ ← network alias `frame-mcp` │ frame-mcp-green :3711 │ (blue/green swap = alias move) │ │ └─────────────────────────────────┘ ``` **핵심**: cloudflared 는 `axe-frame-proxy` 만 알고, blue/green 의 실제 컨테이너 이름·포트는 모릅니다. swap 은 docker network alias 이동으로 처리 (`axe deploy frame axe --apply`). ## Docker 네트워크 — `artemis_default` axelabs 플랫폼의 모든 컨테이너 (axelabs-tunnel, axe-frame-proxy, frame-mcp-blue/green, blueprint-app 등) 는 `artemis_default` 외부 docker 네트워크 위에서 통신합니다. ```bash # 검증 docker network inspect artemis_default | grep '"Name"' ``` frame docker-compose.yml 에서: ```yaml networks: artemis_default: external: true # 별도 docker 명령으로 생성된 망에 합류 ``` ## 포트 할당 (CLAUDE.md SSOT) | 범위 | 프로젝트 | |---|---| | 31xx | Blueprint | | 32xx | Cortex | | 33xx | Artemis | | 34xx | Distributa | | 35xx | Kolon Discussion | | 36xx | mysrt | | 37xx | frame | 전체 포트 테이블은 [/Users/axe/CLAUDE.md](https://github.com/soohunkang/blueprint/blob/main/CLAUDE.md) 가 SSOT. 새 서비스 추가 시 반드시 비어 있는 범위 확인. ## ⚠️ 함정 모음 | 함정 | 결과 | 회피 | |---|---|---| | cloudflared SIGHUP 직접 호출 | process 종료, 다운타임 | host-side proxy 뒤로 | | frame 컨테이너 이름을 cloudflared 에 직접 명시 | blue/green swap 불가 | network alias 사용 | | path strip 기대 (cloudflared) | 라우터가 prefix 못 찾음 | 각 서비스에 `/frame` prefix mount | | 2-level subdomain (`frame.axe.axelabs.ai`) | Cloudflare Universal SSL 무료 미커버 | 1-level + path | | `axellc.com` 으로 platform traffic | corporate ↔ platform 경계 혼탁 | 평행 운영, 신규는 `axelabs.ai` only | | `artemis_default` external network 사전 부재 (신규 customer macmini) | `axe deploy` 시 `network artemis_default declared as external, but could not be found` → compose up fail | onboard step 에 `docker network create artemis_default` pre-step (또는 `axe deploy` 자체에 `_svc_step_network` 빌트인 — 2026-05-25 이후 적용 ✅, trap #17 of B-onboard-d-day-traps-2026-05-25) | 상세 결정 근거는 [DECISIONS](/ops/decisions) §3 D-config-13 참조. --- # Vault 세션/정책 모델 (3 layer) > AXE Vaultwarden 의 session lifecycle + 권한 정책 + per-user unlock 패턴. 운영자가 통제 가능한 layer 와 user 자율 layer 의 구분. 새 직원/customer 의 표준 setup. URL: https://docs.axelabs.ai/architecture/vault-policies # Vault 세션/정책 모델 (3 layer) AXE Vaultwarden (Timshel fork 기반, [D-ops-37](/ops/decisions) + [D-ops-40](/ops/decisions)) 의 session/unlock 정책은 **3 layer 누적**으로 결정. 각 layer 가 누가 통제하는지 + 변경 비용이 다름. | Layer | 통제 누가 | 영향 범위 | 변경 비용 | |---|---|---|---| | **1. Server env** (Vaultwarden config) | 운영자 (compose env_file) | 모든 user, 모든 client | container restart 1회 | | **2. Org Policies** (Bitwarden 호환 정책) | Org Owner (web UI 또는 API) | 해당 org member 전원, 모든 client | 즉시 (next login 적용) | | **3. Per-client preferences** (localStorage) | 각 user 본인 (각 device/client 마다) | 본인의 그 client 만 | 본인 30초 | 3 layer 가 **누적 적용** — 각 layer 가 더 paranoia 방향으로 강제하고, 마지막 user 자율 layer 가 ceiling 안에서 편의 조정. ## Layer 1 — Server env Vaultwarden config 의 env var. `/Users/axe/.axe/vault/docker-compose.yml` 의 `environment:` 블록. ### 핵심: `SSO_AUTH_ONLY_NOT_SESSION` | 값 | 효과 | |---|---| | `false` (default) | SSO 의 refresh token 따라감 — Entra ID 의 token 만료 시점에 자동 logout → re-SSO+MP 강제. user 입력 빈도 ↑ | | **`true` (AXE 현재 설정, 2026-05-26)** | SSO 가 인증만, session lifecycle 은 Vaultwarden 자체 **30일 idle refresh token** 사용. user 가 30일 안 idle 안 하면 무한 갱신 | **적용 효과** (AXE 4 user): - SSO+MP 재입력 = 약 월 1회 (idle 30일 + browser 재시작 시) - Touch ID/PIN unlock 은 daily **trade-off**: - Entra 측 user 비활성/제명이 server session 에 ≤30일 lag - offboarding 시 운영자가 수동으로 vault 측에서도 user 비활성 (UI: AXE org → Manage → Members → user → Remove + admin panel 에서 user disable) — AXE 4명 규모 충분 처리 가능 - customer 규모 ↑ 시 재평가 (별 결정) ### 다른 관련 env | env | 효과 | |---|---| | `SSO_ONLY=false` | MP 단독 fallback 유지 ([D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions)) | | `SIGNUPS_ALLOWED=true` | JIT user provisioning (SSO 신규 user 자동 생성) | | `SSO_SCOPES="openid profile offline_access User.Read"` | offline_access = refresh token 발급 | | `ORGANIZATION_INVITE_AUTO_ACCEPT=true` | invite Accept 단계 자동 통과 | **Vaultwarden 미지원 (의도)**: - ❌ Maximum Vault Timeout — Bitwarden commercial license, AGPLv3 비호환 ([source comment](https://github.com/Timshel/vaultwarden/blob/main/src/db/models/org_policy.rs): `// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)`) - ❌ Disable Personal Vault Export — 동일 license 사유 - ❌ Activate Autofill, Automatic App LogIn — Vaultwarden 미구현 → vault timeout 의 **server-side 강제는 영구 영역 외**. `SSO_AUTH_ONLY_NOT_SESSION=true` 가 가용 영역 안 best 대체. ## Layer 2 — Org Policies (Bitwarden 호환) Org Owner 가 `/api/organizations//policies/` ([Bitwarden 정책 docs](https://bitwarden.com/help/policies/)) 또는 web UI 로 설정. Vaultwarden 의 [supported set](https://github.com/Timshel/vaultwarden/blob/main/src/db/models/org_policy.rs#L26) 안에서. ### 현재 AXE org 적용 (2026-05-26, [D-ops-40](/ops/decisions) Progress) | type | 정책 | 값 | 효과 | |---|---|---|---| | 1 | MasterPassword | min 12자, complexity 1 | MP **변경 시점** 검증. 기존 MP 영향 0 (paranoia 옵션) | | 2 | PasswordGenerator | length 20, upper+lower+number+special | item 신규 생성 시 강한 default | | 3 | SingleOrg | enabled | member 가 다른 org 가입 차단 → 데이터 누설 방지 | | 5 | PersonalOwnership | enabled | 신규 item 은 org collection 필수 ([D-ops-32](/ops/decisions) 본질 정합) | **의도적 미설정** (D-ops-38 + D-ops-26+27): | type | 정책 | 왜 안 켜나 | |---|---|---| | 0 | TwoFactorAuthentication | Entra SSO 가 이미 MFA 역할 ([D-ops-38](/ops/decisions)) — vault 자체 2FA 중복 | | 8 | ResetPassword | 운영자 직권 reset 보존 (default), 강제 policy 불필요 | | 14 | RemoveUnlockWithPin | PIN unlock = 일상 편의 핵심, 차단하면 user 경험 악화 | ### Vaultwarden 미지원 (Bitwarden Cloud 에서는 가능): | type | 정책 | 사유 | |---|---|---| | 4 | RequireSso | Vaultwarden 미구현 (의도 — MP 단독 fallback 보존 정책과 정합) | | 9 | MaximumVaultTimeout | AGPL 비호환 | | 10 | DisablePersonalVaultExport | AGPL 비호환 | ### API 호출 패턴 ```bash # AXE org policy 조회 export BW_SESSION="$(security find-generic-password -s 'axe.vault.session' -w)" AI_USER=0ef2361b-5a69-4c23-973d-e978a4d55512 # ai@ ORG=0c5d8bbd-ad85-42b4-8b8a-2849031981b1 # AXE TOKEN=$(jq -r ".\"user_${AI_USER}_token_accessToken\"" \ "$HOME/Library/Application Support/Bitwarden CLI/data.json") curl -sf -H "Authorization: Bearer $TOKEN" \ "https://axe.axelabs.ai/vault/api/organizations/$ORG/policies" \ | jq '.data | map({type, enabled})' # 정책 변경 (예: MasterPassword 정책 수정) curl -sf -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ "https://axe.axelabs.ai/vault/api/organizations/$ORG/policies/1" \ -d '{"type":1,"enabled":true,"data":{"minComplexity":2,"minLength":14,"requireUpper":false,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"enforceOnLogin":false}}' ``` token 만료 시 (~1h) `bw sync` 로 refresh. ## Layer 3 — Per-client preferences vault timeout / PIN / Touch ID / theme = **각 device 의 client localStorage**. server 가 못 건드림 (Bitwarden 디자인 — privacy by design, prefs 가 user device 안). 정책 부재 시 각 client 의 첫 사용에서 user 가 선택. ### AXE 표준 setup (KDF rotation 완료 후 권장) **4 user 모두 + 모든 새 직원** 다음 3 client 설정: #### A. Web vault (보조용, https://axe.axelabs.ai/vault) | field | 권장 | |---|---| | Vault timeout | `On browser refresh` (web vault 의 최장 옵션 — `Never` 노출 안 됨) | | Vault timeout action | `Lock` (Logout 절대 X — SSO 재진입 비용) | 설정 위치: 우상단 본인 이름 → Account settings → Preferences #### B. Chrome extension (일상 brower-bound) | field | 권장 | |---|---| | Server URL (self-hosted) | `https://axe.axelabs.ai/vault` | | Vault timeout | **`Never`** (extension 은 `Never` 노출됨) | | Vault timeout action | `Lock` | | Unlock with PIN code | ✅ + PIN 4~12자 (e.g. `axe-2026` word) | | Lock with master password on browser restart | ✅ (paranoia 균형) | #### C. Native macOS app (daily driver — 가장 편함) | field | 권장 | |---|---| | Server URL (self-hosted) | `https://axe.axelabs.ai/vault` | | Vault timeout | `Never` | | Vault timeout action | `Lock` | | Unlock with Touch ID | ✅ (macOS Secure Enclave) | | Lock with master password on browser restart | ✅ | **실 효과** (3 layer 누적): - daily unlock = Touch ID (수 초) - 주 1-2회 = PIN (Chrome ext) - 월 1회 (idle 30일 또는 paranoia 재시작) = MP ## Vault scope — 무엇을 보호하는가 (trust boundary) Vault 는 **secret 의 저장 + retrieval + access control** 만 담당하는 저장소. vault item 을 delete/revoke 했다고 해서 그 secret 이 의미하는 **외부 service 의 권한·계정·2FA 가 자동으로 무효화되지 않음**. 두 영역을 혼동하면 offboarding/사고 대응 시 access 잔존 위험. ### 경계 도식 ``` ┌─ Vault scope (이 페이지 + Vaultwarden 이 통제) ──────────┐ │ • secret 값의 저장 / 암호화 (Argon2id + KDF) │ │ • org collection 기반 access control (Layer 2) │ │ • SSO + MP unlock lifecycle (Layer 1, 3) │ │ • item delete/revoke → 그 저장 위치 에서 차단 │ └──────────────────────────────────────────────────────────┘ ↓ vault item 은 외부 service 의 "거울" 일 뿐 ┌─ 외부 service scope (vault 가 못 건드림) ─────────────────┐ │ • 해당 service 측 계정 자체 (Microsoft / Google / ...) │ │ • TOTP secret 의 generation (해당 service 가 발급) │ │ • OAuth client_secret / refresh token invalidation │ │ • GitHub PAT, API key 의 server 측 revoke │ │ • customer 측 데이터 자체 (KB / 홈택스 export 등) │ └──────────────────────────────────────────────────────────┘ ``` → **회수 범위 = vault item delete + 외부 service 측 절차**. 둘 다 해야 완전. ### 회수 대상별 분리표 | 회수 대상 | vault 가 처리 | 외부 service 측 절차 필요 | |---|---|---| | service password 저장값 | ✅ item delete/revoke | 해당 service 의 password reset (admin 또는 본인) | | TOTP secret (2FA) | ✅ item delete | 해당 service 의 2FA reset → TOTP secret regenerate | | OAuth client_secret | ✅ item delete | Azure / Google / GitHub 측에서 client_secret 재발급 + 옛값 폐기 | | GitHub PAT | ✅ item delete | GitHub Settings → Personal access tokens → Revoke (server 측 token row 무효화) | | API key (Anthropic / OpenAI / etc) | ✅ item delete | 해당 provider 의 console 에서 key revoke | | KB / 홈택스 / 회계법인 portal admin 계정 | ❌ vault scope 외 (계정 자체는 vault 가 보관 안 함) | 해당 portal admin 의 계정 비활성/삭제 | | Microsoft 365 / Entra ID 계정 | ❌ vault scope 외 | customer IT 가 Entra ID 에서 user 비활성 | | customer 측 데이터 (Excel, PDF 등 export 본) | ❌ vault scope 외 | customer 자체 DLP / device wipe / 법적 요구 | ### 함의 - **offboarding 시**: [/ops/runbook/employee-offboarding](/ops/runbook/employee-offboarding) 의 "외부 service 권한 회수" 표를 vault item 제거와 **반드시 병행**. - **secret rotation 시**: vault item 값을 새로 채우는 것 만으로는 옛값이 외부 service 측에서 살아있음 — [/ops/runbook/secret-rotation](/ops/runbook/secret-rotation) 의 외부 service rotation step 동반. - **사고 대응 시**: vault item delete = "이 운영자 머신/AXE org 안에서 더 못 꺼냄". 그 secret 이 leak 된 정황이면 외부 service 측 revoke 가 본질. ## 3 layer 누적 그림 ``` ┌──────────────────────────────────────────────────────────┐ │ Layer 3: Per-client preferences (user 자율) │ │ • web `On browser refresh` Lock │ │ • extension `Never` + PIN │ │ • native `Never` + Touch ID │ │ ↓ 위 layer 의 ceiling 안에서 user 편의 조정 │ ├──────────────────────────────────────────────────────────┤ │ Layer 2: Org Policies (Owner 통제, member 전원 적용) │ │ • MasterPassword min 12, complexity 1 │ │ • PasswordGenerator length 20 │ │ • SingleOrg │ │ • PersonalOwnership │ │ ↓ 모든 member 자동 │ ├──────────────────────────────────────────────────────────┤ │ Layer 1: Server env (운영자 통제, 전 instance) │ │ • SSO_AUTH_ONLY_NOT_SESSION=true → 30일 session │ │ • SSO_ONLY=false → MP fallback │ │ • SIGNUPS_ALLOWED=true → JIT │ │ ↓ 인스턴스 기본값 │ └──────────────────────────────────────────────────────────┘ ``` ## 새 직원 onboarding 의무 step 1. **KDF rotation** (1회) — [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) runbook - 옛 PBKDF2 → Argon2id 로 vault re-encrypt - SSO→MP wrapped variant 정합 ([D-ops-40](/ops/decisions)) 2. **Layer 3 setup** (1회 per device) — 위 Web/Extension/Native 표 그대로 3. 끝 — 이후 일상은 Touch ID/PIN 만 운영자가 1번 안내 1회 + 2번 표 공유 1회 = onboarding 끝. layer 1+2 는 server 가 자동. ## Customer (Truvia 등) — sovereignty | Layer | customer 자율도 | |---|---| | Layer 1 (server env) | **customer 자체 통제** — customer 의 macmini compose, customer 자율. AXE 가 권장만 (이 페이지 참조 안내) | | Layer 2 (org policies) | **customer org Owner 가 설정** — AXE 가 정책 enforce 불가. 권장 (위 4 정책 동일) | | Layer 3 (per-client) | customer 직원 본인 — 동일 setup 권장 | → customer 운영자에게 본 페이지 link 만 전달, 권장 follow. [/services/vault](#) 의 customer onboarding 단계에 vault setup 포함 (TODO). ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | Maximum Vault Timeout 정책 시도 (Bitwarden 패턴) | HTTP 400 "Invalid or unsupported policy type" | Vaultwarden 미지원 (AGPL). 대신 `SSO_AUTH_ONLY_NOT_SESSION=true` 사용 | | `SSO_ONLY=true` + Entra ID 장애 | 운영자 vault 진입 불가 (lockout) | `SSO_ONLY=false` 유지 ([D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions)) — MP fallback 보존 | | RemoveUnlockWithPin 정책 enable | PIN unlock 불가 → 일상 MP 입력 강제 | 본 정책 enable 금지 (paranoia 와 편의 균형) | | 옛 KDF (PBKDF2) user 가 SSO→MP unlock 실패 | client 가 wrapped MP 변환 불가 | user 별 KDF rotation 의무 (onboarding step 1) | | Org policy 변경이 즉시 client 에 반영 안 되는 듯 | client 가 sync 시점에 fetch | bw sync 또는 client 강제 sync (일반적 ≤1분 안 자동) | | 운영자가 Layer 3 강제 시도 (Chrome managed policy 등) | macmini 별 enrollment 필요 — overhead 큼 | AXE 4명 규모 = docs 안내 + 신뢰. 10+ 직원 시점에 재평가 ([B-vault-chrome-mdm-policy-distribution](#)) | | Entra user 비활성 후 ≤30일 server session 잔존 | offboard 직후 vault 접근 가능 | offboard 시 운영자가 vault admin panel 에서 user disable 같이 실행 (manual checklist) | ## 관련 - [D-ops-37](/ops/decisions) — AXE-local Vaultwarden fork build - [D-ops-40](/ops/decisions) — axe.3 release plan + Progress (본 정책 결정) - [D-ops-38](/ops/decisions) — 외부 service 2FA = vault TOTP, 내부 2FA skip - [D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions) — SSO_ONLY=false + MP fallback - [/architecture/auth](/architecture/auth) — Entra ID + OAuth-RP middleware - [/architecture/secrets](/architecture/secrets) — 서비스 secret 관리 (vault item) - [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) — KDF rotation runbook - [B-vault-collection-migration-v1](/ops/backlog) — personal vault → org collection 정리 --- # 용어 사전 (Glossary) > AXE Labs docs 에서 반복 등장하는 약어와 용어 정의. URL: https://docs.axelabs.ai/glossary # 용어 사전 AXE Labs docs 전반에서 반복 등장하는 약어·용어 정의. 처음 보는 약어 있을 때 여기를 참조. ## 인증·OAuth | 용어 | 정의 | |---|---| | **MCP** | Model Context Protocol. Anthropic 표준으로 AI 가 외부 도구·데이터에 접근하는 방식. HTTP 또는 stdio transport. | | **FastMCP** | Python 기반 MCP server 구현 라이브러리. `@mcp.tool()` 데코레이터로 도구 등록. | | **SSE** | Server-Sent Events. HTTP 위에서 server → client 단방향 streaming. MCP HTTP transport 의 한 방식. | | **OAuth 2.0** | 인증·인가 표준 (RFC 6749). "사용자 비밀번호 공유 없이 권한 위임" 의 방식. | | **OIDC** | OpenID Connect. OAuth 2.0 위에 사용자 신원 확인 추가 (id_token 발급). | | **RP** | Relying Party. OIDC 에서 신원을 의존하는 측 (= 우리 frame, blueprint). Identity Provider (Microsoft) 는 IdP. | | **PKCE** | Proof Key for Code Exchange (RFC 7636). 모바일/SPA 같은 public client 의 인증 코드 가로채기 방지. `code_challenge` + `code_verifier` 쌍. | | **JWT** | JSON Web Token. claim 들을 JSON 으로 담은 signed token. `header.payload.signature` 형식. | | **JWKS** | JSON Web Key Set. JWT 서명 검증용 공개 키 목록. Microsoft 가 `https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys` 로 제공. | | **RS256** | RSA-SHA256 비대칭 서명. JWT 서명 알고리즘. asymmetric — 공개키로 검증, 비밀키로 서명. | | **HS256** | HMAC-SHA256 대칭 서명. 동일 비밀키로 서명+검증. frame 의 자체 발급 JWT 가 사용. | | **AS** | Authorization Server. OAuth 에서 토큰을 발급하는 서버. Microsoft Entra ID 가 우리의 AS. | | **App ID URI** | Microsoft Entra ID 에서 app 의 식별 URI. `api://137fc0ef-...` 또는 `https://axe.axelabs.ai/frame/mcp`. access_token 의 `aud` (audience) claim 으로 들어감. | | **AADSTS** | Azure Active Directory STS error code prefix. 6자리 숫자 (예: AADSTS9010010). 모든 Microsoft 인증 실패의 진단 코드. | ## 데이터·DB | 용어 | 정의 | |---|---| | **SSOT** | Single Source of Truth. 한 정보의 정본이 한 파일에만 있도록 하는 원칙. `customers.yaml` 이 멀티테넌트 SSOT. | | **RLS** | Row-Level Security (PostgreSQL). 한 테이블 안에서 row 단위 권한 분리. `SET app.tenant_id = X` 같은 session 변수로 자동 필터링. | | **Schema-per-entity** | 한 PostgreSQL DB 안에 entity 별로 별도 schema 둠. frame 의 `axec`, `axev` 가 그 예. cross-schema 조회 불가 = 격리. | | **PII** | Personally Identifiable Information. 주민번호·계좌·주소 등. frame 은 `pgcrypto.pgp_sym_encrypt` 로 entity 별 passphrase 로 암호화. | | **Bitemporal** | 두 시간 축 (`valid_from/to` + `recorded_at`) 으로 "어떤 시점에 무엇이 사실이었는가" + "그 사실을 언제 알았는가" 모두 기록. hive 의 `employment_records` 가 그 예. | | **ltree** | PostgreSQL 의 hierarchical tree 자료형 + 인덱스. hive 의 조직도 트리에 사용. `org.axec.finance.team1` 같은 path. | | **Append-only** | 한 번 쓴 row 는 UPDATE/DELETE 불가, 새 row 만 가능. frame 의 `journal_line`, `raw_transaction` 이 그 예. | ## 인프라 | 용어 | 정의 | |---|---| | **Cloudflare Tunnel** | 외부에 포트를 노출하지 않고 Cloudflare 측에서 origin 으로 outbound TCP 만으로 트래픽 받는 방식. `axelabs-tunnel` 컨테이너가 그 역할. | | **cloudflared** | Cloudflare Tunnel 의 connector binary. `docker.io/cloudflare/cloudflared`. | | **Caddy** | Go 기반 reverse proxy + HTTPS auto-cert. `axe-frame-proxy` (port 3712) 가 frame blue/green selector 로 사용. | | **launchd** | macOS 서비스 매니저 (Linux 의 systemd 격). `com.axe.backup.local` 같은 plist 등록. | | **Tailscale** | Mesh VPN. AXE 운영자 ↔ customer macmini 간 push deploy 의 transport. SSH key 와 별도 layer. | | **restic** | Open-source encrypted incremental backup tool. 3-tier backup (local + ring + cold SSD) 모두 같은 도구. | | **Keychain** | macOS 의 secret store. `security add-generic-password -a axe-cli -s axe.backup.restic.local -w ...` 로 등록. | ## AXE 고유 용어 | 용어 | 정의 | |---|---| | **customer** | AXE 의 고객 회사 (axe, realchoice). 각자 macmini 1대 보유. | | **entity** | 한 customer 안의 법인 단위 (axec, axev, realchoice). DB schema 격리 단위. | | **operator** | 운영 주체 = 액스코퍼레이션 주식회사 (`axec` entity). 자동화 계정 `ai@axellc.com` 이 단일 CLI 진입점. | | **D-ops-N / D-config-N / D-hive-N** | 누적 의사결정 번호. 영문 N 은 시간순 번호. 예: D-ops-15 = OAuth proxy 시도 (dormant). | | **blue/green** | 두 버전이 동시 가동 + alias 이동으로 무중단 deploy. frame 만 적용 (port 3710/3711). | | **axe-frame-proxy** | Caddy 컨테이너 (port 3712). 외부에서 들어온 `/frame` 트래픽을 blue 또는 green 중 active 쪽으로 라우팅. | ## 관련 페이지 - [아키텍처 결정](/ops/decisions) — D-ops / D-config / D-hive 전체 목록 - [인프라 인벤토리](/ops/inventory) — 포트 · 컨테이너 · launchd - [인증 · 권한](/architecture/auth) — Microsoft Entra ID OAuth 흐름 상세 --- # 신규 직원 온보딩 > 회사 이메일로 AXE Labs 플랫폼에 접속하는 4 단계. URL: https://docs.axelabs.ai/onboard # 신규 직원 온보딩 AXE Labs 플랫폼에 접속하기 위해 필요한 것은 **회사 이메일 + Claude 구독** 입니다. ## 사전 조건 (운영자가 미리 처리) 운영자 (`ai@axellc.com`) 가 다음을 완료한 상태여야 합니다: - [ ] 회사 Microsoft Entra ID 계정 발급 - [ ] `customers.yaml` 의 `user_entity_map` 또는 `default_entities_by_domain` 에 본인 매핑 - [ ] (선택) 본인이 작업할 entity 권한 부여 (예: `axec:read,write`) 신규 직원의 운영자 측 단계는 [신규 employee 등록 runbook](/ops/runbook/customer-onboarding) 참조. ## 4-step 설정 1. **Claude 구독**: 회사 이메일 (`@axellc.com`) 로 [claude.ai](https://claude.ai) 가입 (개인 결제 또는 회사 결제) 2. **[Frame connector 설정](/onboard/claude-frame-setup)**: claude.ai 에 회사 회계 시스템 연결 (5분, 1회) 3. **(선택) [M365 connector](/onboard/m365-connector)**: OneDrive·Outlook·Teams 통합 4. **Claude Code 네이티브 앱**: 같은 claude.ai 계정으로 로그인 → connector 자동 동기화 > **터미널·Claude Code·Codex·cron 에서 쓰려면** (claude.ai 커넥터 대신): [AXE CLI](/services/cli) 의 **"에이전트에 붙여넣기"** 블록을 본인 에이전트에 한 번 복붙하면 로그인 1회로 전 서비스 사용. 커넥터는 claude.ai 웹 챗용, CLI 는 셸 가진 모든 곳용. ## 사용 방법 설정 끝나면 채팅에서 자연어로 호출: ``` axec 의 5월 미결 항목 보여줘 → Frame:list_open_items 자동 호출, entity_id=axec ``` ``` axec 와 axev 의 이번 분기 잔액 비교해줘 → Frame:query_balance 두 번 호출, 결과 비교 ``` ``` axec 에 5월 28일 매출 1000만원 계상해줘 — Revenue 1000, Cash 1000 → Frame:post_journal 자동 호출 (write 권한 필요) ``` Entity 명시 (`axec`, `axev`, `realchoice`) 가 정확. 미명시 시 Claude 가 되묻습니다. ## 권한 모델 각자 부여된 권한 내에서만 작업 가능: | 권한 | 가능한 작업 | |---|---| | `read` | 잔액 조회, 분개장, 미결 조회 | | `write` | 분개 등록, 미결 생성/해결 | | `close` | 회계 기간 마감 | | `admin` | 계정과목 변경 (운영자만) | 권한 부족 시 Claude 가 "이 작업은 write 권한이 필요하나 현재 read 만 부여되어 있습니다" 식으로 알려줍니다. ## 다음 - [Frame connector 4-step 설정](/onboard/claude-frame-setup) - [문제 해결](/onboard/troubleshooting) --- # AXE MCP connectors (claude.ai + Claude Code 자동 sync) > claude.ai 의 Custom Connector 등록에 Bitwarden 브라우저 확장 + AXE Vault `MCP Connectors` collection 활용. 4 필드 (Name / URL / Client ID / Client Secret) 를 4 곳에서 찾던 옛 방식 폐기 — 확장 한 번 열면 catalog 자동 suggest. Claude Code 로컬 앱은 같은 claude.ai 계정 로그인 시 자동 sync. URL: https://docs.axelabs.ai/onboard/claude-connectors # AXE MCP connectors axelabs 가 운영하는 MCP server (Frame · Hive · Blueprint 등) 를 본인 Claude 에 등록하는 표준 절차. 4 필드 (이름 / URL / Client ID / Client Secret) 를 4 곳에서 찾던 옛 방식 ([/onboard/claude-frame-setup](/onboard/claude-frame-setup) — legacy, MCP 마다 반복) 폐기. Bitwarden 브라우저 확장이 vault `MCP Connectors` collection 에서 URI 매칭으로 자동 suggest ([D-vault-mcp-catalog](/ops/decisions)). > **커넥터는 claude.ai 웹 챗 통합용.** 터미널·Claude Code·Codex·Cursor·cron 처럼 셸이 있는 곳에선 커넥터를 서비스마다 등록할 필요 없이 [AXE CLI](/services/cli) 한 줄 복붙 — 로그인 1회로 전 서비스. 그 페이지의 **"에이전트에 붙여넣기"** 블록을 본인 에이전트에 그대로 붙이면 끝. ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/onboard/claude-connectors 따라 내 머신에 AXE MCP connector 추가해줘. 진행: 1. 내 OS + 브라우저 확인 (Chrome/Firefox/Safari/Edge) 2. Bitwarden 브라우저 확장 설치 + 본인 vault server URL 등재 + unlock 상태 진단 3. 본인 claude.ai 회사 이메일 로그인 확인 4. 페이지의 각 Step 명령 실행 + 검증, 매 step 결과 받고 다음 5. 함정 발생 시 페이지 "함정 정리" 표 따라 우회 6. Claude Code 로컬 앱 (macOS/Windows) 자동 sync 검증 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## Prereq - AXE 임직원 (`@axellc.com`) 또는 customer 직원 (`@`) email - 본인 AXE Vaultwarden 계정 (운영자 / customer IT 가 Entra SSO 가입 완료) + `MCP Connectors` collection access 권한 - 본인 claude.ai 계정 — **회사 이메일** 로그인 (개인 gmail 등 불가) - 머신: macOS / Windows / Linux + 모던 브라우저 ## Step 1: Bitwarden 브라우저 확장 install + vault server 등재 (이미 설치 + 로그인 돼 있으면 skip → Step 2) ### Chrome / Edge / Brave [Chrome Web Store — Bitwarden Password Manager](https://chrome.google.com/webstore/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb) 에서 추가. ### Firefox [Mozilla Add-ons — Bitwarden Password Manager](https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/). ### Safari App Store → "Bitwarden Password Manager" 설치 + Safari 환경설정 → 확장 프로그램 → Bitwarden 체크. ### Self-hosted server 등재 (필수) 확장 아이콘 클릭 → 로그인 화면 좌하단 **Settings** (gear) → **Self-hosted environment** → **Server URL** 에: | Customer | Server URL | |---|---| | axe (`@axellc.com` 직원) | `https://axe.axelabs.ai/vault` | | realchoice (`@realchoice.co.kr`) | `https://realchoice.axelabs.ai/vault` | | (다른 customer) | 운영자 / customer IT 가 안내 | Save → 확장 화면 복귀 → **Log in** → 회사 이메일 + 마스터 비밀번호 → unlock. 검증: 확장 popup 안에 본인 vault items 가 보이면 OK. **MCP Connectors** collection 클릭 시 N 개 (axe = 3 개: `Frame MCP (axe)` / `Hive MCP (axe)` / `Blueprint MCP (axe)`) 보이면 ready. > realchoice 외 customer 는 customer 측 vault 의 `MCP Connectors` collection 이 아직 미구축 가능 (Q3 sovereignty 마일스톤). 비어있으면 customer IT 에게 catalog 구축 요청 — 임시 우회는 옛 [/onboard/claude-frame-setup](/onboard/claude-frame-setup) hand-find 절차. ## Step 2: claude.ai 의 Custom Connector 추가 화면 열기 [claude.ai](https://claude.ai) → 우측 상단 프로필 → **Settings** → **Connectors** → **Custom connectors** → **+ Add Custom Connector** 클릭. 폼이 열림 — 4 필드: - **Name** (텍스트) - **Remote MCP server URL** (URL) - **Advanced** ▼ 펼치기 → - **OAuth Client ID** (텍스트) - **OAuth Client Secret** (password) > 이 폼이 열린 상태로 다음 Step. 닫지 말 것. ## Step 3: Bitwarden 확장에서 catalog item 의 4 필드 가져오기 브라우저 주소창이 `https://claude.ai/customize/connectors` (또는 `https://claude.com/customize/connectors`) 인 상태에서 **Bitwarden 확장 아이콘 클릭**. URI 매칭으로 `MCP Connectors` collection 의 항목들이 popup 상단 **"Items for this site"** 섹션에 자동 suggest: - `Frame MCP (axe)` - `Hive MCP (axe)` - `Blueprint MCP (axe)` 등록할 MCP 클릭 → 상세 화면. 각 필드의 **copy 버튼** ⎘ 으로 claude.ai 폼에 paste: | Bitwarden 항목 필드 | claude.ai 폼 필드 | 메모 | |---|---|---| | **Name** (= item 제목, 예: `Frame MCP (axe)`) | Name | 본인 화면 라벨 — 짧게 줄여도 OK (예: `Frame`) | | **Username** (= OAuth client_id GUID) | OAuth Client ID | Bitwarden 자동입력 가능성 있음 — 작동 안 하면 copy + paste | | **Password** (= OAuth client_secret, 마스킹됨) | OAuth Client Secret | popup 의 👁 아이콘으로 잠시 확인 가능. 자동입력 가능성 있음 — 작동 안 하면 copy + paste | | **Custom field "MCP URL"** | Remote MCP server URL | 표준 form field 아님 → 항상 copy + paste | > 💡 Client ID 와 Client Secret 의 차이: Client ID 는 공개 식별자 (메일 평문 전달 OK), Client Secret 은 비밀번호 (절대 노출 X). vault 가 두 값 모두 단일 SoT 로 관리 → 본인 손에 메모/종이 보관 0. > Bitwarden 의 자동입력 동작 여부는 claude.ai 폼의 필드 attribute (`autocomplete`, `type="password"` 등) 에 따라 다름. 자동입력 실패해도 popup 의 copy 버튼 4 클릭 으로 4 필드 채움 — 옛 방식 (4 곳 hand-find) 대비 결정적 단축. ## Step 4: Connect → Microsoft 로그인 → 첫 사용 시 동의 폼 4 필드 채운 후 **Connect** 버튼 → 브라우저가 Microsoft 로그인으로 redirect: 1. 회사 이메일 입력 (`firstname.lastname@axellc.com` 또는 customer 도메인) 2. 비밀번호 / Authenticator 3. **(첫 사용 시) 동의 화면** — `Access MCP` 권한 요청 → **Accept** 클릭 4. 자동으로 claude.ai 복귀 5. Connector 상태 = **"Connected"** (녹색 점) → 성공 > 두 번째 이후 등록 (같은 customer 의 다른 MCP) 은 같은 Microsoft 계정이라 동의 화면 skip — 바로 Connected. ## Step 5: 동작 확인 claude.ai 새 채팅에서 자연어 호출: | MCP | 자연어 예시 | Claude 가 호출하는 tool | |---|---|---| | Frame | `axec 의 5월 미결 항목 5 개 보여줘` | `Frame:list_open_items` | | Frame | `axec 4월 시산표` | `Frame:query_trial_balance` | | Hive | `이번 주 휴가 신청 보여줘` | `Hive:leave_get_request` | | Hive | `내 급여 명세 5월` | `Hive:payroll_get_my_payslip` | | Blueprint | `오늘 일정 추가: 14:00 회의실 A` | `Blueprint:create_event` | | Blueprint | `Teams 메시지 본문 fetch ` | `Blueprint:get_teams_message` | Claude 가 tool 호출 + 결과 자연어 정리 → 성공. 같은 절차로 나머지 MCP 도 Step 2~4 반복. catalog 가 3 개 다 보유 — **3 개 모두 등록 권장** (한 번 등록 후 영구 사용). ## Step 6: Claude Code 로컬 앱 자동 sync 같은 claude.ai 계정으로 **Claude Code 네이티브 앱** (macOS / Windows) 에 로그인하면 위에서 등록한 connector 들이 **자동 동기화** — 별도 설정 X. 검증: 1. Claude Code 앱 열기 2. `/status` → "Anthropic Account" 줄에 본인 회사 이메일 표시 3. `/mcp` → 위에서 등록한 connector 들이 목록에 보임 (`Frame` / `Hive` / `Blueprint` 등, claude.ai connector 표식 함께) 4. 채팅에서 자연어로 호출 가능 > Cursor / Cline / Witsy 같은 다른 MCP host 는 각자 별도 설정 — 본 페이지 범위 밖. 표준 `mcpServers` JSON 패턴은 동일하므로 catalog 의 4 필드 값 그대로 활용 가능 (각 host 의 config 형식만 차이). ## 함정 정리 | # | 증상 | 원인 | 우회 | |---|---|---|---| | 1 | Bitwarden 확장 popup 에 `MCP Connectors` collection 보이지 않음 | vault sync 안 됨 / unlock 만료 / 다른 customer vault server 에 로그인 | 확장 → Settings → **Sync vault** 클릭 / 확장 lock → unlock 재실행 / Step 1 의 server URL 본인 customer 와 일치 확인 | | 2 | claude.ai 페이지에서 확장 클릭 시 매칭 항목 0 — 다른 사이트의 일반 item 만 보임 | catalog item 의 URI 가 등록되지 않음 (catalog 갱신 누락) | 운영자 (`ai@axellc.com`) 에게 보고 — `axe mcp publish` 재실행 필요. 임시 우회 = `MCP Connectors` collection 을 확장 안에서 직접 열어 item 수동 copy | | 3 | Bitwarden 자동입력이 OAuth Client ID/Secret 칸에 작동 안 함 | claude.ai 폼의 필드 attribute 가 표준 `username`/`password` 아님 | popup 의 각 필드 옆 **copy 버튼** 사용 + 폼에 paste (4 클릭) — 정상 우회 | | 4 | "Remote MCP server URL" 칸이 자동입력 안 됨 | URL 은 custom field (`MCP URL`) 라 표준 form field 아님 | item popup → Custom fields 섹션 → MCP URL 옆 copy 버튼 → 폼에 paste — 항상 manual (claude.ai 폼 구조상 영구) | | 5 | Connect 클릭 후 Microsoft → `AADSTS50011: redirect_uri mismatch` | 본인 customer 외 다른 customer 의 URL 입력 (예: realchoice 직원이 axe URL) | Step 1 의 customer 별 server URL 표 + Step 3 의 item 본인 customer 의 것 확인 | | 6 | Connect 후 Microsoft → `AADSTS500011: resource not found` | MCP app 의 `application_id_uri` 가 본인 회사 Entra tenant 에 등록 안 됨 (다른 customer 의 vault item 일 수 있음) | 본인 customer 의 vault 사용 확인 / 운영자 / customer IT 에 보고 | | 7 | 동의 화면이 안 뜨고 바로 Connected | 이미 처음 한 번 동의함 — 재동의는 Settings → Connectors → Disconnect → re-Connect 시점에만 발생 | 정상. 권한 변경 필요 시 Disconnect 후 재연결 | | 8 | Connected 표시 후에도 채팅에서 tool 호출 안 됨 | claude.ai 가 tool 목록 fetch 중 또는 connector 비활성 | 1~2 분 대기 후 새 채팅 / Settings → Connectors → 본 connector toggle off → on | | 9 | Bitwarden 확장에 master password 입력 후에도 `MCP Connectors` 안 보임 | 본인이 해당 collection 의 멤버 아님 (organization 권한 미부여) | 운영자 (`ai@axellc.com`) 에게 본인 email 으로 collection access 요청. axe 측 = ai / soohun / taehun / jinwoo 4명 기본 access | | 10 | Claude Code 네이티브 앱에 `/mcp` 가 비어있음 (Step 6) | claude.ai 와 다른 Anthropic 계정으로 로그인됨 / sync 지연 | `/login` → claude.ai 와 같은 회사 이메일 / Claude Code 앱 재시작 → 1~2 분 대기 | ## 매 작업 시 사전 작업 | 빈도 | 작업 | 자동화 | |---|---|---| | 1회 (초기 setup) | Step 1~4 (extension + vault + claude.ai 등록) | — | | Vault timeout 마다 | Bitwarden 확장 unlock (master password) | ✅ Bitwarden 의 vault timeout 설정 (확장 → Settings → Vault timeout) | | Secret rotation 발생 시 | (없음 — 운영자가 `axe secret rotate` 실행 시 catalog 자동 재발행, Bitwarden sync 후 확장에서 새 secret 자동) | ✅ [D-vault-mcp-catalog](/ops/decisions) step 4.5 hook | | 매 claude.ai 사용 | (없음) | — | ## 참조 - [D-vault-mcp-catalog](/ops/decisions) — 본 catalog 모델 (Vaultwarden org collection + `axe mcp publish` CLI + `axe secret rotate` 자동 hook) - [/architecture/secrets#mcp-connectors-catalog-d-vault-mcp-catalog-2026-05-26](/architecture/secrets) — catalog 구조 도식 + 신규 MCP 추가 3-step 표준 절차 - [/onboard/claude-frame-setup](/onboard/claude-frame-setup) — Frame 단독 connector 옛 절차 (4 필드 hand-find, legacy — 본 페이지로 대체 권장) - [/onboard/vault-setup](/onboard/vault-setup) — Vault setup 표준 (KDF + 4 client 셋업) - [/onboard/m365-connector](/onboard/m365-connector) — Microsoft 365 connector (별 MCP 가 아님, 보조 reading) - [B-customer-sovereignty-architecture](/ops/backlog) — customer 측 vault catalog 분리 (Q3 milestone) - [/ops/known-gaps](/ops/known-gaps) "매니페스트 / vault 측 미해결" — bw cache stale 함정 (운영자 측, end-user 무관) --- # Frame connector 설정 > claude.ai 에 회사 회계 시스템 (Frame) 연결하는 5분 절차. URL: https://docs.axelabs.ai/onboard/claude-frame-setup # Frame connector 설정 (claude.ai) > **🔁 신규 권장 절차** = [/onboard/claude-connectors](/onboard/claude-connectors) (Bitwarden 확장 + AXE Vault `MCP Connectors` collection 으로 Frame 포함 모든 MCP 한 번에 자동 suggest). 본 페이지는 단일 connector 의 hand-find 절차 (legacy, 2026-05-26 이전) 보존용. 회사 회계 시스템 (Frame) 에 claude.ai 에서 자연어로 접근하기 위한 1회 설정. ## 본인 단계 (5분) ### 1. claude.ai 에 회사 이메일로 로그인 [claude.ai](https://claude.ai) → 회사 이메일 (`@axellc.com` 또는 본인 회사 도메인) 로 로그인. > 개인 이메일 (gmail 등) 로 가입한 계정은 회사 데이터 접근 불가. 회사 이메일 사용 필수. ### 2. Custom Connector 추가 claude.ai 우측 상단 프로필 → **Settings** → **Connectors** → **Custom connectors** → **+ Add Custom Connector** 클릭. ### 3. 입력값 4 개 | 필드 | 값 | 비밀성 | |---|---|---| | **Name** | `Frame` | 공개 (자기 화면 라벨) | | **Remote MCP server URL** | `https://axe.axelabs.ai/frame/mcp` (또는 회사 URL) | 공개 | | **Advanced** 펼치기 ↓ | | | | **OAuth Client ID** | 운영자가 준 GUID (예: `137fc0ef-...`) | **공개** (ID 일 뿐 — 메일에 평문 OK) | | **OAuth Client Secret** | 운영자가 준 secret 값 | **비밀** (Bitwarden Send 1회용 링크로만 전달됨, 입력 후 잊으세요) | > 💡 Client ID 와 Client Secret 의 차이: Client ID 는 "회사 출입증 번호 = 누구라도 봐도 됨", Client Secret 은 "비밀번호 = 절대 노출 X". 둘 다 함께 있어야 인증 가능. > URL 은 본인 회사 (customer) 에 따라 다릅니다. 운영자가 안내한 URL 사용: > - axe customer (axellc.com 직원): `https://axe.axelabs.ai/frame/mcp` > - realchoice customer (realchoice.co.kr 직원): 운영자가 onboard 완료 후 통지 (배포 시점에 활성) > > 다른 customer 의 URL 입력 시 Microsoft 가 redirect_uri mismatch (AADSTS50011) 로 거부합니다. > Client ID 는 평문으로 (Teams/메일 OK), Client Secret 은 Vaultwarden 의 Bitwarden Send 1회용 링크로 전달됩니다. 링크는 1회 열람 후 자동 무효 — 받자마자 connector 화면에 붙여 넣고 닫으세요. (운영자 측 절차: [/architecture/secrets § 사람에게 전달](/architecture/secrets#사람에게-전달--bitwarden-send)) ### 4. Connect → Microsoft 로그인 `Connect` 버튼 클릭 → 브라우저가 Microsoft 로그인 페이지로 이동: 1. 회사 이메일 입력 (`firstname.lastname@axellc.com` 등) 2. 비밀번호 입력 (또는 Authenticator 앱) 3. **첫 사용 시 동의 화면** — `Access Frame MCP` 권한 요청 → **Accept** 클릭 4. 자동으로 claude.ai 로 복귀 5. Connector 상태가 **"Connected"** (녹색) 로 표시되면 성공 ### 5. 동작 확인 claude.ai 새 채팅에서: ``` axec 의 5월 미결 항목 5개 보여줘 ``` Claude 가 `Frame:list_open_items` 호출하고 결과를 자연어로 정리해서 보여주면 끝. ## Claude Code 네이티브 앱 자동 동기화 같은 claude.ai 계정으로 Claude Code 네이티브 앱 (macOS / Windows) 에 로그인하면 **자동으로 connector 가 동기화** 됩니다 — 별도 설정 불필요. 확인: 1. Claude Code 앱 열기 2. `/status` 입력 → "Anthropic Account" 줄에 본인 이메일 표시 3. `/mcp` 입력 → `Frame` connector 가 목록에 보임 (claude.ai connector 표식 함께) 4. 채팅에서 `axec 의 미결 5개` 등 자연어로 호출 ## 일반 흐름 설정 완료 후 자주 쓰는 패턴: | 자연어 | Frame tool | |---|---| | "axec 의 5월 매출은?" | query_balance, list_journals | | "axec 의 미결 항목 보여줘" | list_open_items | | "axec 와 axev 의 이번 분기 잔액 비교" | query_balance × 2 | | "axec 에 매출 1000만원 계상" | post_journal (write 권한 필요) | | "axec 의 4월 시산표" | query_trial_balance | | "axec 의 5월 분개장" | list_journals | | "은행 입출금 파일 업로드: 파일첨부" | ingest_file_blob (write) | ## 보안 권장 - **Client Secret 을 다른 채널에 보관하지 마세요** — claude.ai 의 Advanced 칸에만 입력. 한 번 입력하면 다시 볼 일 없습니다. - **공용 PC 에서 claude.ai 로그인 후 로그아웃 잊지 마세요** — connector 가 활성 상태로 노출됨. - **회사 이메일이 의심스럽게 사용된 흔적이 있으면** 즉시 `ai@axellc.com` (운영자) 에게 신고. ## 문제 발생 시 [/onboard/troubleshooting](/onboard/troubleshooting) 참조 또는 운영자에게 다음 정보 전달: 1. 어느 단계에서 막혔는지 2. 오류 메시지 전문 (특히 `AADSTS` 코드) 3. 브라우저 주소창의 전체 URL ## 다음 - [Microsoft 365 connector](/onboard/m365-connector) — OneDrive·Outlook·Teams 통합 (선택) - [문제 해결 모음](/onboard/troubleshooting) --- # Microsoft 365 connector > OneDrive · Outlook · Teams 통합 (Anthropic 제공). URL: https://docs.axelabs.ai/onboard/m365-connector # Microsoft 365 connector ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/onboard/m365-connector 따라 본인 claude.ai 에 Microsoft 365 connector 추가해줘. 진행: 1. 본인 claude.ai 회사 이메일 (`@axellc.com` 또는 customer 도메인) 로그인 확인 2. 페이지 "Frame connector 와 무관" 표 따라 별도 인증 컨텍스트 인식 (M365 토큰 ≠ Frame 토큰, Anthropic 운영) 3. 페이지의 설정 5 step (claude.ai/customize/connectors → Microsoft 365 찾기 → Connect → Microsoft 로그인 → 권한 5종 동의) 매 step 결과 받고 다음 4. 함정 — 회사 IT 가 외부 OAuth app consent 차단 시 admin consent 요청 (요청 권한 5종 = User.Read · Mail.Read · Files.Read.All · Sites.Read.All · ChannelMessage.Read.All) 5. "Connected" 상태 확인 + 페이지 "사용" 예시 1개 (지난주 메일 검색 등) 으로 토큰 동작 검증 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. Anthropic 이 제공하는 공식 connector. 회사 이메일·OneDrive·Teams 메시지·SharePoint 파일에 Claude 가 read-only 로 접근. ## Frame connector 와 무관 M365 connector 는 Anthropic 이 운영합니다. Frame connector 와 **별도 인증** 합니다. 두 connector 의 토큰은 서로 공유되지 않습니다. | connector | 운영자 | 데이터 | |---|---|---| | Frame | AXE Labs (본인 회사 macmini) | 회계 | | M365 | Anthropic | OneDrive · Outlook · Teams · SharePoint | ## 설정 (2분) 1. [claude.ai/customize/connectors](https://claude.ai/customize/connectors) → **Microsoft 365** connector 찾기 (Anthropic 공식 목록) 2. **Connect** 클릭 3. Microsoft 로그인 (회사 이메일) 4. 권한 동의: - User.Read - Mail.Read - Files.Read.All - Sites.Read.All - ChannelMessage.Read.All (Teams) 5. 자동 복귀, "Connected" 상태 ## 사용 ``` 지난주 ai@axellc.com 한테 온 메일 중 'invoice' 키워드 있는 것 요약 → Microsoft 365:search_emails 호출 ``` ``` Finance OneDrive 폴더 의 2026-05 파일 목록 → Microsoft 365:list_files 호출 ``` ``` Teams '회계' 채널 의 최근 5일 메시지 보여줘 → Microsoft 365:list_channel_messages 호출 ``` ## Frame 과 조합 활용 ``` OneDrive Finance/2026-05 폴더의 carddata.xlsx 를 frame axec 에 ingest 해줘 → 1. Microsoft 365:download_file (read) → 2. Frame:ingest_file_blob (write to axec) ``` 이렇게 두 connector 가 협업. ## 보안 - M365 connector 는 **read-only** — Claude 가 OneDrive 의 파일을 수정/삭제할 수 없음. (read 만) - 토큰은 Anthropic 백엔드에 암호화 저장. 직원이 직접 볼 수 없음. - 직원이 access 권한 가진 파일만 Claude 가 봄 — 권한 자체는 회사 Microsoft 365 의 권한 그대로. ## 관련 - [Anthropic 공식 문서](https://support.claude.com/en/articles/12542951-enable-and-use-the-microsoft-365-connector) - [M365 보안 가이드](https://support.claude.com/en/articles/12684923-microsoft-365-connector-security-guide) --- # SSH 로컬 작업 (Claude Code / Cursor / 기타 AI session) > AXE macmini 에 SSH 진입해 로컬 작업하는 표준 절차. 사람이 read 하거나, AI session 에 본 페이지 URL 을 던지면 step-by-step interactive 진행. URL: https://docs.axelabs.ai/onboard/ssh-access # SSH 로컬 작업 ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/onboard/ssh-access 따라 내 머신을 SSH 셋업해줘. 진행: 1. 내 머신 OS 진단 (Windows/macOS/Linux) 2. 기존 환경 확인 (cloudflared 설치, ~/.ssh/id_ed25519 존재, ~/.ssh/config 등) 3. 페이지의 각 Step 명령 실행 + 검증, 매 step 결과 받고 다음 4. 함정 발생 시 페이지 "함정 정리" 표 따라 우회 5. Step 3 server-side 등록 = 운영자 ai (ai@axellc.com) 에 Teams DM 으로 내 public key + email 전달, 회신 받고 다음 진행 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## Prereq - AXE 임직원 (`@axellc.com`) 또는 customer 직원 (해당 customer 도메인) email - Microsoft Entra SSO 가능 — 본인 회사 IT 가 Entra 직원 등록 완료 - 머신: Windows 10/11 또는 macOS 또는 Linux ## Step 1: cloudflared 설치 ### Windows ```powershell winget install --id Cloudflare.cloudflared ``` 설치 후 **새 PowerShell 창** 열어서 검증: ```powershell cloudflared --version ``` 함정: 같은 PowerShell 창 안에서 검증 시 `cloudflared : The term 'cloudflared' is not recognized` — PATH 환경변수가 같은 shell session 에 갱신 안 됨. **새 창** 필수. ### macOS ```bash brew install cloudflare/cloudflare/cloudflared ``` ### Linux [Cloudflare 공식 install guide](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/). ## Step 2: ed25519 keypair 생성 본인 머신에 SSH key 가 없으면 생성: ``` ssh-keygen -t ed25519 -C "@axellc.com" ``` 기본 경로 (Enter 로 수락): - Windows: `C:\Users\\.ssh\id_ed25519` - macOS / Linux: `~/.ssh/id_ed25519` passphrase 입력 권장 (key 유출 시 안전장치). 검증 (public key 출력): ``` cat ~/.ssh/id_ed25519.pub ``` 출력 예: ``` ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzszmDFVQCu3ViuGwWV2NatlLhYozWalisAtw6xgQWh you@axellc.com ``` ## Step 3: server-side 등록 (운영자 ai 처리) 본인 public key 한 줄을 운영자 ai (`ai@axellc.com`) 에게 전달. 방법 2가지: **A. Teams DM 으로 전달** — 본인 1:1 채팅에 paste. 운영자 ai 가 메시지 받으면 자동 처리. **B. AI session 자동 forward** — 본인 Claude Code session 이 본 페이지 따라 진행 중이면 자동으로 운영자 ai 에 메시지 발송 (Blueprint MCP `get_teams_message` 패턴 — [B-bp-mcp-teams-admin-consent](/ops/backlog) 완료 후). 운영자 ai 가 수행하는 작업 (참고): ```bash # (a) public key append to axe user's authorized_keys (codebase 작업용) # ⚠️ axe 는 공용 계정 — install 은 destination replace 라 기존 직원 키 전부 삭제됨. # 반드시 tee -a 로 append. 권한·소유자는 기존 file 의 것이 유지됨 (file 이 이미 600/axe:staff). echo "" | sudo /usr/bin/tee -a /Users/axe/.ssh/authorized_keys >/dev/null # (b) SACL 추가 (PAM pam_sacl.so 통과) sudo /usr/sbin/dseditgroup -o edit -a -t user com.apple.access_ssh # (c) (선택) 본인 home directory 도 SSH 진입 허용 — 본인 personal 작업 시 (첫 등록 시만 install OK) sudo /usr/bin/install -d -m 700 -o -g dev /Users//.ssh sudo /usr/bin/install -m 600 -o -g dev /Users//.ssh/authorized_keys # 두 번째 키 추가 시에는 (a) 처럼 tee -a 로 append. ``` 전부 NOPASSWD whitelist 안 (`tee /Users/*` / `install` / `chown` / `dseditgroup`) — 운영자 password 입력 불요. 운영자 ai 회신 받으면 다음 step. ## Step 4: ~/.ssh/config 작성 ### Windows (PowerShell) ```powershell @' Host axe-macmini HostName ssh-axe.axelabs.ai User axe ProxyCommand "C:\Program Files (x86)\cloudflared\cloudflared.exe" access ssh --hostname %h IdentityFile ~/.ssh/id_ed25519 '@ | Out-File -Encoding ascii -FilePath $env:USERPROFILE\.ssh\config ``` ### macOS / Linux ```bash cat <<'EOF' >> ~/.ssh/config Host axe-macmini HostName ssh-axe.axelabs.ai User axe ProxyCommand cloudflared access ssh --hostname %h IdentityFile ~/.ssh/id_ed25519 EOF ``` 설명: - `HostName ssh-axe.axelabs.ai` — Cloudflare Tunnel 의 public hostname. 옛 `ssh.axe.axelabs.ai` 폐기됨 ([D-ops-39](/ops/decisions)). - `User axe` — OS account = `axe` (AXE platform codebase 공통 위치 `/Users/axe/` 진입). audit 식별은 Cloudflare Access SSO log + ssh key fingerprint + git author 3중. - `ProxyCommand cloudflared access ssh --hostname %h` — Cloudflare Access SSH 게이트 통과. Windows 는 절대 경로 + 따옴표. ## Step 5: Cloudflare Access 인증 (24h 1회) ``` cloudflared access login https://ssh-axe.axelabs.ai ``` 브라우저 자동 열림 → Microsoft 로그인 (본인 email) → 완료. JWT 토큰 발급 (24h 유효, `session_duration: "24h"` per Access app 설정). ## Step 6: 첫 ssh 시도 ``` ssh -o StrictHostKeyChecking=accept-new axe-macmini ``` `accept-new` = 첫 시도 시 host fingerprint 자동 등록 (사람 prompt 없이). `axe@AXEs-Mac-mini ~ %` prompt 떨어지면 성공. 함정: 단순 `ssh axe-macmini` 시도 시 `Host key verification failed` — host key prompt 가 non-interactive shell 에서 답을 못 받음. 반드시 `accept-new` 옵션. ## Step 7: AI session 등록 (Claude Code / Cursor / 등) ### Claude Code (Windows) — ProxyCommand 미지원 함정 Claude Code Windows native app 의 SSH backend 는 OpenSSH `ProxyCommand` 를 호출 안 함 → 동일 `~/.ssh/config` 인데 PowerShell 의 `ssh axe-macmini` 는 통과, Claude Code 는 timeout/handshake fail. **우회 = cloudflared TCP forward** (PC 부팅 후 1회): ```powershell Start-Process -FilePath "C:\Program Files (x86)\cloudflared\cloudflared.exe" -ArgumentList 'access tcp --hostname ssh-axe.axelabs.ai --url localhost:2222' ``` background 창이 자동으로 뜸 — 그대로 둠 (작업 종료 시까지). 첫 등록 시 known_hosts 추가: ``` ssh -o StrictHostKeyChecking=accept-new -p 2222 axe@localhost ``` Claude Code SSH 연결 다이얼로그: - 이름: `axe-macmini` - SSH 호스트: `axe@localhost` - SSH 포트: `2222` - Identity File: `~/.ssh/id_ed25519` 저장 → New Session → axe-macmini 선택 → `/Users/axe/` 하위 폴더 (axelabs, axelabs-docs, frame, hive, blueprint, .axe 등) 보임. ### Claude Code (macOS) — ProxyCommand 정상 macOS native ssh backend 라 ProxyCommand 작동. Step 4 의 `~/.ssh/config` 그대로 + Claude Code 다이얼로그에 host = `axe@axe-macmini`. TCP forward 불필요. ### TCP forward 자동화 (Windows, user-context Scheduled Task + VBS wrapper) cloudflared TCP forward 를 PowerShell 창에서 띄우면 창 종료 시 같이 죽음 (함정 #9). NSSM 등으로 SYSTEM 서비스화하면 JWT 토큰 격리로 origin 인증 무한 루프 (함정 #10). PowerShell `-WindowStyle Hidden` 는 초기 깜빡임이 있음 (함정 #13). Task 의 `-RestartCount` 는 FAILURE 에만 발동 — cloudflared 정상 종료 시 silent 중단 (함정 #14). 해결 = **user-context Scheduled Task + VBScript 무한 루프 wrapper**: - `wscript.exe` 호스팅 → 깜빡임 0 - VBS 내부 루프 → 어떤 종료 코드든 5초 뒤 재기동 - Hidden + AtLogOn + 본인 계정 → 본인 끌 UI 0 **Setup (관리자 PowerShell 한 번에)**: ```powershell New-Item -ItemType Directory -Path 'C:\ProgramData\cloudflared-ssh-axe' -Force | Out-Null # VBScript wrapper — 0-flash + 무한 재시작 @' Set sh = CreateObject("WScript.Shell") Do sh.Run """C:\Program Files (x86)\cloudflared\cloudflared.exe"" access tcp --hostname ssh-axe.axelabs.ai --url localhost:2222", 0, True WScript.Sleep 5000 Loop '@ | Out-File -FilePath 'C:\ProgramData\cloudflared-ssh-axe\tcp-forward.vbs' -Encoding ascii # Task 등록 $action = New-ScheduledTaskAction -Execute 'wscript.exe' ` -Argument '"C:\ProgramData\cloudflared-ssh-axe\tcp-forward.vbs"' $trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME" $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable ` -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries ` -ExecutionTimeLimit ([TimeSpan]::Zero) -Hidden Register-ScheduledTask -TaskName 'cloudflared-ssh-axe-tcp' ` -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force Start-ScheduledTask -TaskName 'cloudflared-ssh-axe-tcp' ``` **검증**: ```powershell Start-Sleep -Seconds 5 Get-NetTCPConnection -LocalPort 2222 -State Listen Get-Process cloudflared, wscript -ErrorAction SilentlyContinue | Select-Object Name, Id, StartTime # 가시적 창 0 확인 Get-Process cloudflared, wscript -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object Name, Id, MainWindowTitle ``` 마지막 쿼리가 빈 결과 = 진짜 hidden. 포트 2222 LISTENING + 프로세스 살아있으면 성공. **작동 특성**: - PowerShell 창 종료: cloudflared 살아있음 (task scheduler 자식 프로세스 트리) - cloudflared 정상/비정상 종료: VBS 루프가 5초 뒤 재실행 (`-RestartCount` 의존성 0) - 로그오프: cloudflared/wscript 죽음 → 재로그인 시 AtLogOn 자동 부활 - PC 재부팅: 로그온 후 자동 시작 - 본인 계정 컨텍스트로 실행 → `~/.cloudflared/` 토큰 캐시 공유 (renewal task 와 동일) - 가시적 창 0개 — 작업 관리자 "세부 정보" 또는 작업 스케줄러 GUI 까지 가야 보임 ## Token 자동 갱신 (Windows) 기존 문서: 24h 마다 사람이 `cloudflared access login` 수동 실행. 이를 자동화하는 self-rescheduling Scheduled Task. **설계 원칙**: - **폴링 없음** — 토큰의 실제 `exp` claim 을 파싱해서 그 시점에만 fire (24h 주기 task fire 2~3회) - **만료 5분 전 미리 갱신** → 다운타임 0 - **Self-rescheduling** — task 가 자기 trigger 를 재예약 (외부 cron 불필요) - **사람 클릭 0회 (조건부)** — Microsoft "로그인 상태 유지" 켜져 있으면 갱신 시 브라우저 깜빡 → SSO 자동 통과 **중요 — `-RunLevel Highest` 필수** (함정 #11): task 가 자기 자신의 `Set-ScheduledTask` 호출해야 해서 elevated 필요. Interactive 만으로는 `Access is denied`. ### Setup (관리자 PowerShell 한 번에) ```powershell $dir = 'C:\ProgramData\cloudflared-ssh-axe' New-Item -ItemType Directory -Path $dir -Force | Out-Null @' #Requires -Version 5.1 $ErrorActionPreference = 'Stop' $Config = @{ Cloudflared = 'C:\Program Files (x86)\cloudflared\cloudflared.exe' AppUrl = 'https://ssh-axe.axelabs.ai' LogPath = 'C:\ProgramData\cloudflared-ssh-axe\renewal.log' TaskName = 'cloudflared-token-renewal' RenewWindowMinutes = 5 PostLoginRecheckMinutes = 5 FallbackRetryMinutes = 60 } function Write-Log { param([string]$Message) "$([DateTime]::Now.ToString('s')) $Message" | Out-File $Config.LogPath -Append -Encoding utf8 if ((Get-Item $Config.LogPath).Length -gt 1MB) { Get-Content $Config.LogPath -Tail 200 | Set-Content $Config.LogPath -Encoding utf8 } } function Get-JwtExpiry { param([string]$Jwt) $payload = $Jwt.Trim().Split('.')[1].Replace('-','+').Replace('_','/') $payload += '=' * ((4 - ($payload.Length % 4)) % 4) $json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json [DateTimeOffset]::FromUnixTimeSeconds($json.exp).LocalDateTime } function Get-CurrentToken { try { $t = & $Config.Cloudflared access token "--app=$($Config.AppUrl)" 2>$null if ($LASTEXITCODE -eq 0 -and $t -and $t -notmatch 'Unable to find token|please login') { return $t.Trim() } } catch {} return $null } function Set-NextRun { param([DateTime]$When) $triggers = @( (New-ScheduledTaskTrigger -Once -At $When), (New-ScheduledTaskTrigger -AtLogOn) ) Set-ScheduledTask -TaskName $Config.TaskName -Trigger $triggers | Out-Null Write-Log "next run: $($When.ToString('s'))" } function Invoke-Login { Start-Process $Config.Cloudflared -ArgumentList @('access','login',$Config.AppUrl) -WindowStyle Hidden Write-Log 'triggered: cloudflared access login' } try { $token = Get-CurrentToken if ($null -eq $token) { Invoke-Login Set-NextRun (Get-Date).AddMinutes($Config.PostLoginRecheckMinutes) return } $exp = Get-JwtExpiry $token $minsLeft = [int]($exp - (Get-Date)).TotalMinutes Write-Log "token exp=$($exp.ToString('s')) (${minsLeft}min)" if ($minsLeft -lt $Config.RenewWindowMinutes) { Invoke-Login Set-NextRun (Get-Date).AddMinutes($Config.PostLoginRecheckMinutes) } else { Set-NextRun $exp.AddMinutes(-$Config.RenewWindowMinutes) } } catch { Write-Log "ERROR: $($_.Exception.Message)" try { Set-NextRun (Get-Date).AddMinutes($Config.FallbackRetryMinutes) } catch {} exit 1 } '@ | Out-File -FilePath "$dir\renew-token.ps1" -Encoding utf8 $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\ProgramData\cloudflared-ssh-axe\renew-token.ps1"' $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" ` -LogonType Interactive -RunLevel Highest # ← Highest 필수 (함정 #11) $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable ` -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries Register-ScheduledTask -TaskName 'cloudflared-token-renewal' ` -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force ``` ### 검증 ```powershell Start-ScheduledTask -TaskName 'cloudflared-token-renewal' Start-Sleep -Seconds 3 Get-Content C:\ProgramData\cloudflared-ssh-axe\renewal.log -Tail 5 ``` `token exp=... (NNNmin)` + `next run: ...` 두 줄 보이면 정상. `ERROR: Access is denied` 가 보이면 `-RunLevel Highest` 누락 (함정 #11). ### (선택) 갱신 시 사람 클릭 0회 기본 브라우저에서 `https://account.microsoft.com` 로그인 시 **"로그인 상태를 유지하시겠습니까?" → 예**. 그러면 renewal task 가 `cloudflared access login` 트리거해도 Microsoft 가 자동 redirect → 사람 손 안 가는 상태. 조직 Entra 정책이 sticky session 차단하면 갱신 시점에 한 번씩 클릭은 필요 (정책 문제, client 우회 불가). ## (부록) NSSM/Windows 서비스화 시도가 실패하는 이유 직관적으로 cloudflared 를 Windows 서비스화하고 싶을 수 있음 — PowerShell 창 의존성 제거 + 로그오프 후에도 살아있음. 그러나 두 시나리오 모두 실패: **A. SYSTEM 으로 실행 → 토큰 격리** `cloudflared access login` 으로 받은 JWT 는 `%USERPROFILE%\.cloudflared\-token.json` 에 저장. SYSTEM 서비스는 `C:\Windows\System32\config\systemprofile\.cloudflared\` 를 봄. 증상 (stderr.log): ``` failed to acquire app token lock: timed out waiting for lock file C:\Windows\System32\config\systemprofile\.cloudflared\...-token.lock ``` SYSTEM 서비스는 토큰 없으니 자체 login 시도 → SYSTEM 은 desktop session 없어서 브라우저 못 띄움 → 무한 "Waiting for login..." 루프. **B. 사용자 계정으로 실행 → 패스워드 저장 문제** `nssm set ObjectName .\ ` 또는 services.msc 의 Log On 탭에서 사용자 계정 + 패스워드 입력 필요. 평문 명령행 노출 + Microsoft 계정/Windows Hello 사용자는 패스워드 입력 자체 번거로움 + 계정 패스워드 변경 시 서비스 정지 (운영 부담). **결론** SSH 가 사람의 active session 에서만 의미 있음 (SSH 시도 = 본인 로그인 상태). 따라서 user-context Scheduled Task (AtLogOn + VBS wrapper) 가 NSSM 보다 깔끔: - 패스워드 저장 0 - 토큰 캐시 자동 공유 - 본인 계정 컨텍스트 일관 - 본인 끌 UI 0 (가시적 창 0) - VBS 무한 루프로 정상/비정상 종료 모두 자동 재기동 ## 함정 정리 | # | 증상 | 원인 | 우회 | |---|---|---|---| | 1 | `cloudflared not recognized` (Windows) | PATH 미반영 | 새 shell 창 | | 2 | `tls: handshake failure` (Windows: `SEC_E_ILLEGAL_MESSAGE`, macOS: `sslv3 alert handshake failure`) | hostname 2단 (e.g. `ssh.axe.axelabs.ai`) — Universal SSL 1-level | 평탄 hostname (`ssh-axe.axelabs.ai`), [D-ops-39](/ops/decisions) | | 3 | pubkey 통과 후 `Connection closed by UNKNOWN port` | SACL `com.apple.access_ssh` 미가입 → PAM `pam_sacl.so` account 거부 | Step 3 (운영자 ai 가 `dseditgroup` 추가) | | 4 | `Host key verification failed` (prompt 답 못 함) | non-interactive ssh | `-o StrictHostKeyChecking=accept-new` | | 5 | Claude Code SSH timeout / handshake fail (PowerShell 직접 ssh 는 통과) | Claude Code 의 SSH backend 가 `ProxyCommand` 미지원 | TCP forward (Step 7) | | 6 | `ssh: connect to host localhost port 2222: Connection refused` | cloudflared TCP forward background 안 띄움 | `Start-Process ... access tcp ...` (Step 7) | | 7 | Claude Code 폴더 선택 시 axelabs / frame / hive 안 보임 | SSH user = 본인 shortname (본인 home 만 보임). AXE platform codebase 는 `/Users/axe/` | SSH user = `axe` (Step 4 의 config), git author = 본인 email (audit) | | 8 | 24h 후 `cloudflared access ssh` 에러 / Microsoft 로그인 페이지 prompt | JWT 토큰 exp | `cloudflared access login https://ssh-axe.axelabs.ai` 재실행 (또는 § "Token 자동 갱신") | | 9 | cloudflared TCP forward 가 PowerShell 창 종료 시 같이 죽음 | `Start-Process` 자식이 부모 종료에 종속 | user-context Scheduled Task + VBS wrapper (§ TCP forward 자동화) | | 10 | NSSM 등으로 cloudflared 서비스화 후 stderr 에 `failed to acquire app token lock: ...\Windows\System32\config\systemprofile\.cloudflared\...` 무한 루프 | SYSTEM 프로필 토큰 캐시 비어있음. JWT 는 user-scoped 라 SYSTEM 컨텍스트와 격리 | 서비스화 금지. user-context Scheduled Task (§ TCP forward 자동화) | | 11 | Token renewal task 가 `ERROR: Access is denied` 로 자기 trigger 수정 실패 | `Set-ScheduledTask` 는 elevated 권한 필요 | Principal 에 `-RunLevel Highest` | | 12 | 24h 마다 SSO 재인증을 사람이 매일 수동 실행해야 함 | docs 기본 흐름이 수동 | Self-rescheduling renewal task (§ Token 자동 갱신) | | 13 | `powershell.exe -WindowStyle Hidden` 으로 task action 구성 시 매 fire 마다 콘솔 창 깜빡임 | Windows 가 hidden flag 적용하기 전 수십 ms 노출 | TCP forward 같은 daemon 류는 `wscript.exe` + VBS wrapper 사용 (§ TCP forward 자동화) | | 14 | Task `-RestartCount` 설정해도 cloudflared 정상 종료 (exit 0) 후 재시작 안 됨 → 며칠 후 silently SSH 끊김 | `-RestartCount` 는 FAILURE (non-zero exit) 에만 발동 | VBS wrapper 안에 무한 재시작 루프 (§ TCP forward 자동화) | ## Audit Trail (D-ops-29) OS user = `axe` 공통이지만 식별 3중: | Layer | 식별자 | |---|---| | Cloudflare Access SSO log | email (Cloudflare 대시보드 — Zero Trust → Logs → Access) | | sshd auth log | ed25519 fingerprint (`/var/log/system.log`) | | git commit author | `user.email` per session | 본인 session 의 git config 분리 (axe 의 global `~/.gitconfig` 가 ai@ 으로 되어있을 수 있음): ```bash # repo 별 cd /Users/axe/ git config user.email "@axellc.com" # 또는 본인 session 환경변수 export GIT_AUTHOR_EMAIL="@axellc.com" export GIT_COMMITTER_EMAIL="@axellc.com" ``` ## 매 작업 시 사전 작업 | 빈도 | 작업 | 자동화 | |---|---|---| | PC 로그온 시 | cloudflared TCP forward 시작 | ✅ Scheduled task `cloudflared-ssh-axe-tcp` (AtLogOn + VBS 무한 루프) | | Token 만료 ~5분 전 | `cloudflared access login` | ✅ Scheduled task `cloudflared-token-renewal` (self-rescheduling, RunLevel Highest) | | 매 SSH 시도 | (없음) | — | | (선택) Microsoft "로그인 상태 유지" | 갱신 시 사람 클릭 제거 | ⚠️ 본인 브라우저 설정 + Entra 정책에 의존 | ## 참조 - [D-ops-41](/ops/decisions) — SSH client-side automation (NSSM 폐기, user-context Scheduled Task + VBS wrapper, self-rescheduling renewal) - [D-ops-39](/ops/decisions) — Universal SSL 1-level + flat-hostname 컨벤션 - [D-ops-29](/ops/decisions) — Dual identity (ai@ automation vs soohun.kang human work) - [B-bp-mcp-teams-admin-consent](/ops/backlog) — Blueprint MCP Teams tools admin consent (미완) - [/ops/known-gaps](/ops/known-gaps) — Cloudflare Universal SSL 1-level 함정 + 14 trap 분석 _Last updated: 2026-05-27 (Soohun Kang — Windows TCP-forward 자동화 + 24h SSO 자동 갱신 셋업 검증 + 함정 #9~#14 추가)_ --- # 문제 해결 > Frame connector 설정 시 자주 발생하는 오류와 대응. URL: https://docs.axelabs.ai/onboard/troubleshooting # 문제 해결 ## "Couldn't reach the MCP server" claude.ai 가 frame 서버에 도달 못 함. 원인 후보: 1. **URL 오타** — `https://axe.axelabs.ai/frame/mcp` (마지막에 `/mcp` 포함) 2. **회사 customer 와 다른 URL** — `axe` 직원은 `axe.axelabs.ai`, `realchoice` 직원은 `realchoice.axelabs.ai` 3. **claude.ai 캐시** — connector 완전 삭제 후 시크릿 창에서 재등록 시도 curl 로 외부 reachability 확인: ```bash curl -sS https://axe.axelabs.ai/frame/health # → {"status":"ok","service":"frame-mcp"} ``` 200 OK 안 나오면 운영자에게 알리세요. ## "Authorization with the MCP server failed" Microsoft 인증 또는 그 다음 단계에서 실패. URL 바의 `error_code=` 와 `entra_aadsts_code=` 가 결정적 단서. ### Reconnect 후에도 401 — frame middleware 의 RFC 9728 metadata path **증상**: Connector 가 "Connection has expired. You can reconnect to re-authenticate" 표시 → Reconnect 클릭 → Microsoft 로그인 성공 (Entra Trace ID 있음, AADSTS error code 없음) → 다시 같은 메시지 반복. **원인**: frame middleware 의 `/frame/mcp/.well-known/oauth-protected-resource` (resource-level RFC 9728 metadata) path 가 unauthenticated allowlist 누락 시 401 반환. claude.ai Connector 가 OAuth challenge metadata 못 받아 reconnect flow 진행 불가. **해소**: **D-ops-23 (2026-05-22)** — `src/frame/mcp/http_server.py` 의 `_PUBLIC_PATHS` + `inner.router` 양쪽에 resource-level path 추가. 본 시점 이후 reconnect 정상. **검증 명령**: ```bash curl -sS -o /dev/null -w "%{http_code}\n" \ https://.axelabs.ai/frame/mcp/.well-known/oauth-protected-resource # 200 expected (D-ops-23 이전엔 401) ``` ### `/frame/schemas` 또는 `/hive/schemas` 401 — auth-required (정상) **증상**: `curl https://axe.axelabs.ai/frame/schemas` 가 401 반환. WWW-Authenticate: Bearer header 동반. **원인**: 의도된 동작. `/schemas` 는 RFC 9728 metadata 와 달리 **`_PUBLIC_PATHS` 미포함** — Blueprint artifact 지식 레이어 ([D-bp-artifact-1](/ops/decisions)) 의 schema discovery 가 MCP 토큰 인증 필요. 토큰 없이 호출 시 일반 MCP 호출과 동일 401. **해소**: MCP 토큰 (`FRAME_MCP_TOKEN` 또는 OAuth bearer) 부여 후 재시도. Blueprint 가 자동 호출 시는 [`B-bp-artifact-schema-discovery`](/ops/backlog) 가 frame token 재사용. ```bash curl -H "Authorization: Bearer $FRAME_MCP_TOKEN" \ https://axe.axelabs.ai/frame/schemas # 200 + { version, service, schemas } envelope expected ``` ### AADSTS700016 — Application not found ``` Application with identifier '<...>' was not found in the directory '...' ``` - Client ID 입력 오타 (vaultwarden 의 GUID 와 Frame MCP 의 GUID 혼동 등) - 운영자가 새 customer 의 Azure 등록을 아직 안 한 상태 → 운영자에게 정확한 Client ID 확인 요청. ### AADSTS9010010 — resource/scope mismatch ``` The resource parameter provided in the request doesn't match with the requested scopes ``` 운영자 측 Azure 설정 문제. Application ID URI 와 scope URI prefix 가 다를 때 발생. → 운영자 측에서 처리, 직원 작업 X. ### AADSTS7000218 — missing client_secret ``` The request body must contain the following parameter: 'client_assertion' or 'client_secret' ``` - claude.ai 의 Advanced 에 **OAuth Client Secret 미입력** - 또는 Azure 측 "Allow public client flows" 가 No 상태인데 secret 없음 → Advanced 칸에 운영자가 준 secret 입력. secret 분실했으면 운영자에게 재요청. ### Bitwarden Send URL — "This Send has been deleted" / "Send not found" 운영자가 Teams DM 으로 보낸 1회용 URL 을 클릭했는데 위 메시지 나옴. - **1회 클릭 이미 소진** — `-a 1` 설정이라 누군가 (운영자 본인 검증 포함) 이미 1회 열었으면 즉시 무효 - **1일 만료 (`-d 1`)** 경과 — 운영자가 발사한 시각 + 24h 지남 - **운영자가 수동 삭제** (`bw send delete`) → 운영자에게 재발급 요청: 본인 이메일 + 어느 서비스 (Blueprint / Hive / Frame) 인지 명시. 운영자 1 명령 (`axe secret send --service --to `) + broadcast-dm 으로 새 URL 즉시 송부. 절차: [/architecture/secrets § 사람에게 전달](/architecture/secrets#사람에게-전달--bitwarden-send). ### AADSTS65001 — user/admin consent 없음 ``` The user or administrator has not consented to use the application ``` - 첫 로그인 시 동의 화면에서 **Accept** 안 누름 - 또는 admin consent 가 필요한 scope 이지만 미부여 → 다시 connect 시도, 동의 화면 나오면 Accept 클릭. 안 나오면 운영자 호출. ### AADSTS50011 — redirect URI mismatch ``` The reply URL specified in the request does not match the reply URLs configured ``` 운영자 측 Azure 설정 문제 (redirect_uri 누락). → 운영자 작업. ## "mcp_client_invalid" Anthropic 이 Microsoft 에서 받은 토큰을 frame 에 못 보냄. URL 바의 `entra_aadsts_code=` 확인. URL 형식: `claude.ai/customize/connectors?...&error_code=mcp_client_invalid&entra_aadsts_code=<코드>&entra_trace_id=<id>` `entra_aadsts_code` 가 있으면 위 AADSTS 섹션 참조. 없으면 운영자에게 전체 URL 전달. ## "SSO 화면 자체가 안 뜸" - claude.ai connector 가 stale state — 완전 삭제 후 **브라우저 hard refresh (Cmd+Shift+R)** → 재등록 - 또는 시크릿/incognito 창에서 재시도 ## Vault (axe.axelabs.ai/vault) — "Log in with SSO" / Identifier 입력 안 보임 Vaultwarden 기본 로그인 페이지는 **이메일 입력이 먼저** 노출됩니다. SSO Identifier 화면으로 바로 가려면: - 직접 URL: `https://axe.axelabs.ai/vault/#/sso` → "Organization SSO Identifier" 입력 칸 노출 → `AXE` 입력 → "Log In" - 또는 이메일 입력 화면 하단의 **"Enterprise single sign-on"** 링크 클릭 ## Vault — "Your provider does not send email verification status" 전체 오류: ``` Your provider does not send email verification status. You will need to change the server configuration (check `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`) to log in. ``` **원인**: Microsoft Entra ID 의 OIDC id_token 에는 `email_verified` claim 이 포함되지 않음. Vaultwarden Timshel fork 의 기본 동작은 검증 실패 → 로그인 거부. **해소**: **D-ops-24 (2026-05-22)** — `/Users/axe/.axe/vault/docker-compose.yml` 의 `axe-vaultwarden.environment` 에 `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"` 추가 후 `docker compose up -d --force-recreate axe-vaultwarden`. tenant 내부 사용자만 SSO 흐름에 진입하므로 추가 검증 면제 안전. 직원은 본 해소 이후 위 "Identifier 입력 안 보임" 안내대로 재시도. ## "Connected 떴는데 도구 호출이 실패함" 채팅에서 `Frame:list_open_items` 호출 시 "권한 없음" 또는 "Entity not authorized": - 본인 이메일이 `customers.yaml` 의 `user_entity_map` 에 매핑 안 됨 - 또는 잘못된 entity 명 사용 (`axec` vs `axes`) → 운영자에게 entity 매핑 확인 요청. ## "권한 부족" — 분개 등록 실패 ``` 이 작업은 write 권한이 필요하나 현재 read 만 부여되어 있습니다 ``` 본인의 role 이 `read` 만 부여된 상태. 운영자에게 권한 상승 요청. ## "토큰 만료" — 며칠 후 갑자기 안 됨 Microsoft access_token 은 기본 60-90분, refresh_token 은 90일. claude.ai 가 자동 갱신하지만, 90일 무사용 시 만료. → claude.ai 의 connector 페이지에서 **Re-authorize** 클릭. Microsoft 로그인 1회 → 재활성. ## `axe login` 은 됐는데 `axe …` 가 401 — Blueprint 플랫폼 토큰 (D-axe-idp-1) [AXE CLI](/services) 의 `axe login`(브라우저 Entra SSO)은 성공해 토큰을 받았는데 `axe frame tools` 등이 401 이면: - **서비스가 Blueprint 를 아직 신뢰 안 함** — 해당 서비스에 `BLUEPRINT_ISSUER=https://blueprint.axellc.com` 가 설정돼야 `iss=blueprint` 토큰을 수용. unset 이면 그 토큰은 HS256 경로로 빠져 거부됨(의도된 비파괴 기본값). 운영자: 서비스 env(frame 은 `.env.local`)에 추가 후 `docker compose up -d --force-recreate`. - **`aud` 불일치** — 플랫폼 토큰 `aud` = `https://axe.axelabs.ai`(`BLUEPRINT_AUDIENCE` 기본). 서비스의 `BLUEPRINT_AUDIENCE` 와 같아야 함. - **email 미매핑** — 토큰은 유효하나 `customers.yaml` `user_entity_map`/`default_entities_by_domain` 에 해당 email 없음 → `USER_NOT_MAPPED`. customers.yaml 에 추가. - **Blueprint JWKS 도달 불가** — 서비스가 `https://blueprint.axellc.com/.well-known/jwks.json` 를 못 받으면 `OIDC_UNAVAILABLE`(503). 네트워크/터널 확인. 운영자 배포 함정: blueprint DB 는 `prisma migrate deploy` 가 안 먹음(`_prisma_migrations` 부재) → 새 모델은 `prisma db push`(또는 raw SQL). `BLUEPRINT_OIDC_PRIVATE_KEY` 는 env_file 따옴표/개행 문제 회피 위해 **base64 단일라인**. ## 그 외 — 운영자에게 전달할 정보 문제 해결 안 될 때 운영자 (`ai@axellc.com`) 에게 다음 전부 전달: 1. **어떤 작업 도중** 문제 발생했는지 (connector 추가 / tool 호출 / 다른 것) 2. **정확한 오류 메시지** (스크린샷) 3. **URL 바 전체** (특히 `error_code=`, `entra_aadsts_code=`, `entra_trace_id=` 부분) 4. **claude.ai connector 페이지 상태** (Connected / Disconnected / Authorization failed) 5. **(가능하면) Browser DevTools Network 탭** 에서 `login.microsoftonline.com` 으로 가는 요청의 Request URL --- # Vault setup — KDF rotation + 4 client + Bitwarden Authenticator > AXE Vaultwarden 의 SSO→MP unlock 정상화 (KDF rotation, 옛 user 만) + 데스크톱/Chrome/Mobile 3 client + Bitwarden Authenticator 표준 setup. 한 번 (~30분) 후 일상 = Touch ID / PIN. 새 직원 onboarding 의무 step. URL: https://docs.axelabs.ai/onboard/vault-setup # Vault setup ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/onboard/vault-setup 따라 내 vault 본인 setup 진행해줘. 진행: 1. 내 머신 OS + 기존 환경 진단 (macOS Touch ID 유무, Chrome 설치, mobile 종류) 2. 페이지의 Prereq 확인 — 내가 KDF rotation 대상인지 (axe.2 적용 전 가입 user 만), 아니면 Phase 1 skip 3. 페이지의 각 Phase 명령 + 검증 매 step 결과 받고 다음 4. 함정 발생 시 페이지 "함정 정리" 표 따라 우회 5. 모든 Phase 완료 후 운영자 ai 에게 Teams 회신 "vault setup 완료" ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## Prereq - AXE 임직원 (`@axellc.com`) — Entra ID SSO 등록 완료 - 본인 macmini / 노트북 (macOS Touch ID 또는 Windows Hello 권장) + iPhone 또는 Android - 본인 vault master password 기억 (변경 시 current MP 확인 + 새 MP 입력 필수) - Microsoft Authenticator 가 phone 에 이미 설치되어 Entra MFA 등록 완료 상태 **Phase 별 강제도 (skip 가능성 판단)**: | Phase | 강제도 | skip 시 영향 | |---|---|---| | 1. KDF rotation | 옛 user 만 필수 | 옛 user 가 skip 하면 SSO→MP unlock 깨짐 (Phase 1 본문 참조) | | 2. Desktop 앱 | 필수 | 데스크톱 일상 unlock 불가 | | 3. Chrome extension | 필수 | 웹 자동 채우기 + 검색 + 단축키 입력 불가 | | 4. Mobile | **권장** | mobile 자동 채우기 / 외출 시 vault 조회 불가. 데스크톱-only 워크플로면 skip 가능 | | 5. Bitwarden Authenticator | **선택** | Google Authenticator 등 기존 TOTP 앱 계속 사용 OK (Phase 5 본문 참조). 새 2FA 등록만 vault TOTP 필드 권장 | **Phase 1 (KDF rotation) 대상 = "옛" user**: - ✅ 본인이 **axe.2 release (2026-05-26) 이전 가입자** 이면 KDF rotation 필요 (`client_kdf_type=0`, PBKDF2) - ❌ axe.2 release 이후 신규 가입자 (realchoice 측 등) 는 새 schema 로 가입돼 KDF rotation 불필요 → **Phase 1 skip** - 헷갈리면 일단 Phase 1 시도 — 옛 schema 면 효과, 새 schema 면 no-op (안전) ## Phase 1: KDF rotation (옛 user 만, ~5분) vault 의 SSO→MP unlock 정상화 ([D-ops-40](/ops/decisions)). 옛 KDF (PBKDF2) 로 wrap 된 user.akey 를 새 schema 로 재wrap. 1. https://axe.axelabs.ai/vault — **새 incognito (Chrome 시크릿) 창** 에서 접속 2. 본인 email (`firstname.lastname@axellc.com`) → Continue 3. ★ **SSO 누르지 말고** Master password 단독 로그인 (본인 현재 MP) 4. 우상단 본인 이름 → Account settings → 좌측 사이드바 Security → 상단 **Master password** 탭 5. 입력: - **Current master password**: 본인 현재 MP - **New master password**: 같은 값으로 OK (또는 더 강한 12자 이상 새 값) - **Confirm new master password**: new 와 동일 - ☑ **Also rotate my account's encryption key** — **반드시 체크** 6. **Change master password** 클릭 → 수십 초 대기 (client-side 가 모든 vault item 재암호) 7. 자동 logout **검증** (같은 incognito 창에서): 8. https://axe.axelabs.ai/vault → 본인 email → Continue 9. 이번엔 **Enterprise single sign-on** 클릭 10. Microsoft 로그인 + MFA → vault redirect → **MP 입력** → unlock 정상이면 ✅ axe.2 까지는 10번 단계에서 "Cannot read properties of null (reading toWrappedAccountCryptographicState)" 에러로 막혔음. KDF rotation 후 정상. ## Phase 2: Bitwarden 데스크톱 앱 (~5분) 본인 OS 에 맞는 sub-section 따라가기. ### Phase 2a — macOS 1. https://bitwarden.com/download/ → **macOS Desktop (Universal)** 다운로드 → `/Applications` 설치 2. 첫 실행 → 좌측 아래 **gear (Settings)** → **Self-hosted environment** 3. **Server URL**: `https://axe.axelabs.ai/vault` → **Save** 4. **Log in with SSO** (Enterprise single sign-on) → Entra ID + MFA → MP 입력 → Unlock 5. 메뉴 → **Settings** (⌘+,) → **Preferences**: - **Vault timeout**: **Never** - **Vault timeout action**: **Lock** - **Unlock with Touch ID**: ☑ (Touch Bar 또는 Magic Keyboard Touch ID) - **Lock with master password on browser restart**: ☑ 6. **Save** → 이후 일상 unlock = Touch ID (1-2 초). MP 입력 = 앱 완전 재시작 + paranoia 옵션 시만. ### Phase 2b — Windows 설치 경로 2 가지 — **Microsoft Store** vs **`.exe` installer**. 결과물 + 동작 같음, 자동 업데이트 채널만 다름. | 설치 방법 | 다운로드 | 자동 업데이트 | 추천 | |---|---|---|---| | Microsoft Store | Store 검색 "Bitwarden" | Store 가 자동 (Windows Update 함께) | 일반 사용자 | | `.exe` installer | https://bitwarden.com/download/ → **Windows Installer (64-bit)** | 앱 내장 updater (수동 확인 OK) | 회사 정책상 Store 제한 PC | 설치 후: 1. 첫 실행 → 좌측 아래 **gear (Settings)** → **Self-hosted environment** 2. **Server URL**: `https://axe.axelabs.ai/vault` → **Save** 3. **Log in with SSO** → Entra ID + MFA → MP 입력 → Unlock 4. 메뉴 → **Settings** → **Preferences**: - **Vault timeout**: **Never** - **Vault timeout action**: **Lock** - **Unlock with Windows Hello**: ☑ (Windows Hello 사전 설정 PC 만 — 아래 분기 참조) - **Lock with master password on browser restart**: ☑ 5. **Save** **Windows Hello 미설정 PC 의 fallback** = PIN unlock: - 설정에서 **Unlock with Windows Hello** 옵션이 회색 비활성 = OS 단에서 Hello 미등록 (PIN / 얼굴 / 지문 어느 것도 없음) - 두 선택지: - (a) Windows Settings → Accounts → Sign-in options → **Windows Hello PIN** 등록 → Bitwarden 재시작 → Hello unlock 활성화 - (b) Hello 등록 거부 시 → **Unlock with PIN code** ☑ + PIN 6자리 이상 (Chrome ext 와 동일 방식, Phase 3 참조) - 권장 = (a). Hello = OS biometric / PIN 통합 채널이라 Bitwarden + 회사 다른 앱 동일 채널 사용 가능 → 이후 일상 unlock = Windows Hello (또는 PIN). MP 입력 = 앱 완전 재시작 + paranoia 옵션 시만. ## Phase 3: Chrome extension (~3분) 1. Chrome 에서 다운로드: ``` https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb ``` 2. extension 아이콘 클릭 → **⚙ Settings** → **Self-hosted environment** 3. **Server URL**: `https://axe.axelabs.ai/vault` → **Save** 4. **Log in with SSO** → 첫 MP unlock 5. **⚙ Settings** → **Account security**: - **Vault timeout**: **Never** - **Vault timeout action**: **Lock** - **Unlock with PIN code**: ☑ + PIN 6자리 이상 (숫자보다 word 권장, 예: `axe-2026`) - **Lock with master password on browser restart**: ☑ → 이후 unlock = PIN. Chrome 완전 재시작 시 MP 1회 (paranoia 옵션이면). ## Phase 4: Mobile (iOS / Android, ~5분, 권장) > **권장 (필수 아님)**. 데스크톱-only 워크플로 (외근 / 외출 중 vault 조회 불필요) 사용자는 skip 가능. skip 시 영향 = mobile 자동 채우기 / 외출 vault 조회 불가. 대부분 직원은 SMS / KB / 홈택스 등 mobile 2FA 도착 시 vault TOTP 조회가 필요해 설치 권장. 1. App Store / Play Store → **"Bitwarden"** 검색 → **노란 방패 아이콘** (Password Manager — Authenticator 와 다른 앱) 2. 설치 → 첫 실행 → **Settings** → **Self-hosted environment**: - **Server URL**: `https://axe.axelabs.ai/vault` → **Save** 3. **Log in with SSO** → Entra MFA → MP 입력 → Unlock 4. **Settings** → **Account security**: - **Unlock with Face ID** / **Touch ID**: ☑ - **Vault timeout**: **1 month** - **Vault timeout action**: **Lock** → Safari/Chrome 모바일 자동 채우기, 앱 안 자동 채우기 다 됨. ## Phase 5: Bitwarden Authenticator (TOTP, ~3분, 선택) 외부 service (KB / 홈택스 / GitHub / 등) 의 2FA 코드를 vault 와 분리 저장 ([D-ops-38](/ops/decisions) 보조). > **선택 (필수 아님)**. 기존 Google Authenticator / Microsoft Authenticator 앱에 등록된 TOTP 는 **그대로 유지 OK** — 새 2FA 등록 시점부터 본 표준 (vault item TOTP 필드) 적용을 권장하는 것. **기존 item 일괄 수정 SOP 아님**. ### "vault item TOTP 필드 저장" 의 의미 | 시나리오 | 행동 | |---|---| | 이미 Google/MS Authenticator 에 있는 기존 TOTP | **건드릴 필요 없음**. phone authenticator 그대로 사용 | | 새 서비스 2FA 등록 (오늘 이후) | 새 표준 = vault item 의 Authenticator key (TOTP) 필드에 secret 저장 | | 기존 TOTP 를 vault 로 이전 (선택) | 본인 시간 날 때만. phone authenticator 의 backup code 또는 service 의 "2FA reset" 흐름 거쳐 재발급 → vault 저장 | ### vault item TOTP 필드 UI 동작 (Bitwarden 공통) 데스크톱 / Chrome ext / mobile 어디서나 동일: | 상태 | 동작 | |---|---| | **View 모드** (item 클릭만) | TOTP secret 있는 item 만 "Verification code (TOTP)" / "인증 코드 (TOTP)" 섹션 노출. 없으면 섹션 자체 안 보임 | | **편집 모드** (item Edit) | Authenticator key 입력 필드 노출 (View 모드엔 안 보임) — secret 추가 / 변경 / 삭제 | | 상단 **"인증 코드 복사"** 버튼 | 해당 item 에 TOTP secret **있을 때만 동작**. 빈 item 에서 누르면 무반응 (오류 메시지 없음) — 정상 | → "복사 버튼 무반응" 이면 = 본인 item 에 secret 없음 = 편집 모드 진입 후 secret 입력 (또는 phone authenticator 에 그대로 두기). ### Mobile (권장) 1. App Store / Play Store → **"Bitwarden Authenticator"** 검색 - ★ Phase 4 의 Password Manager 와 **별도 앱** — 회색 방패 + 시계 아이콘 2. 설치 → 첫 실행 → **Bitwarden 계정** (axe.axelabs.ai/vault SSO 동일) 로그인 → **sync 활성화** 3. 끝 — Password Manager 의 TOTP 필드와 양방향 동기화 > 기존 Google / MS Authenticator 와 **병행** OK. 신규 TOTP 만 Bitwarden 쪽에 등록해도 됨. ### Desktop Bitwarden Authenticator 는 mobile-only (2026-05 기준 macOS / Windows 공식 앱 없음). **대안 = Phase 2 의 Bitwarden Password Manager 데스크톱 앱이 TOTP 내장**: - vault item 의 **Authenticator key (TOTP)** 필드에 secret 저장 → 데스크톱에서 6-digit 자동 회전 (30초) - D-ops-38 표준: **새** 외부 service 2FA 등록 시 phone Authenticator 앱 대신 vault item TOTP 필드 사용 → mobile + desktop 양쪽 동일 코드 ## 검증 + 완료 신호 | # | 검증 항목 | 통과 기준 | |---|---|---| | 1 | (Phase 1 대상자) SSO → MP unlock | https://axe.axelabs.ai/vault SSO 흐름 → MP 입력 → vault 열림 | | 2 | Phase 2 데스크톱 unlock | Touch ID 1회 → vault 열림 | | 3 | Phase 3 Chrome ext unlock | PIN 입력 → vault items 검색 가능 | | 4 | Phase 4 Mobile unlock | Face ID / Touch ID → vault 열림 | | 5 | Phase 5 Mobile Bitwarden Authenticator | Bitwarden 계정 sync 성공 (vault item TOTP 필드와 같은 코드 회전) | | 6 | vault item 접근 (워크플로 분기 아래 참조) | 본인 워크플로 기준 항목 통과 | ### 자동 채우기 검증 — 워크플로 분기 vault item 접근 방식은 사용자별로 다름. 본인 워크플로에 맞는 검증만 통과하면 OK. | 워크플로 | 검증 방법 | 통과 기준 | |---|---|---| | **웹 폼 위주** (KB / 홈택스 / SaaS 로그인 페이지 잦음) | Safari/Chrome 의 login form 에서 Bitwarden suggestion 자동 노출 | suggestion drop-down 클릭 → ID/PW 자동 입력 | | **SSO 위주** (대부분 회사 서비스 = Entra SSO 한 번 + remote-host credential 위주) | 데스크톱 앱에서 vault 검색 → 필드 우클릭 / 단축키 복사 | 검색 → item 클릭 → 필드 단축키 (⌘C / Ctrl+C) 또는 컨텍스트 메뉴 복사 | | **터미널 / SSH 위주** | Chrome ext 단축키 자동 입력 (Ctrl+Shift+L on focused input) 또는 SSH agent 연동 | focused field 에 자동 입력 ID/PW 전송 | > SSO 위주 사용자 (대다수 AXE 개발자 / taehun 같은 패턴) 는 웹 자동 채우기 거의 안 씀 = 자동 채우기 검증 skip OK. 검색 + 필드 복사가 정상 작동하면 통과. 전부 통과하면 운영자 ai (`ai@axellc.com`) 에게 Teams 1:1 채팅으로 "vault setup 완료" 회신. ## 함정 정리 | # | 증상 | 원인 | 우회 | |---|---|---|---| | 1 | Phase 1 step 5 의 "Change master password" 후 vault 깨짐 | Step 6 의 client-side 재암호 wait 중 새로고침 / 창 닫기 | 수십 초 절대 건드리지 X. 끊겼으면 운영자 ai 에게 즉시 신고 (백업 복원 가능, [/ops/runbook/vault-recovery](/ops/runbook/vault-recovery)) | | 2 | Phase 1 KDF rotation 후에도 SSO→MP 깨짐 | (a) 옛 schema 가 아니라 다른 원인, 또는 (b) Phase 1 의 "rotate encryption key" 체크 빠짐 | 운영자 ai 에게 docker logs capture 시도 신고 — 옛 schema 가설 외 후속 진단 | | 3 | Phase 2-4 의 Web vault timeout 에 `Never` 옵션 없음 | 보안 정책상 web vault 가 `Never` 미노출 (Bitwarden 의도) | Web vault 는 `On browser refresh` 가 최장. Native + Extension 은 `Never` OK | | 4 | Phase 3 Chrome ext 가 SSO 로그인 안 됨 | Self-hosted environment 의 Server URL 미설정 / 오타 | `https://axe.axelabs.ai/vault` (https + /vault 까지) 정확 입력 | | 5 | Phase 4 mobile 에서 vault 로그인 매번 MP 요구 | Vault timeout = `Immediately` 또는 짧음 + biometric unlock 미활성 | Settings → Account security → Vault timeout `1 month` + Face/Touch ID ☑ | | 6 | Phase 5 Bitwarden Authenticator 가 데스크톱 안 보임 | Bitwarden Authenticator = mobile-only (2026-05 기준) | 데스크톱 = Phase 2 의 Password Manager 데스크톱 앱 TOTP 기능 사용 | | 7 | Phase 5 모바일 Authenticator sync 안 됨 | Bitwarden 계정 로그인 빠짐 (Authenticator 첫 실행 시 "Use without account" 선택했음) | 앱 Settings → Account → Log in → axe.axelabs.ai/vault SSO 계정 | | 8 | Phase 5 Authenticator app 에서 vault item TOTP 안 보임 | sync 활성화는 됐지만 Password Manager 측 item 에 TOTP 필드 미설정 | Password Manager 에서 해당 item 편집 → Authenticator key 필드에 외부 service 의 TOTP secret 입력 → 양쪽 동기화 | | 9 | Phase 1 의 SSO 단계에서 Entra ID 가 MFA 요구하는데 Microsoft Authenticator 미설치 | MFA enrollment 미완 | https://mysignins.microsoft.com/security-info → Add sign-in method → Microsoft Authenticator → 모바일 설치 + 등록 | | 10 | "Phase 1 대상자인지 모르겠음" | 본인이 옛 user 인지 신규 user 인지 불확실 | 일단 Phase 1 시도 — 옛 schema 면 효과, 새 schema 면 no-op (Master password change 가 그냥 동일 값으로 갱신, 안전). 시간 = 5분 | | 11 | Phase 2b Windows 의 "Unlock with Windows Hello" 옵션이 회색 비활성 | OS 단에서 Hello 미등록 (PIN / 얼굴 / 지문 어느 것도 없음) | Windows Settings → Accounts → Sign-in options 에서 Hello PIN 등록 → Bitwarden 재시작. 또는 PIN unlock fallback (Phase 2b 본문) | | 12 | Phase 5 의 "vault item 의 인증 코드 복사" 버튼 눌러도 무반응 | 해당 item 의 Authenticator key (TOTP) 필드가 비어 있음 (정상 동작 — 오류 메시지 없음) | item 편집 모드 진입 → Authenticator key 필드에 secret 입력 (또는 phone authenticator 그대로 유지) | | 13 | Phase 5 "기존 Google Authenticator TOTP 를 vault 로 옮겨야 하나" 헷갈림 | "vault item TOTP 필드 저장" 의미 = 신규 SOP, 기존 일괄 마이그레이션 X | 기존 TOTP 는 그대로 유지 OK. 새 2FA 등록 시점부터 vault TOTP 필드 사용 | | 14 | Phase 검증 6번 (자동 채우기) 안 됨 | SSO 위주 사용자라 웹 폼 자동 채우기 거의 안 씀 | 본인 워크플로 분기 (검증 섹션 표) 따라 SSO 위주 / 터미널 위주 검증으로 대체. 자동 채우기 검증 skip OK | ## Phase 별 시간 | Phase | 시간 | 비고 | |---|---|---| | 1. KDF rotation | 5분 | 옛 user 만 | | 2. Desktop (macOS / Windows) | 5분 | 모두 (필수) | | 3. Chrome extension | 3분 | 모두 (필수) | | 4. Mobile | 5분 | 권장 (skip 가능) | | 5. Bitwarden Authenticator | 3분 | 선택 (Google/MS Authenticator 병행 OK) | | 검증 + 회신 | 5분 | 모두 | | **합계** | **~25-30분** | 옛 user / 5 phase 다 | ## 표준 setup vs valid 변형 본 페이지의 **표준 setup** 외 valid 변형도 인정. 본인 워크플로에 맞으면 변형 사용 OK — 운영자 ai 에게 "valid 변형 X 사용 중" 명시만. | 구성 | 표준 (권장) | valid 변형 | |---|---|---| | Stack | 3-stack (Desktop + Chrome ext + Mobile) + Bitwarden Authenticator | 2-stack (Desktop + Chrome ext) — Mobile / Bitwarden Authenticator 생략 | | TOTP 앱 | Bitwarden Authenticator (mobile) + vault item TOTP 필드 (desktop) | Google Authenticator / Microsoft Authenticator 계속 사용 + 새 2FA 만 vault 등록 | | 자동 채우기 | 웹 폼 + Chrome ext suggestion 적극 활용 | SSO + remote-host credential 위주 = 검색 + 필드 복사 + 단축키 입력 | | 적합 사용자 | 직접 SaaS 로그인 잦은 운영 / 기획 / 영업 등 | 개발자 (SSO + ssh / remote-host 위주) — taehun 같은 패턴 | > 둘 다 D-ops-37 + D-ops-38 + D-ops-40 정합 — secret 이 vault SoT 에 있고 user master password 가 단일 unlock factor 면 valid. ## 일상 운영 (setup 완료 후) - **daily unlock** = Touch ID (macOS) / Windows Hello (Windows) / Face ID (mobile) / PIN (Chrome ext) — 1-2 초 - **MP 입력 빈도** = 데스크톱 / mobile 완전 재시작 시 + paranoia 옵션 켰을 때만 (월 1-2회) - **vault item 추가** = 데스크톱 앱이 가장 편함 (form 더 큼 + TOTP 필드 명시) - **service 2FA 코드 조회** = Bitwarden Authenticator (모바일) 또는 Password Manager 의 TOTP 필드 (데스크톱). Google/MS Authenticator 병행 사용자는 기존 앱 그대로 - **자동 채우기 안 뜨면** = Chrome ext 의 Self-hosted environment 확인 + extension 권한 (해당 사이트에서 contents 접근 허용). SSO 위주 사용자는 자동 채우기 미사용이 정상 — 검색 + 단축키 복사로 대체 ## 참조 - [D-ops-40](/ops/decisions) — vault axe.3 release plan + Progress (본 setup 의 배경) - [/architecture/vault-policies](/architecture/vault-policies) — 3 layer 정책 모델 (server env + Org Policies + per-client preferences). Phase 2-5 의 "Vault timeout / Unlock with X" 권장값의 근거 - [D-ops-38](/ops/decisions) — 외부 service 2FA = vault TOTP 필드 (Phase 5 본질) - [D-ops-37](/ops/decisions) — axelabs-ai/vault Timshel fork build - [D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions) — SSO_ONLY=false + MP fallback 보존 - [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) — KDF rotation 진행 status (4명 완료 후 ✅) - [/ops/runbook/vault-recovery](/ops/runbook/vault-recovery) — vault 복구 (Phase 1 step 6 중단 등 비상 시) - [/ops/runbook/operator-broadcast](/ops/runbook/operator-broadcast) — 본 setup 안내 broadcast 채널 (운영자가 신규 직원에게 본 페이지 URL 발송) --- # 운영자 대시보드 > 운영자 (`ai@axellc.com`) 전용 — 일상·주간·월간 점검 항목. URL: https://docs.axelabs.ai/ops # 운영자 대시보드 > 본 섹션은 **운영자 (`ai@axellc.com`)** 전용. Microsoft Entra ID 로그인 게이트 (예정). 정보가 customer 별 운영 디테일 포함. ## 매일 (자동, 점검만) | 항목 | 자동화 | 점검 방법 | |---|---|---| | 03:00 KST local backup | `com.axe.backup.local` | `restic snapshots` 직접 | | 03:30 KST ring push (axe → realchoice) | `com.axe.ring.push` | `axe ring status` | | 매시 frame integrity check | `com.frame.integrity-check` | `ssh axe-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec"` | | 매시 cron jobs (stream/magnet) | supercronic | 각 service 로그 | 매일 운영자가 받는 Slack 보고 (또는 메일): - 어제 backup 성공/실패 - frame integrity 결과 (모든 entity 정상이면 1줄 OK) - stream/magnet alert 합계 ## 매주 - 직원 활동 audit (frame `audit_log` × 모든 entity) - secret 만료 임박 항목 (30일 이내) - Cloudflare tunnel uptime - macmini 디스크 사용량 (모든 customer) ## 매월 - 직원 권한 review (`customers.yaml > user_entity_map`) - backup 크기 변화 - Vaultwarden organization 멤버 변동 - 회계 사무소 export 송부 ## 분기 - Restore drill (`com.axe.restore-drill`, Jan/Apr/Jul/Oct 15) - DECISIONS 회고 (변경 필요?) - 비용 review (Cloudflare, Microsoft, Anthropic) - Cold SSD rotation ## 주요 도구 | CLI | 용도 | |---|---| | `axe customers list/add/show` | customer 목록 | | `axe health <target>` | 인프라 health probe (positional, `target` 기본값 `all`) | | `axe deploy <service> <customer> --apply` | blue/green deploy | | `axe ring push/sync/status/init` | ring backup (Phase 5) | | `axe cold init/sync/status` | cold SSD backup | | `axe console rebuild/data/restart/sso/token` | admin.axelabs.ai 콘솔 갱신 | | `axe secret get/list/status` | Vault 직접 호출 (단건) | | `axe secret check/pull/push/rotate <svc>` | 매니페스트 기반 일괄 ([architecture/secrets](/architecture/secrets), D-ops-17) | | `axe phase` | 현재 rollout phase | | `axe onboard <customer> --apply` | 신규 customer bootstrap (18-step) | | `axe restore --customer X --from Y --as-of Z` | restic 복원 — **Phase 5 stub**, 현재 restic 직접 호출 사용 ([known-gaps](/ops/known-gaps)) | | `axe docs-check [--since <ref>]` | 코드 ↔ axelabs-docs drift 감지 ([release flow](/ops/runbook/release-flow)) | | `axe ship [<service>]` | release-gate: docs-check + push + deploy. **`git push origin main` 대신 사용** ([release flow](/ops/runbook/release-flow)) | 자동 일일 backup = `com.axe.backup.local` launchd (매일 03:00 KST). 수동 시도는 `restic backup` 직접. ## 외부 콘솔 | 콘솔 | 용도 | |---|---| | `https://admin.axelabs.ai` | 운영자 web 콘솔 (Microsoft SSO) | | `https://axe.axelabs.ai/vault` | Vaultwarden (master password) | | Azure portal | Entra ID app 관리 | | Cloudflare dashboard | DNS, tunnel, Universal SSL | ## 핵심 파일 | 파일 | 역할 | |---|---| | `/Users/axe/.axe/customers.yaml` | SSOT — customer registry | | `/Users/axe/.axe/bin/axe` | CLI | | `/Users/axe/CLAUDE.md` | 포트 할당, launchd 서비스 목록 | | `/Users/axe/multi-tenant-platform-plan.md` | 마스터 플랜 + D 결정 | | `/Users/axe/frame/DECISIONS.md` | D-ops-1~15 | | `/Users/axe/.axe/state.yaml` | 현재 rollout phase, 다음 세션 컨텍스트 | | `/Users/axe/.axe/tunnels/axelabs/config.yml` | cloudflared ingress | | `/Users/axe/.axe/vault/docker-compose.yml` | Vaultwarden | ## 다음 세션 시작 시 1. `cat /Users/axe/.axe/state.yaml` — 현재 phase 확인 2. `cat /Users/axe/.axe/NEXT_SESSION.md` — 마지막 세션의 다음 작업 3. 운영자 콘솔 ( `https://admin.axelabs.ai` ) 의 dashboard 확인 4. 미해결 알람 (어제 backup 실패 등) 처리 ## 비상 | 사건 | 즉시 대응 | |---|---| | frame DB 손상 | [Runbook · DB 복구](/ops/runbook/db-recovery) | | cloudflared 죽음 | `docker start axelabs-tunnel` | | Vaultwarden 죽음 | [Runbook · Vaultwarden recovery](/ops/runbook/vault-recovery) | | customer macmini 도난/화재 | [Runbook · macmini 손실](/ops/runbook/macmini-loss) | | secret 노출 의심 | [Runbook · secret rotation](/ops/runbook/secret-rotation) | ## 다음 - [Runbooks](/ops/runbook) — 표준 절차들 - [DECISIONS](/ops/decisions) — 아키텍처 결정 누적 - [인프라 인벤토리](/ops/inventory) — 서비스/포트/파일 전체 목록 --- # 백로그 (실행 큐) > 다음 세션의 entry point. 신규/Ready/in-progress/done lifecycle 로 무엇부터 할지가 5초 안에 보임. URL: https://docs.axelabs.ai/ops/backlog # 백로그 (실행 큐) > **다음 세션이 들어오면 이 페이지부터 본다.** [known-gaps](/ops/known-gaps) 는 "함정과 미해결 사실", 본 페이지는 "다음 실행 항목". [roadmap](/ops/roadmap) 은 마일스톤 큰 그림 — 각 백로그 항목은 마일스톤 ID (M1~M5) 로 묶임. 이미 ship 된 ✅ 항목은 [updates](/ops/updates) 의 Ship Log 로 promote (Phase 2 자동 = `B-axe-ship-update-hook`). > > **시간축 4 페이지** ([D-docs-updates-1](/ops/decisions)): **본 페이지** (현재) · [roadmap](/ops/roadmap) (미래) · [updates](/ops/updates) (과거) · [known-gaps](/ops/known-gaps) (사실). > > **Lifecycle**: 🆕 신규 (분류 안 됨) → 📋 ready (다음 세션이 집어가도 됨) → 🔧 in-progress (작업 중) → ✅ done (1주 후 archive 또는 삭제) → ⏸️ blocked (외부 차단) > > **운영 ritual**: > 1. **세션 시작**: 본 페이지 + [known-gaps](/ops/known-gaps) 같이 확인. 작업 주제의 항목 있으면 🔧 로 옮기고 owner 자기 이름으로 > 2. **세션 종료**: 끝낸 항목 ✅ + 새 발견 🆕. ✅ 항목은 다음 정비 시 archive 표로 이동 > 3. **항상**: 항목 = 한 줄 행동. 1주 안 끝날 항목은 마일스톤 (M-) 으로 승격하거나 쪼개기 ## 🔧 In-progress (작업 중) | ID | 항목 | 마일스톤 | owner | 시작일 | 비고 | |---|---|---|---|---|---| | **B-search-deploy** | docs.axelabs.ai UI 검색 (Pagefind) 라이브 배포 — 코드 머지 끝, `axe ship docs` 후 검색박스 동작 검증 | — | 운영자 | 2026-05-22 | [D-docs-search-1](/ops/decisions), 본 항목 ✅ 후 [known-gaps](/ops/known-gaps) 의 "docs 사이트 UI 검색" 섹션 제거 | | **B-axe-idp-1** 🔧 | **Blueprint = 플랫폼 OIDC Provider** — 로그인 1회로 전 서비스 ([D-axe-idp-1](/ops/decisions), 설계 SSOT [/architecture/platform-identity](/architecture/platform-identity)). **Phase 1 (모델 증명, 진행 중)**: Blueprint OIDC-OP(`.well-known/openid-configuration`·`jwks.json`·`/oauth/authorize`(getServerSession 재사용)·`/token`·`/register`·`/revoke`) + RS256 키(vault) + Prisma 모델(AuthCode·RefreshToken·Client) + `axe login` loopback PKCE + **frame 만** Blueprint 신뢰(`_is_blueprint_iss` 분기 + `auth_blueprint.py`). **Phase 2**: hive·cortex·index·matrix trust 이전 + frame/cortex 프록시([D-ops-14/15](/ops/decisions)) 통합·폐기. **Phase 3**: 인가 중앙화(`ent` 클레임 + fine scope) + 감사 UI + headless device-code + 키 회전. **비파괴 cutover**: 서비스별 flag `BLUEPRINT_ISSUER`/`BLUEPRINT_JWKS_URL` unset = 현행 그대로(롤백 = env 제거). | — | Claude Code (ai@axellc.com) | 2026-06-03 | 보안 핵심 → 설계 우선. **최종 검증 = 운영자 브라우저 SSO 1회 필요**(단독 e2e 불가). B-axe-cli-P0 후속. | ## 📋 Ready (다음 세션이 집어갈 수 있음) > 📋 항목은 의존성이 해소돼서 **지금 시작해도 되는 것들**. 마일스톤 ID 가 묶음을 보여줍니다. | ID | 항목 | 마일스톤 | 추정 | 의존성 / 비고 | |---|---|---|---|---| | **B-bp-mcp-teams-admin-consent** | Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 에 Microsoft Graph **`Chat.Read.All` + `ChatMessage.Read.All`** Application permission 추가 + tenant admin consent grant. 검증: `get_teams_message(url=...)` → 200 (현재 AADSTS65001 `invalid_grant` 502). 운영자 1회 작업. D-bp-mcp-teams-1/2/3 가 LIVE 됐으나 본 grant 부재로 tool 모두 사실상 unusable. `B-bp-mcp-calendar-2 admin consent` 와 동일 패턴 (known-gaps "Blueprint Azure App ID 혼동" 참조). 후속: `docker restart blueprint-app-green` 으로 MSAL `.default` 토큰 캐시 무효화 (D-bp-mcp-calendar-2 함정 #3). | M1 | 0.2d | 2026-05-26 본 세션 발견 (soohun.kang 1:34 메시지로 smoke test → 두 chat 모두 AADSTS65001). 운영자(Global Admin = soohun.kang) 만 가능 — `ai@axellc.com` 은 일반 사용자라 admin consent 권한 X (known-gaps D-bp-mcp-calendar-2 §"Global Admin = soohun.kang 단독"). | | **B-cortex-xlsx-import-run** 🆕 | Cortex live 후 잔여 — `Network_CRM.xlsx` 660 행 enrichment 1회 마이그. 전제: operator 가 connect_google + sync_google_now 완료 (Google Contacts → person artifact). 절차: xlsx 를 macmini 로 복사 → `cortex import-xlsx --path --owner ` (dry-run) → 매칭 통계 + private 카운트 확인 → 필요시 `--private-cols HPE,메모,소개` 조정 → `--apply`. owner_id 는 claude.ai 로그인한 Entra email (whoami 로 확인). 멱등 (subject+key 중복 skip). 완료 후 `~/Network/` → `~/Network.legacy/` archive + Network Manager Desktop OAuth client 폐기 | M1 | 0.3d | 2026-05-28 — cortex production live 후. CLI 전용 (로컬 파일이라 MCP tool 불가). **민감 컬럼 → `visibility=private` 태깅 구현 완료** (`--private-cols`, default HPE, commit `2ff4cf4`, [D-cortex-9](/ops/decisions)) — 잔여 = 파일 복사 + Google sync + 실 dry-run/apply (운영자 prerequisite) | | **B-cortex-visibility-phase2-mirror-contract** 🆕 | [D-cortex-9](/ops/decisions) Phase 2 (M6 착수 시) — Blueprint mirror job 이 `visibility='shared'` 만 가져가도록 계약 enforce + field-level privacy (payload `private` 하위객체 mirror 제외 + 선택적 `CORTEX_PII_PASSPHRASE` pgp 암호화) + private artifact 의 audit event redact. 선행 = M6 Blueprint artifact mirror ([D-bp-artifact-1](/ops/decisions)) | M6 | 2d | 2026-05-28 요청 3.2+3.4. M6 (2026 Q4) 종속 | | **B-cortex-visibility-phase3-e2e** 🆕 | [D-cortex-9](/ops/decisions) Phase 3 (opt-in) — client-held key sealed payload, 서버 복호화 불가. search_person(`payload::text ILIKE`)·Google push 포기 trade-off → 가장 민감한 메모만 "sealed" 모드 | M6 | 3d | 2026-05-28 요청 3.5 (보너스). 우선순위 최하 | | **B-cortex-mcp-catalog-publish** 🆕 | `axe mcp publish` 로 cortex_mcp 를 Vaultwarden MCP Connectors collection 에 등재 ([D-vault-mcp-catalog](/ops/decisions)). customers.yaml `sso.apps.cortex_mcp` 이미 4-key (client_id + application_id_uri + scopes + client_secret_env) 있음 → publish idempotent. 그러면 다음 사용자가 claude.ai connector 등록 시 Bitwarden 확장이 URI 매칭으로 cortex 4-piece auto-suggest | M1 | 0.1d | 2026-05-28. frame/hive/blueprint 와 동일 | | **B-cortex-google-oauth-verify** 🆕 | Cortex Google OAuth 앱 정식 검증(verification) 제출 → 동의 화면 "확인되지 않은 앱" 경고 제거. 앱 = External+production, 스코프 `auth/contacts` (**sensitive**), client `135512942819-l18ra7gkf1ac93ai8t4hf2h4jl6mi0a2.apps.googleusercontent.com` (project no. 135512942819). 현재 unverified → 동의 시 경고("고급 → 이동" 우회 중) + unverified 100-user 상한. 외부 개인 Gmail(kangthge@gmail.com 등) 온보딩 신뢰 문제로 검증 필요. **제출물**: 브랜딩(앱명·로고·지원이메일) + 앱 도메인(홈페이지·개인정보처리방침·서비스약관 URL) + 승인된 도메인 Search Console 소유권 verify(axelabs.ai) + 민감 스코프 정당화 사유 + OAuth 동의 흐름 데모 영상 → Google Auth Platform(`console.cloud.google.com/auth/*`) 제출. **금전비용 0** — contacts 는 sensitive(not restricted)라 CASA 제3자 보안평가 불필요(restricted였으면 수천 $/yr). 대안 기각: Internal user type = Workspace 한정이라 개인 Gmail 제외 부적합. 참고: production 전환으로 Testing 의 refresh_token 7일 만료는 이미 회피(sync 안정성 확보, 검증과 별개). | M1 | 0.5d (+Google 검토 수일~수주) | 2026-05-29 발견. 관문 = 개인정보처리방침/약관 페이지(axelabs.ai 정적 호스팅) — [B-legal-pages](/ops/backlog) 연계 | | **B-axe-secret-pull-noquote** 🆕 | `axe secret pull` 이 env_file 작성 시 값에 따옴표를 붙이는데, docker compose `env_file:` directive 는 따옴표를 literal 로 처리 (compose `.env` variable substitution 과 다름) → 컨테이너 안 secret 에 따옴표 포함 → DB auth / API key mismatch. cortex 가 첫 피해 (postgres auth 실패, 수동 sed 로 우회). fix: `axe secret pull` 이 quote 안 붙이게 (또는 env_file 소비처가 strip). frame/hive 는 Python config 가 우연히 strip 해서 안 터졌을 뿐 | M1 | 0.3d | 2026-05-28 cortex 발견. [known-gaps Cortex #2](/ops/known-gaps) | | **B-axe-tunnel-add-ingress** 🆕 | `axe tunnel add-ingress ` CLI 신설. `axe` 에 이미 `_cf_request` helper + CF API token (vault `Cloudflare API - axelabs`) 있음 → Cloudflare Dashboard remote-managed 터널의 ingress 를 GET→insert→PUT. cortex 등재 시 4-step curl 수동 (fetch config + jq insert + PUT) — 1 명령으로 축소. catch-all 보다 앞 + axe.axelabs.ai 의 no-path rule 보다 앞에 삽입 | M1 | 0.3d | 2026-05-28 cortex 발견. [known-gaps Cortex #7](/ops/known-gaps) | | **B-artemis-tunnel-orphan** 🆕 | `artemis-tunnel` (cloudflared, Up, 2026-05-09 생성) 가 `project=artemis` compose 라벨만 달고 현 `docker-compose.yml` 엔 정의 없음 — [D-ops-20](/ops/decisions)(터널 axelabs 스택 이전)의 잔재. 증상: artemis 에 `docker compose` 명령 시 orphan 경고 + `down/up --remove-orphans` 시 **살아있는 터널 삭제** 함정. 조치: 용도 확인(서빙 라우트 식별) → 불필요시 `docker rm -f artemis-tunnel`, 필요시 axelabs 스택 compose 로 라벨 이전. 2026-06-03 artemis launchd 스크립트(filings/collectors/reporters) `docker run`→`docker compose run` 전환으로 경고 노출 빈도↑. | M1 | 0.2d | 2026-06-03 발견. 현재 동작 무영향(안전). [D-ops-20](/ops/decisions) 잔재 | | **B-bp-entity-table-create** 🆕 | Blueprint Prisma `Entity` model 신설 ([D-bp-entity-canonical](/ops/decisions) Phase 1). schema = `Entity` 의 컬럼 (id text PK = axec/axev/axep/truvia, canonicalName 한글, customerId axe/realchoice, role 운영법인/GP/기타, isActive, createdAt) + EntityRole FK refactor (현재 EntityRole.entityId text → Entity.id FK). **데이터 seed**: (a) customers.yaml.entities iterate → Entity row 1 per id, (b) customers.yaml.entity_meta.(id).legal_name → canonicalName, (c) frame.shared.entity.legal_name 과 cross-check (D-bp-entity-canonical migration 의 frame i18n 정정 후 sync). REST endpoint `/api/admin/entities` (GET list + GET by id) — axe CLI 와 docs/inventory.mdx 가 fetch 가능. backward compat = customers.yaml.entities 는 SoT 유지 (deployment prerequisite, 부팅 self-contained) | M2 | 1d | [D-bp-entity-canonical](/ops/decisions). 선행 = `B-org-fanout-1` (organizationId FK pattern 일관) | | **B-customers-yaml-entity-meta-derive** 🆕 | `customers.yaml.entity_meta` (2026-05-27 본 세션 신설) 를 Blueprint Entity 의 derived view 로 전환 ([D-bp-entity-canonical](/ops/decisions) Phase 2). **3 sub-step**: (a) axe CLI 의 `cmd_customers_list` 가 Blueprint REST `/api/admin/entities?customerId=(id)` fetch → entity_meta 표시 (yaml 의 entity_meta block 은 보존하되 "derived from Blueprint Entity, do not edit manually" comment 추가), (b) matrix collector 에 `entity_meta_drift_check` 추가 — customers.yaml 의 entity_meta vs Blueprint Entity table cross-check, mismatch detect 시 alert, (c) `axe deploy` 부팅 시 entity_meta 가 stale 이면 warning (단 부팅은 계속). transitional 기간 = Phase 1 완료 후 1 주 검증 + Phase 2 진입 | M2 | 0.5d | [D-bp-entity-canonical](/ops/decisions). 선행 = B-bp-entity-table-create | | **B-axelabs-matrix-customers-yaml-direct** 🆕 | `axelabs.ai/matrix` UI 의 customer 정보 drift 영구 fix. **현재**: `/Users/axe/axelabs/app/api/matrix/customers/route.ts` 가 `http://axe-console-app:8228/api/customers` proxy. console-app = 폐기 대상 ([B-matrix-admin-retire](/ops/backlog)), docker ps 미가동, build 시점 prerender stale 본 노출. 2026-05-27 본 세션이 customers.yaml SoT (axec/axev/axep 한글명 + truvia rename + entity_meta 신설) 변경했으나 production UI 는 옛 본 (axe="에이엑스이 LLC" / axev 누락 / realchoice="리얼초이스 주식회사" / entities=["realchoice"]). **fix**: (a) route.ts 를 `fs.readFile('/axe-data/customers.yaml')` + js-yaml parse 로 변경 (console proxy 제거), (b) axelabs container `docker-compose.yml` 에 `/Users/axe/.axe:/axe-data:ro` mount + js-yaml deps 추가, (c) Next.js `revalidate: 0` 유지 + page 의 customers metric card 가 server-fetch (현재 client fetch → SSR 으로 변환 권장), (d) /api/matrix/services / launchd / containers / backups / console / state 6 route 도 동일 패턴 검토 (모두 console proxy). [D-bp-entity-canonical](/ops/decisions) "customers.yaml = deployment SoT" 정합 + [B-matrix-admin-retire](/ops/backlog) prerequisite (console-app 폐기 가능). 작업자 = axelabs container owner | 2026-05-27 | M1, 1d (route 6개 마이그레이션 + compose mount + rebuild + ship) | | **B-org-fanout-1** | Blueprint `Workspace/UsageLog/Agent` 에 `organizationId` FK 추가 (`prisma/schema.prisma:688` "별도 PR" 주석 해소) — D-bp-org-fanout 신규 결정 등재 동반 | M1 | 0.5d | cross-tenant 쿼리 누락 = 전 고객 노출 차단 | | **B-azure-multi-tenant** | `src/lib/auth.ts:101` 의 `AZURE_AD_TENANT_ID` 단일 env → per-org `Organization.azureTenantId` + multi-tenant Azure AD provider 분기 | M1 | 1d | B-org-fanout-1 선행 | | **B-legal-pages** | TOS / Privacy / DPA / PIPA 4 페이지 신설 + Blueprint 노출 | M1 | 1d | 법무팀 review 1회 필요 | | **B-edge-rate-limit** | Blueprint API edge rate-limit (per-org, IP 보조). Cloudflare WAF 또는 next/middleware | M1 | 0.5d | — | | **B-sentry-wire** | Blueprint + frame + hive 에 Sentry SDK + DSN secret + sourcemap upload | M1 | 0.5d | vault 에 SENTRY_DSN 등재 prereq | | **B-matrix-docker-deploy** | matrix Docker compose full deployment — launchd 또는 systemd 자동 시작 등록 | M1 | 0.5d | [D-matrix-1](/ops/decisions) | | **B-matrix-blueprint-mcp** | Blueprint 에이전트에 matrix MCP connector 등록 | M1 | 0.5d | [D-matrix-1](/ops/decisions) | | **B-matrix-admin-retire** | `admin.axelabs.ai` → `axelabs.ai/matrix` 리다이렉트 + `axe-console-app` 컨테이너 폐기. Caddy ingress 변경 + DNS CNAME 제거 | M1 | 0.5d | matrix 가 console 데이터 전량 흡수 완료 (2026-05-23). 인증(Entra ID OAuth) 미구현 — 상태 보드 공개 또는 별도 auth gate 결정 필요 | | **B-matrix-wan-monitor** 🔧 | matrix collector WAN/인터넷 가용성 모니터링 (`wan-gateway`/`wan-internet`/`wan-dns` + ISP 귀책 판별) — [D-matrix-2](/ops/decisions). **상태**: 코드 구현 완료 (matrix branch `feat/wan-monitor`, `cargo check` 통과) + docs draft 완료. **잔여 = 운영자 검토 → docs `axe ship` (D-matrix-2 origin/main 반영) → matrix `axe ship` (blue/green rebuild, NET_RAW+iputils-ping+env 3종) → `/matrix/api/status` 에서 wan-* 등장 + 강제 차단 테스트**. 시작점 = 댁내 wifi 민원용 wifi.log 분석 (`~/wifi-complaint/`) | M1 | 0.2d (ship만) | 2026-06-03, ship 대기 | | **B-matrix-wan-report-tool** 🆕 | matrix `get_wan_report` MCP tool (D-matrix-2 v2) — `check_results` 에서 `wan-internet`/`wan-gateway` 전환을 읽어 **장애 구간 (start/end/지속) + 업타임% + 귀책** 을 ISP 민원 제출용 타임라인으로 출력. wifi.log 일회 분석 (2026-06-03 `~/wifi-complaint/`) 의 상시 자동화 버전 | M1 | 0.5d | [D-matrix-2](/ops/decisions) v2, B-matrix-wan-monitor 선행 | | **B-netheal-install** ✅ | 인터넷 자가치유 데몬 — [D-matrix-4](/ops/decisions). **완료 (2026-06-04)**: pty 고갈로 sudo 불가 → `osascript ... with administrator privileges` 우회 설치, `com.axe.netheal` LaunchDaemon 가동 중 (PID 16331, `netheal started` 로그, 오발동 0), inventory.mdx 등재. **잔여(선택)** = 콘솔에서 실 WiFi-바운스 치유 1회 검증 (원격 ~15s 끊김) | M1 | ✅ | 2026-06-04 | | **B-frame-runtime-invariant-test** | DB CHECK runtime test (entity_kind 위반·ownership all-or-none·bounds·link_kind 등 invalid INSERT 차단 검증) + sum invariant (active row sum(numerator)==denominator) | M2 | 0.5d | [D-ops-22](/ops/decisions) 후속. tests/test_shared_fund_domain.py 의 metadata test 외 runtime layer | | **B-bp-workspace-entity-nm** | Blueprint `WorkspaceEntity` join migration — D-bp-entity-1 plan 의 `Workspace.entityId` scalar → N:M 수정 | M2, M3 | 1d | D-bp-entity-1 plan 수정 등재 | | **B-bp-para-dispatch-spike** | Path B Spike — DB 변경 없이 단일 workspace 로 PARA dispatch 흐름 검증 (3~5일) | M3 | 4d | 본 구현 (Path A) 전 권장 | | **B-hive-payroll-cell-compare** | 첨부 급여대장 셀별 정확 일치 검증 (axec/axev 4월·5월 데이터) | M4 | 1d | payroll v2 ✓ 검증 마지막 단계 | | **B-hive-dkim-prod** | hive 메일 발송용 DKIM/DMARC DNS prod 확정 (Cloudflare API token 자동화 검증) | M4 | 0.5d | [D-hive-23](/ops/decisions) | | **B-backup-restore-drill** | frame-postgres / blueprint-postgres / hive-postgres restore drill 첫 실시 + 회고 + 정기 cron. **부분진척 2026-06-03**: `axe drill`(D-config-14 restic canary) 메커니즘 강화 — snapshot freshness 검사(`--max-age-days`·`--strict`, silent backup-stop 탐지) + robust canary(non-empty+YAML parse, customers.yaml live-drift false-FAIL 해소; axe-cli `fc0f29f`). **잔여 = 본 항목 핵심**: frame/blueprint/hive **postgres 실제 restore drill + 회고** (restic 스냅샷은 DB Docker 볼륨 미포함이라 별도 pg_dump 기반 drill 필요) | M5 | 1d | [D-hive-backup](/ops/decisions), [D-config-14](/ops/decisions) | | **B-mysrt-backup-decision** | mysrt-postgres 의 SRT 외부 SOT 여부 사용자 확인 → Tier A 합산 또는 보류 결정 등재 | M5 | 0.5d | — | | **B-frame-observability** | frame `observability.py` 의 Prometheus percentile 계산 — 현재 TODO 상태 | M1 | 0.5d | `/Users/axe/frame/src/frame/observability.py` TODO 코멘트 | | **B-frame-lpm-classify** | LLM classify pipeline (`raw_transaction → journal_line` 자동 분류) — 현재 `analyze_file_format` 만 존재. **ETA 3-단계 (2026-05-25 RE^4 회신)**: (a) **D+90 (2026-09-01) minimum viable** — LLM proposal + 운영자 confirm + commit, 정확도 60~80% (axe 백테스트), (b) **D+120 (2026-10-01) rule-based pre-classifier 결합**, 정확도 80%+, (c) **2026-12-01 stable** — axec/axev 1년 backtest + 회귀 green. Truvia D+30 사이클은 수작업 분개 (`/services/frame#신규-customer-1-차-사이클-runbook`) 로 보완 | **M2 (top)** | 누적 4-6 주 | `/Users/axe/frame/DECISIONS.md` M2 Phase C, 트루비아 RE^4 | | **B-frame-fund-cross-journal** | 펀드 cross-entity mirror 분개 도구 (GP commitment / 운용보수 / 성과보수) | M2 | 2d | `/Users/axe/frame/DECISIONS.md` D-frame-N4. B-frame-cross-journal-link 선행 | | **B-frame-vat-schema** | journal_line 의 VAT (부가세) 컬럼 계산 로직 — 현재 컬럼만 예약, 로직 0 | M2 | 1d | `/Users/axe/frame/DECISIONS.md` "VAT handling... but no logic yet" | | **B-hive-phase2-spike** | hive Phase 2 (성과평가 OKR/KPI, AI 목표 추천) 기능 명세 SPIKE 문서 작성 | — | 3d | `/Users/axe/hive/DECISIONS.md` Phase 2 scope. 본격 구현 전 명세 | | **B-hive-dispatch-channel** | hive 이벤트 채널 5개 추가 (`employee.hired/terminated`, `payroll.finalized`, `compensation.granted`, `blueprint.agent.*`) | — | 1d | `/Users/axe/hive/DECISIONS.md` KNOWN_INCOMING_EVENT_TYPES Phase 2 | | **B-hive-compensation-validate** | hive compensation Phase 3 검증 — 현재 Phase 1 schema 만, Phase 3 tools 검증 조건 정의 | M4 | 1d | `/Users/axe/hive/DECISIONS.md` Phase 1=schema / Phase 3=tools | | **B-bp-artifact-propose-api** | Blueprint `POST /api/artifact/propose` REST endpoint 신설 — **cross-service automation 용 generic surface**. axelabs platform 내부 worker / 외부 서비스 / 사용자 정의 automation 이 artifact propose 가능. body = ProposeInput JSON. Bearer `ARTIFACT_PROPOSE_TOKEN` AppSetting auth (service-account, NextAuth session 미요구). admin UI 의 NextAuth-gated `/api/admin/mcp-schemas/refresh` 와 별 surface. response = StoredArtifact JSON | M6 | 2d | [D-bp-artifact-1](/ops/decisions). 통합 자체의 도메인 결정은 각 호출자 측 별도 | | **B-mysrt-srt-risk-review** | SRT 약관 위반 risk 재검토 (자동 예매 기능, 계정 정지 risk) + 운영 정책 재정의 | — | 0.5d | `/Users/axe/mysrt/README.md` "약관 위반... 계정 정지" | | **B-ops-stack-consolidation** | docker-compose 7-stack 통합 운영 시작 (`~/.axe/axelabs/docker-compose.yml` 신설 + 12→7 마이그레이션) | M1 | 1d | [D-ops-20](/ops/decisions) | | **B-bp-artifact-arch-doc** | Blueprint artifact + PARA 아키텍처 결정 문서 본격 작성 — D-bp-artifact-1~7 등재 후 implementation 결정 문서 (`Artifact` 테이블 정확한 컬럼 / Citation 6-kind resolver contract / PARA dispatch field-level engine / MCP tool 시그니처 / ctx 진화 path) | M6 | 2d | [D-bp-artifact-1](/ops/decisions) ~ [D-bp-artifact-7](/ops/decisions). 코드 0줄, ADR landing | | **B-bp-artifact-citation-resolver** | Citation kind 별 resolver (onedrive / frame.* / hive.* / teams.message / mail.thread / external.web) — typed contract + 통합 read API | M6 | 3d | B-bp-artifact-prisma 선행 | | **B-bp-artifact-dispatch-engine** | PARA dispatch field-level engine — Project artifact 종료 시 Area/Resource/Archive routing rule + 자동 분배 (LLM 제안 + 사용자 confirm). copy + link 2 mode 지원. M3 의 workspace-level dispatch (B-bp-para-provenance-fields) 의 field-level 진화 | M6 | 5d | M3 PARA Dispatch land 선행 권장 + B-bp-artifact-link-table | | **B-bp-artifact-query-api** | Cross-PARA / cross-artifact / time-travel query — **MCP tool 위주** (`query_knowledge`, `get_artifact`, `list_artifacts` 등 [D-bp-artifact-7](/ops/decisions) Layer 2 정합). 웹 UI 만 최소 REST endpoint (curation UX). agent SDK 가 grep 대신 typed query | M6 | 3d | B-bp-artifact-citation-resolver 선행 | | **B-bp-mcp-paraLayer-tools** | `list_projects` / `list_areas` / `list_resources` / `search_archive` 등 paraLayer 별 MCP tool 분리 — LLM intent clarity (tool name 자체에 paraLayer 의도 드러남). + `create_workspace(paraLayer, ...)` / `close_workspace(id, dispatch_plan?)` / `reclassify_workspace(id, new_paraLayer)` workspace lifecycle tool | M6 | 2d | [D-bp-artifact-6](/ops/decisions). B-bp-artifact-query-api 와 같이 진행 가능 | | **B-bp-workspace-lifecycle-ui** | 4 paraLayer 별 UI 페이지 신설 — `/axe/projects` (기존) + `/axe/areas` / `/axe/resources` / `/axe/archive` (신설). 각 page 의 "+New Workspace" 가 paraLayer 그 page 에 고정. + "Close Project" / "Reclassify" / dispatch plan 모달. ARAClient 의 3-tab 폐기 (분리로 대체) | M6 | 5d | [D-bp-artifact-6](/ops/decisions). B-bp-artifact-prisma 선행 (paraLayer 컬럼 활성) | | **B-bp-ctx-skill-evolve** | ctx skill 진화 — markdown PKM → artifact curation interface. flows: agent propose → ctx review → confirmed fact + audit. markdown 은 보조 (review UX, archive). 기존 ctx sync/review/status 3 mode 호환 유지 + artifact 모드 추가. **+ markdown → artifact 점진 migration mode** (LLM extract 시도 → ctx review queue 등재 → 사용자 confirm/edit/reject → confirmed artifact + 원본 entry `` 마킹) | M6 | 5d | B-bp-artifact-query-api 선행. [D-bp-artifact-5](/ops/decisions) | | **B-bp-ctx-migration-mode** | ctx review 의 markdown → artifact propose flow (점진 마이그레이션 UX) — review queue 에 migration_propose event 표시 + source markdown 발췌와 extracted typed fact 나란히 + confirm 시 markdown entry 에 archive 마킹 자동 삽입. B-bp-ctx-skill-evolve 안 포함될 수 있으나 UX 작업량 명시 분리 | M6 | 3d | B-bp-ctx-skill-evolve 와 같이 진행 | | **B-bp-markdown-archive-marker** | migrated 마킹 표준 (``) — 어떤 markdown entry 가 어떤 artifact 로 변환됐는지 trace. ctx review confirm 시 자동 삽입 + grep tool (`ctx migrated-trace `) | M6 | 1d | B-bp-ctx-migration-mode | | **B-bp-knowledge-overview** | Knowledge Overview dashboard (`/axe/knowledge`) — 3-pane layout (4 layer count + state / Sankey flow 지난 30일 / Cross-PARA query 입력) + Area health alert (stale 30일, inactive 90일) + drill-down view (PARA flow over time / artifact graph / provenance trace) | M6 | 5d | B-bp-artifact-query-api + B-bp-mcp-paraLayer-tools 선행 | | **B-bp-artifact-ic-pilot** | IC pipeline 첫 vertical pilot — 1 deal (Sentry 권고) 의 §재무 가 raw markdown 대신 `ICMemoArtifact` 의 typed fact read. before/after 토큰 측정 + IRR 결정론성 검증. M6 활성화 검증 datapoint | M6 | 5d | B-bp-ctx-skill-evolve 후. Stage 0 PoC 의 production 변환 | | **B-index-blueprint-citation-kind** | Blueprint `src/lib/artifact/citations/index.ts` resolver 신설 — `index.*` kind 14 sub-type 의 stable ID → MCP call mapping. `index.financial_output` → `query_irr` / `index.deal` → `get_deal` 등 | M7 | 1d | [D-index-6](/ops/decisions) Phase 1 | | **B-index-ic-skill-push-mode** | ic skill SKILL.md 에 신규 Rule 25 (`--push-to-index` mode) 추가 + 산출물 5종 (financial_model xlsx + scenario_deltas yaml + exit_matrix yaml + memo md + recap yaml) → index propose 자동화 script. ctx review queue 경유 | M7 | 2d | [D-index-8](/ops/decisions) Phase 1, B-index-phase0-mcp-tools 선행 | | **B-index-frame-event-mirror** | pg_notify `index_events` channel (capital_call / valuation_recorded / exit_distributed) + frame 측 `frame-worker` LISTEN 추가 + cross_journal_link 분개 mirror. frame M2 의 commitment_ledger 정합 | M7 | 2d | [D-index-9](/ops/decisions) Phase 2, M2 의존 | | **B-index-axev-fund2-frame-register** | frame 에 `axe_ia_002` (액스 투자조합 2호) 등록 — Iippo 2호 split 의 fund entity 가 frame 에 부재 (현재 axe_ia_001 만). `frame register-entity --id axe_ia_002 --kind kvf` + fund_meta JSONB 채움 | M7 | 0.2d | M2 frame 펀드 회계 + index Phase 0 prereq | | **B-ic-push-mode-impl** | ic skill `--push-to-index` 5th mode 구현 ([D-index-11](/ops/decisions)) — SKILL.md Rule 25 + Step 5.5 (40-60 lines) + gate_memo.sh post-V_COUNT atomic propose dispatch (80-120 lines, idempotency_key 계산 + `propose_deal_closure` curl + polling) + orchestration-initial.md (50-80 lines) + build_3fs.py / calc_irr.py / postmortem_stub.py minor edits. 총 ~280-350 lines. **backward compat 100%** (flag 미사용 시 기존 동작). `ic/index_push_state/v(N).json` local checkpoint 도입 | M7 | 2d | [D-index-11](/ops/decisions), B-index-phase0-mcp-tools 선행 | | **B-portfolio-mgmt-deprecation** | `portfolio-management` skill 3-phase deprecation ([D-index-12](/ops/decisions)) — Phase 0 (즉시) SKILL.md 에 deprecation notice + Blueprint suggest UI 에 marker / Phase 1 (M7 Phase 1 launch 후) KPI 표 → `pmc/references/kpi-catalog.md` copy + cross-link / Phase 2 (3개월 후) SKILL.md disable (Blueprint search 에서 미노출) / Phase 3 (6개월 후) archive (`.deprecated/` 이동). 점진 패턴 [D-bp-artifact-5](/ops/decisions) 정합 | M7 | 0.5d per phase | [D-index-12](/ops/decisions) | | **B-investor-relations-deprecation** | `investor-relations` skill 3-phase deprecation ([D-index-12](/ops/decisions)) — portfolio-mgmt 와 동형. LP 보고 구조 + 펀드 성과 지표 → `pmc/references/ir-lifecycle.md` migration. Phase 0/1/2/3 timing 동일 | M7 | 0.5d per phase | [D-index-12](/ops/decisions) | | **B-index-error-model-enum** | `src/index/error_model.rs` Rust enum SoT ([D-index-14](/ops/decisions)) — 12+ case (IdempotencyConflict / ValidationWarning / SchemaNotFound / CitationResolveFailed / FinancialModelLocked / DslSyntaxError / UnknownEnum / DealNotFound / FundEntityNotFound / ServiceUnavailable / RateLimitExceeded / InsufficientScope) + `impl From IndexError to McpErrorResponse` deterministic mapping. ic/pmc skill 측 `lib/index_client.py` (또는 동형) 의 code 별 분기 handler. unit test 가 모든 case 의 wire-level response 검증. silent fallback 절대 금지 | M7 | 1d | [D-index-14](/ops/decisions), B-index-skeleton-mirror 선행 | | **B-index-integration-fixtures** | `/Users/axe/index/tests/fixtures/ic_integration_phase0.json` + `pmc_integration_phase1.json` + `cross_skill_regression.json` ([D-index-14](/ops/decisions), D-index-15 정합) — Iippo (1:N fund) + Sentry (option round) + Canopy (multi-round) 3 deal regression. ic_integration_phase0 assertion: `propose_deal_closure` 성공 + `compute_outputs(base)` IRR ±0.5pp vs ic `v(N).xlsx` cell J42 + citation roundtrip resolve. cross_skill_regression: 5 skill (ic/dd/vc-deal-sourcing/portfolio-management/investor-relations) 동시 propose → audit_trail 충돌 없음 + 마지막 silent overwrite 검출. PR merge gate (`axe test index --fixtures`) | M7 | 1.5d | B-index-phase0-mcp-tools + B-ic-push-mode-impl 후 | | **B-index-mcp-checklist-extension** | [/architecture/mcp-server-checklist](/architecture/mcp-server-checklist) § 8 운영 14 체크포인트 → **21** 항목으로 확장 — skill 통합 5 항목 (#17 skill idempotency 2× call 409 검증 / #18 citation resolver roundtrip ±0.001 percent / #19 cross-skill conflict 시 audit_trail 또는 last-write-wins + warning modal / #20 graceful degradation: index 죽이고 ic 호출 시 markdown-only fallback / #21 schema evolution: 새 field 추가 시 old skill 호환). `axe test index --accept-gate` 명령 + 24h production monitor (error rate under 0.1 percent, citation cache hit over 95 percent) | M7 | 1d | B-index-error-model-enum + B-index-integration-fixtures 선행 | | **B-index-vision-boundary-rule** | xlsx vision 보조 vs PDF vision 분리 룰 ([D-index-15](/ops/decisions) worst-case #2 차단) — index 의 `ingest_financial_model_xlsx` 는 xlsx sheet/row label 만 vision (LLM 보조 inference), numeric cell value 는 mechanical (Rust calamine) 파싱 강제, PDF (IR pack 등) 는 vision 불가 → ingest skill 의 markdown clone 만. dataroom IR PDF 의 "5,941원" 가 "5,941엔" 오인식 → DB authoritative cell 가 되는 worst-case 영구 차단. 본 룰 `/services/index/financial-model.mdx` 추가 + ingest tool 의 input validation (PDF blob 거부) | M7 | 0.3d | [D-index-15](/ops/decisions), B-index-phase0-mcp-tools 선행 | | **B-index-proceeds-bridge-retrofit** 🆕 | [D-index-22](/ops/decisions) 후속 — Iippo·Canopy 의 `legacy_ev` → `ev_bridge` per-leaf 전환. 두 deal 은 EV-based 이나 per-leaf 에 exit_ev_krw/exit_net_debt_krw/exit_stake_pct 미itemize (validate Check 7 = legacy_ev WARN 2건). **작업**: (a) Canopy — `exit_assumptions` 의 net debt 300억(EqV 498억) 를 per-leaf 로 분해, 40+ leaf 각각 exit_ev + stake 도출 → axe_proceeds ±2% reconcile, (b) Iippo — asset-light net debt 0, 30+ leaf 에 exit_ev(=proceeds/stake) + stake; **동시에 entry-F/D-flat dilution 교정** (중간 라운드 희석 미반영 → exit_stake 를 round-별 희석 반영값으로 — proceeds 수 pp 과대 낙관 해소), (c) proceeds_basis=ev_bridge 변경 + re-ingest + IRR ±1pp regression. Sentry(equity_value)·pre-IPO 3(mom) 는 bridge N/A 라 대상 아님. | M7 | 1d | 2026-05-29 [D-index-22](/ops/decisions). **검토 후 보류** (2026-05-29 "정확도 심화" 세션): (a) Canopy 는 per-leaf 순부채가 **원자료에 없어** itemize 하려면 시나리오별 부채를 근거 없이 가정해야 함 ([D-index-22](/ops/decisions) "없는 부채 날조 안 함" 원칙상 불가) — 모델-level net debt 300억은 이미 명시·정확, proceeds/IRR 도 검증됨 (즉 현 데이터는 정확, itemize 만 미수행); (b) Iippo net_debt=0 이라 itemize 가 tautological (exit_ev=proceeds/stake 환원); (c) **dilution 교정은 [D-index-24](/ops/decisions) 와 충돌** (Iippo/Sentry 는 executed → 모델 사후 재작성 금지). 숫자 불변 + WARN→PASS 표현 rigor 만이라 효과 대비 위험 큼 → legacy_ev WARN 유지가 정직. 실 DD per-leaf 부채 데이터 확보 시 재개 | | **B-index-skill-overlap-sunset** | [D-index-17](/ops/decisions) overlap 기간 종료 후 Blueprint mirror 측 `.claude/skills/{ic,ingest}` 제거. 3 sunset 기준 모두 충족 시: (a) 다음 3 deal IC 가 `index/skills/ic/` 기반 정상 작동, (b) Blueprint Claude Agent SDK 의 index/skills/ mount/sync 자동화 확립, (c) ic skill `--push-to-index` mode 일반화 ([B-ic-push-mode-impl](/ops/backlog)). 작업 = Blueprint repo `.claude/skills/ic` + `.claude/skills/ingest` 디렉토리 삭제 + Claude Agent SDK 가 index/skills/ 또는 MCP 로 fetch 하도록 변경. drift 0 검증 (sha256). 충족 안 되면 overlap 지속 | M7 | 0.5d | 3 기준 충족 후 | | **B-index-skill-versioning-schema** | [D-index-17](/ops/decisions) Phase 2 — `shared.skill_resource` table + `index.skill_resource@1.0` schema. name + version + skill_md + scripts (JSONB) + references_md (JSONB) + domain + locked_at + superseded_by. Per-deal trace: `index.ic_decision.skill_version_used` field 추가 → "이 IC 메모는 ic skill v8.3 으로 작성" 영구 audit. Cross-deal "ic v8.3 vs v8.0 평균 IRR" 분석 가능 | M7 | 1d | [D-index-17](/ops/decisions) | | **B-index-dd-vc-sourcing-ownership** | [D-index-17](/ops/decisions) Phase 1 후속 — `due-diligence` + `vc-deal-sourcing` skill 의 index 산하 이전 검토. 두 skill 모두 투자 도메인이지만 ic + ingest 보다 결합도 낮음. 별도 결정 필요 (D-index-? 신규) | M7 | 0.3d (검토) + 1h (이전) | [D-index-17](/ops/decisions) overlap 안착 후 | | **B-index-ingest-structured-extraction** 🔧 ⭐ | **✅ L1 SHIPPED (2026-05-30, [D-index-41](/ops/decisions), blueprint PR #379 merged → `~/.claude/skills/ingest` v2)** — convert_xlsx v2 (`{stem}_xlsx.cells.json` + key_outputs 자동탐지 + Model Summary/Key Outputs md) + convert_pdf v2 (`figures.json`). Apposter 20-sheet model → 31 key outputs(Exit IRR 57.2%/MoM 8.60x/EV 270억) 자동 surfacing(이전 truncated 주석 매몰), test 40/40, 의존성 0. **+ ✅ L2-core artifact화 SHIPPED ([D-index-42](/ops/decisions), index `4ca5868`, blue/green live)** — ingest sidecars → proposed artifacts (`artifact`+`citation`+`artifact_event` op=propose, RLS index_app+GUC, idempotent). artifact store 0→35 (dormant 던 store 첫 가동). **+ ✅ lifecycle SHIPPED ([D-index-43](/ops/decisions), `3eece93`+`118568a`, blue/green live)**: Query API(`query_artifacts`/`get_artifact`) + ctx review(`confirm_artifact`/`reject_artifact`) + L3-lite `reconcile_artifacts`(metric spread flag) + L2 `draft_seed_from_artifacts`(보수적 scaffold — 날조금지 구조강제, draft non-ingestable). 6 신규 MCP tool, 27 test. **+ ✅ metric_kind 세분 ([D-index-44](/ops/decisions), blueprint PR #380 — valuation/multiple/moic 분리+curation) + lifecycle 회귀 스위트(8 test, audit 영속성 발견) SHIPPED**. **잔여 (축소, 📋): ctx review UI(Blueprint markdown-diff) · Query API → /ic·seed-building primary read path 통합 · 전 deal artifact화(현 Apposter 1) · L4 versioned diff**. **⭐ 중요 — ingest 고도화: 단순 md transcription → source-type-aware structured extraction** (사용자 2026-05-30 "IR·Excel 이 md 로 소화 안 되는 케이스 많음"). ingest 는 파이프라인 정문이라 여기 손실은 ic·index 에서 복구 불가 (GIGO). 현 md photocopy = L0. **L1 (즉시·저위험)**: source-type routing — xlsx→구조화(HTML table 또는 cell+merge JSON), IR deck(PDF/PPT)→페이지 이미지+vision, 일반 prose→md 유지. 사용자 체감 손실(Excel 표 구조·deck 차트)의 80% 해결. **L2 (핵심 레버리지)**: transcription→typed extraction — ingest 가 베끼는 대신 index typed 스키마를 채움 ([D-index-31](/ops/decisions) intake 5필드 series·round_size·committed·entry_date·pre/post + exit 가정 + cap table → seed·financial_model 초안 생성). 지금 사람이 dataroom 읽고 손으로 쓰는 seed yaml 을 ingest 가 초안 잡고 사람은 검토·confirm — 정문 자동화. **L3 (품질 배수)**: cross-source reconciliation — deck vs xlsx vs cap table 수치 불일치 flag (silently 택1 금지) + confidence 태그 (mechanical=high · vision-inferred=review). **L4**: versioned/idempotent 재ingest diff (v2 IR vs v1 변경점). **불변식**: ① 숫자는 항상 mechanical(calamine) → vision 은 정성·cross-check 만 ([D-index-15](/ops/decisions) vision-boundary 정합·확장), ② 원본은 항상 evidence blob 보존 (fidelity SoT, 클론은 파생·교체가능), ③ 추출 fact 마다 citation(page/cell) 자동 부착. **선행**: 구현 전 D-index 결정으로 설계 ratify (extraction-not-transcription + source-type-aware 명문화). 관련 [B-index-vision-boundary-rule](/ops/backlog). | M7 | L1 1d / L2 3-5d / L3 2d | 2026-05-30 사용자 "중요". [D-index-15](/ops/decisions)+[D-index-31](/ops/decisions) 위에서 진행, D-index 결정 선행 | ## 🆕 신규 (분류 안 됨) > 발견했지만 아직 마일스톤·우선순위·owner 미정. 다음 정비 시 📋 로 옮기거나 [known-gaps](/ops/known-gaps) 로 강등. | ID | 항목 | 발견일 | 비고 | |---|---|---|---| | **B-index-judgment-in-seeds** ✅ ⭐ | **완료 2026-06-03 ([D-index-46](/ops/decisions), index `8ec2c10`)** — seed.yaml 이 judgment 단일 SoT: per-seed `judgment:`(assumptions+calibrations) + `seeds/_corpus.yaml` + seed-ingest `--emit-judgment`(natural-name anchor→corpus id 해소, idempotent). 23 seed 마이그레이션 후 `backfill.rs`+`corpus.rs`(2824 LoC) 삭제. GATE: 재ingest +0 new · committed wipe→재현 byte-identical(42 dup citation pruned) · append-only intact · 51 test · validate-seeds Δ0. 신규 deal 자동 artifact화 — hardcode transcription 0. | 2026-06-03 ✅ | [D-index-46](/ops/decisions) | | **B-index-epic2-confidence-load-bearing** 🆕 | [D-index-45](/ops/decisions) Epic 2 — confidence 를 장식→load-bearing. 입력 assumption 신뢰도를 출력(E[CF]·E[MoM])으로 전파: 미확정/저신뢰 가정 위 딜은 저신뢰 플래그. provenance 위 confidence rollup. epistemic 차별점 심화. | 2026-06-03 | M7, 2-3d | | **B-index-epic3-self-calibrating-loop** 🆕 ⭐ | [D-index-45](/ops/decisions) Epic 3 (북극성) — prediction→outcome→recalibration. IC 시점 calibrated E[CF]/P(loss) 를 불변 `prediction` artifact 로 freeze → 실제 outcome(exit·후속·writeoff) 포착 → deal-class 별 예측-실제 bias 노출(체계적 낙관/비관) → 다음 IC 자동 보정. base-rate corpus(현 35 comp+5 base_rate) 누적. Palantir 가 안 주는 복리 자산. **outcome-capture 는 B-index-postmortem-loop 가 공급.** | 2026-06-03 | M8, prediction freeze 선행 | | **B-index-pmc-operational** 🆕 ⭐ | [roadmap M7 Phase 3](/ops/roadmap) — pmc(Post-Money Care) 운영 파이프라인. 현재 pmc 는 skill skeleton([D-index-12](/ops/decisions)) + P3 에서 blueprint mirror 됐으나 **index persistence 미구현**. 8-agent 분기 cycle(KPI/risk/NAV/postmortem) → `index.portfolio_kpi`·`risk_alert`·`valuation_snapshot`·`postmortem` typed fact + pmc `--push-to-index`. IC결의~Exit(5년+) 포트폴리오 케어 phase. | 2026-06-04 | M7 Phase 3 | | **B-index-postmortem-loop** 🆕 ⭐ | [roadmap M7 Phase 3](/ops/roadmap) — post-mortem 루프: ic `postmortem_stub` → reminder cadence → pmc fill → `index.postmortem`(IC 예측 vs 실제 회고). **Epic 3 의 outcome-capture leg** — prediction(freeze)과 짝이 되어 self-calibrating 루프를 닫음. ic postmortem_stub/reminder(투자 phase) + pmc fill/render/push(care phase) 2-track([D-index-12](/ops/decisions)). | 2026-06-04 | M7/M8, Epic 3 짝 | | **B-index-evidence-durability** ✅ v1 ⭐ | **v1 완료 2026-06-04 (index `b273979`, deployed)** — citation `sha256` content-anchor(범용·무의존) + Blueprint-id 필드 readiness(`workspace_id`/`driveItemId`/`marker`/`rel_path`). `CitationAnchor`+`is_durable`(durable = sha256 OR drive_item_id OR workspace_id+rel_path) · ingest sidecar sha256 emit + **propose-time fallback hashing**(skill 무변경에도 durable) · `verify_citations(deal?)` MCP tool · 현황 페이지 "citation durability %" 라인. additive(기존 357 artifact/545 citation byte-identical) · 80 test · validate-seeds Δ0 · 공개 페이지 누출 0. 폴더 이동·삭제 분석 + 운영자 "workspace unique id" 제안에서 도출. | 2026-06-04 ✅ | [D-index-45](/ops/decisions)/47 | | **B-index-evidence-durability-v2** 🆕 | [roadmap M7 Phase 3](/ops/roadmap) — v1 deferred 잔여(cross-repo): ① 기존 545 path-only citation **backfill**(resolvable source hash→ref 에 sha256 merge, 또는 deal 별 재ingest) ② **Blueprint resolve API**(stable id→현 위치, sync 갱신) ③ `driveItemId`/`workspace_id` 채우기(Blueprint `Workspace` 쿼리) ④ ic/ingest skill blueprint mirror(런타임이 sidecar sha256 emit). v1 schema/predicate 가 이미 수용. | 2026-06-04 | M7 Phase 3, cross-repo | | **B-index-skill-p1-stamp** ✅ | **완료 2026-06-03 ([D-index-47](/ops/decisions), index `11aca12`, deployed)** — `artifact_event`(op=propose).payload_after 에 `_provenance`(skill·skill_version·bundle_sha256·schema_envelope_version·frozen_enums_hash·actor·surface) 스탬프 + propose 시 schema-drift hard gate(불일치→`SCHEMA_CONTRACT_DRIFT`, insert 전 fail-closed). frozen_enums_hash=`3b58b4b0…`(전용 SoT fn, pinned tripwire test) + /index/schemas 가 publish(ic-push discover+pin). 66 test(+17) · additive(기존 357 untouched, append-only 라 retro-stamp 불가) · validate-seeds Δ0. live: 드리프트 probe→SCHEMA_CONTRACT_DRIFT(무오염). | 2026-06-03 ✅ | [D-index-47](/ops/decisions) | | **B-index-skill-p2-ci-mirror** ✅ ⭐ | **완료 2026-06-03 ([D-index-47](/ops/decisions), index `6edf0ff`)** — ⭐ **"런타임 fix" 는 이미 달성**(P0 + 기존 sync): ic/ingest 가 index SoT↔blueprint origin/main↔런타임 전부 byte-identical(attest: ic `cb9b9ec`/ingest `7d09bcd` IN-SYNC). 그래서 P2 = content push 아닌 **메커니즘**: `index-skill-sync.py`→`--attest`(deterministic dir-sha256 drift sentinel, advisory) 재범위(깨진 byte-delivery 제거) + `index-skill-mirror.sh`(미래 미러, sanctioned PR flow, pmc fail-closed 거부) 빌드. **blueprint 무쓰기·무push·무PR**, pmc 보류(P3). | 2026-06-03 ✅ | [D-index-47](/ops/decisions) | | **B-index-skill-sync-wrapper-retire** ✅ | **resolved 2026-06-03 — neutralized by P0** — `blueprint/scripts/sync-index-skills.sh` 는 (a) origin/main 에 없음(untracked local-only) (b) 호출하던 launchd `ai.axe.index-skill-sync.plist` 가 P0 에서 폐기 → **invoker 0 = 무해**. 물리 파일은 blueprint working-tree 의 untracked 파일(병렬 세션 활성 편집 중 — re-point 가능성) → index scope 밖, blueprint tree owner 가 rm/re-point. index-side action 불필요. | 2026-06-03 ✅ | [D-index-47](/ops/decisions) | | **B-index-skill-p3-contamination-purge** ✅ ⭐ | **완료 2026-06-03 ([D-index-47](/ops/decisions), blueprint PR #381 gate + #382 pmc, both squash-merged origin/main)** — 8 투자 skill(ic·ingest·due-diligence·vc-deal-sourcing·investor-relations·portfolio-management·legal-compliance + pmc)에 `owner:index` 태그 + boot(`start.sh`)/webhook(`route.ts`) 양 경로 gate(customer `services.index` 판정 → 비-index `rm -rf`+skip purge, index keep). **axe 3중 안전**(services.index + `:-axe` 기본 + fail-safe keep + 운영자 skills-sync.sh ungated → simul KEEP 16/16) · realchoice/Truvia PURGE 7/KEEP 9 · `customers.ts customerHasService` + test 21/21 · tsc 0. pmc(index-only)도 base 로 mirror(gated→axe 만). **운영자 잔여**: Truvia `BLUEPRINT_CUSTOMER_ID` 확인 + 컨테이너 재기동(boot gate 가 purge) + R7 DB residue(assigned skill row). rollback=revert(axe 무영향). | 2026-06-03 ✅ | [D-index-47](/ops/decisions) | | **B-index-skill-owner-frontmatter** 🆕 | [D-index-47](/ops/decisions) follow-up — ic/ingest/pmc SKILL.md frontmatter 에 `owner: index` 추가(현재 `INDEX_OWNED.txt` manifest 가 de-facto SoT, frontmatter 미기재). drift-guard 가 frontmatter 키로 동작 시 필요. blueprint mirror PR 동반. | 2026-06-03 | 소, P2 동반 | | **B-index-ic-screen-mode** 🆕 | [D-index-36](/ops/decisions) 후속 — `/ic` skill 에 lightweight **screen-mode** 통합. 현재 Screening 게이트는 수동(Claude best-effort: 외부 리서치 5종 + research-calibrated 재무모델 + engine IRR(E[CF]) + premortem 가정라벨). 이를 `/ic --screen`(또는 stage 자동감지)로 정규화: 19-agent full 대신 축약 파이프라인(market/competition/comps base-rate research + financial model + premortem-critic + screen-memo synthesist), **dataroom 불요**(teaser/1-pager 입력), 산출 = ic/memo screen + research + finance + index 적재(stage='Screening'). DD 전환 gate(조건) 명시. 첫 수동 사례 = 에듀온([D-index-36](/ops/decisions)). | 2026-05-30 | M7, 1.5d. ic skill 4-mode(INITIAL·REVISION·APPEND·FINALIZE)에 SCREEN 추가 | | **B-hive-compensation-adjustment-impl** ✅ | hive `compensation_events.adjustment` 정식 MCP 도구 구현 — D-hive-17 후행 leg 완성. **2026-06-01 완료 ([D-hive-29](/ops/decisions))**: `compensation_event_adjustment_create(employee_id, period_label, line_kind, delta_krw, reason, evidence_url?)` admin scope. 페이슬립 line append + 합계 보정 + `compensation_events.adjustment` audit row + `hive.payroll.event` (subtype=adjustment) 발행. `compute_period` UPSERT-with-gates 의 case C/D/E (발송됨/지급됨/역분개됨) skip 케이스의 정공법 정정 경로. `delta_krw` 부호 = `line_items.amount_krw` 부호 (공제 460원 증가 = -460). 발송된 페이슬립 정정 시 직원 메일 ≠ DB → 운영자 재발송 책임 (운영 절차). | 2026-05-30 → 2026-06-01 ✅ | [D-hive-17](/ops/decisions) + [D-hive-29](/ops/decisions) | | **B-frame-payroll-event-consumer-adjustment** 🆕 | frame consumer-worker 측 `hive.payroll.event` LISTEN + subtype='adjustment' dispatch → 인건비 정정 분개 자동. [D-frame-1](/ops/decisions) pending_payroll 패턴 정합 (이중 분개 위험 0). [D-hive-29](/ops/decisions) 동반 — hive 측 event 발행 ✅ but consumer 측 미구현. 본 leg 없으면 frame 분개 ≠ hive 페이슬립 차액 누적. | 2026-06-01 | M3, 1.5d. [D-hive-29](/ops/decisions) | | **B-hive-adjustment-auto-redispatch** 🆕 | `apply_adjustment` 직후 `send_payslip_email` 자동 호출 (`auto_redispatch=True` default) + 메일 본문 템플릿에 정정사유·차액·새 net 표시 (정정명세서 전용 템플릿 `payslip_adjusted.j2`). 운영자가 "정정 → 재발송" 두 step 잊지 않게. 직원 메일 (옛 net) ≠ DB (새 net) 정합 갭 해소. | 2026-06-01 | M3, 1d. [D-hive-29](/ops/decisions) | | **B-hive-adjustment-operator-alert** 🆕 | `apply_adjustment` 직후 Blueprint Teams DM 운영자 알림 (`payslip 정정 ps# 직원 원 — 재발송 자동 ✅ / 운영자 확인 필요 ⚠️`) — `auto_redispatch` 실패 / 차액 송금 필요 시 강조. | 2026-06-01 | M3, 0.5d. [D-hive-29](/ops/decisions) + B-hive-adjustment-auto-redispatch 후속 | | **B-hive-payroll-base-policy-decision** 🆕 | hive `compute_period` 의 4대보험 base **일할 케이스 정책 결정** — 현재 [D-hive-29 룰 K](/ops/decisions) 로 base = `reported_income_*_krw` (보수월액), 값은 R(과세표준)과 같게 set 정책. **정액 케이스**: 보수월액 = R → 결과 일치. **일할 케이스** (신규 입사·중도 퇴사): R(일할 적용된 변동값) vs 보수월액(고정값) → 결과 다를 수 있음. 예: 강태훈 axev 2/19 입사 → 2월 prorate. compute_payroll_v2 가 `taxable_total` 에는 prorate 적용 (= R 변동) 하는데 base = reported_income 사용 시 보수월액 고정값으로 NPS/NHIS 계산 → 일할 적용 안 됨. **요구**: 일할 케이스 시뮬 + 회계법인 cross-check (NPS/NHIS 공단 정합 = 일할 시 보수월액도 비례 조정? 또는 고정?) → 룰 K 보강 결정 (예: 일할 비율을 보수월액에도 곱 적용 vs 보수월액은 항상 고정). | 2026-06-05 | M3, owner: 강수훈 dev. 우선도 🟡 중간 (현재 정액 결과 정합이라 시급 X). [D-hive-29](/ops/decisions) 룰 K 후속 일할 정책 | | **B-index-ingest-pipeline-candidates** 🆕 | 2026-05-30 데이터룸 스캔 발견 — index 미적재 deal 중 **IC 완료 후 적재 대상**: **Prj_Render** (dataroom + ic 스캐폴드 有, IC 메모 미완 → 메모 완료 시 적재), **Prj_Whale=위시켓** (보유 포폴 — 세무조정·주식이관·주총·등기 admin docs 만, 원 IC 재무모델 확보 시 적재). 비대상: 델리후레쉬(RFP 제휴건), Pipeline 잔여(Curi AI·엘리시움 등 — 미screen). **적재 완료** (7-deal): Interstellar(10th, PASS)·에듀온(11th, Screening PASS [D-index-36](/ops/decisions))·EGA(12th, IC)·수성별(13th, Passed)·Nanora(14th, full IC, [D-index-37](/ops/decisions))·**디벨로퍼그룹(15th, buyout-class)·유비랩(16th, Screening)** ([D-index-38](/ops/decisions), 2026-05-30). 나머지 Pipeline 은 screen 또는 IC 시 D-index-36/21/24/26 프로토콜 적용. **Prj_Render = 리얼초이스/트루비아** = 2026-05-30 best-effort SCREEN 완료 → 적재 (23rd, [D-index-40](/ops/decisions), par-entry cheap-option). ⭐ **별도 Archive batch** (`1_Project (Archive)`): Medistaff·Open Research·JS E&L·Catalyst·데이톤·Apposter 6건 = [D-index-39](/ops/decisions) 적재 (17~22th, 전부 미투자/correctly-avoided). **⛔ 데이터룸 소진 (DB 23)** — 잔여 = Prj_Whale=위시켓(executed 포폴이나 원 IC/재무모델 부재, **자료 확보 시 적재**) · Purple AI/TR Corp/엘리시움(DHP 빈 폴더, 자료 입수 시 screen) · 이노씨앤에스/Curi AI/가람봇/딥트리/블루밍(thin lead). 비-딜: Prj_Artemis(AXE 자체 제품)·델리후레쉬(RFP). | 2026-05-30 | M7, deal당 ~1d (screen) / DD 시 full /ic | | **B-blueprint-broadcast-mail** 🆕 | Blueprint `/api/admin/broadcast-dm` (Teams DM) 와 동급의 **실 SMTP/Graph email broadcast REST 추가** — `/api/admin/broadcast-mail`. 본 use case = 운영자가 임직원 1:N 공지 시 Teams DM 외에 이메일도 발송 필요한 경우 (외부 회계법인 cc / 감사 audit trail 보존 / 비-Teams 사용자 대비). `sendEmail()` 함수 (`src/lib/graph.ts:412`) + Mail.Send permission 이미 보유 — REST wrapper 만 추가하면 됨. Auth 동일 `CRON_SECRET`. 운영 정합 = [/ops/runbook/operator-broadcast](/ops/runbook/operator-broadcast) 의 함정 표 그대로 적용. 사용 빈도 추정 = 분기 1-2회 (vault 공지 / 신규 customer launch / 시스템 변경). 본 작업 ROI = 작음 (Teams DM 으로 대부분 충분), 다만 audit trail / 외부 cc 가 본질 필요한 deal 결정 발생 시 즉시 우선순위 ↑. **2026-05-29 update**: MCP `send_mail` ([D-bp-mcp-mail-1](/ops/decisions)) 가 agent/connector 의 단발(1:N 임의 수신자 포함) 발송 + `MailSendLog` audit + 외부 cc 를 이미 커버 — 본 항목은 이제 **운영자용 REST bulk broadcast** (CRON_SECRET auth, 비-MCP 트리거) 한정으로 축소, ROI 더 작아짐. | 2026-05-26 | M5, 0.3d | | **B-axe-secret-prompt-store-cli** 🆕 | `axe secret prompt-store --collection X --uri ... --username ...` CLI subcommand 신설 — [vault-secret-capture](https://github.com/axelabs-ai/blueprint/blob/main/.claude/skills/vault-secret-capture/SKILL.md) skill 의 osascript hidden-dialog + vault item 저장 패턴을 한 줄 명령으로. 사용 빈도 (PAT 발급 / API key 등록 / OAuth client_secret 회전 등) 증가하면 진행. 본 작업 ROI = 중간 (skill 로도 충분, CLI 화 = 운영자 본인 typing 짧아짐). | 2026-05-26 | M3, 0.3d | | **B-dev-platform-dns-placeholder-cli** | `axe customers dns-placeholder ` — Cloudflare DNS API 로 `.axelabs.ai` 의 CNAME stub 추가 (tunnel target). vault token (`Cloudflare API - axelabs`) 가 zone DNS Edit 보유 — 즉시 가능 | 2026-05-23 | 1시간 추정 | | **B-dev-platform-customer-deploy** | `axe customer deploy ` — D-dev-platform-2 후속, customer macmini side. SSH (Tailscale/Cloudflare Tunnel) 로 customer 머신 진입 → Docker stack (frame/hive/blueprint blue/green + postgres + caddy) 배포 + cloudflared tunnel 등록 + customers.yaml 동기화. realchoice 첫 실 운영 | 2026-05-23 | b5 — 2-3시간 추정. M1-M2 | | **B-dev-platform-user-add-cli** | `axe user add --customer ` — Phase 1 강태훈 setup (macOS user + SSH key + Vaultwarden item + GitHub collaborator + Cloudflare Access policy) 의 자동화. 본 PR 의 강태훈/Soohun 수동 절차 1 명령으로 | 2026-05-23 | Phase 1 의 반복 가능 형태 | | **B-axe-pat-rotation-cron** | GitHub PAT (axe-labs-ai, 90일 TTL) + Cloudflare token + 기타 vault secret 의 만료 D-7 launchd alert (D-bp-alert-1 패턴) + `axe secret rotate ` end-to-end 회전 명령. D-dev-platform-4 의 운영 prereq | 2026-05-23 | 0.5d | | **B-dev-platform-host-rebuild-doc** | axe-macmini 전체 host 재구성 시 본 세션의 multi-user setup 재현 자동화 — sudoers + dev group + macOS users + sshd_config drop-in + Cloudflare tunnel + Vault. ad-hoc script 가 아니라 도구화 (e.g. `axe host bootstrap`) | 2026-05-23 | 1d | | **B-axe-host-inventory-cli** | `axe host inventory` — 본 머신 13 layer ([ops/host-setup](/ops/host-setup)) 의 현재 값 자동 수집 + docs 의 표 와 drift detect → markdown report. 신규 머신 진단 + 본 머신 정기 audit (launchd cron) | 2026-05-23 | 1d. D-host-setup-1 후속 | | **B-axe-host-bootstrap-cli** | `axe host bootstrap` — 신규 macmini 의 layer 1-13 자동 setup. inventory CLI 의 reverse — 본 머신 현 값을 reference 삼아 신규 머신 동일 상태로. layer 0/6/12 (Hardware/Entra/OneDrive) 는 manual prompt, 나머지 자동. Tailscale invite + Cloudflare tunnel UUID 발급 + sudoers + SSH + Docker stack + launchd + git config 모두 1 명령 | 2026-05-23 | 3-5d. realchoice 본격 onboarding 의 핵심 prereq | | **B-axe-pty-max-launchd** | `kern.tty.ptmx_max=511` 한도 → 2047 로 늘림 + launchd plist 로 boot 시 자동 적용 (sudoers `/usr/sbin/sysctl` 추가 필요). Claude Desktop PTY leak 같은 함정 영구 차단 | 2026-05-23 | 0.3d. ops/host-setup 의 알려진 함정 | | **B-claude-desktop-pty-leak-track** | Claude Desktop app (`/Applications/Claude.app/Contents/MacOS/Claude`) 의 PTY leak. **재발 확정 — 2026-05-26 D-bp-mcp-calendar-2 ship 중 `forkpty: Device not configured` (ENXIO) 로 새 터미널 차단** (43일 가동 누적, 511 한도 도달). `lsof /dev/ptmx` 결과 = Claude.app PID 가 fd 43,44,46,88,89,92,94,98,99,102+ 점유. userspace zsh 좀비 (82개 누적, Claude Code subprocess) 정리해도 해소 안 됨 = Claude.app 본체 leak 확정. macOS `kern.tty.ptmx_max` 가 511 초과 거부 → sysctl raise 무효 → Claude.app 종료/재시작이 유일한 fix. **Anthropic 측 issue 제기 (5/26 재발로 1주 모니터 기간 단축, B-claudeapp-fd-leak-anthropic-report 로 split)** | 2026-05-23, 2026-05-26 재발 | 모니터 → 리포트 | | **B-blueprint-user-add-entra-oid** | Blueprint `User` 테이블에 `entraOid` 컬럼 추가 (NextAuth Azure AD callback 에서 `account.providerAccountId` 또는 `profile.oid` 저장). 현재 `User.id` = Prisma cuid 라 Graph `/users/{key}` 호출 시 UPN/email 만 사용 가능. 미래 Graph endpoint 중 oid 만 받는 것 (event subscription, change notification 등) 대응. D-bp-mcp-calendar-2 의 send-as 함정 발견 직접 결과 ([/ops/known-gaps#blueprint-user.id-≠-microsoft-entra-oid](/ops/known-gaps)) | 2026-05-26 | 0.3d (schema migration + auth callback 갱신 + 백필 1회) | | **B-msal-cache-flush-on-consent** | Azure App admin consent 직후 MSAL `acquireTokenByClientCredential` 의 in-memory 토큰 캐시가 옛 (insufficient-scope) 토큰 계속 반환하는 함정. 현재 우회 = blueprint-app 컨테이너 수동 재시작 (전체 MSAL instance 폐기). 영구 fix 후보: (a) `getMsalApp()` 의 캐시 invalidation hash 에 "last admin consent timestamp" 같은 외부 signal 포함 — Azure CLI 의 admin-consent 명령 후 운영자가 `axe blueprint touch-msal-cache` 한 줄 실행, (b) `acquireTokenByClientCredential({ skipCache: true })` 를 매 호출 사용 (성능 영향 검토), (c) MSAL 의 `clearCache()` API 호출하는 admin endpoint 신설. (a) 가 가장 간단. ([/ops/known-gaps#msal-acquiretokenbyclientcredential-토큰-캐시](/ops/known-gaps)) | 2026-05-26 | 0.5d | | **B-frame-cross-check-workflow** | 외부 회계법인 cross-check workflow — frame 의 monthly trial_balance / income_statement export + 회계법인 회신 매칭 + adjustment 분개 자동 생성. hive payroll L2 패턴 mirror. Truvia 측 외부 회계법인 협의 결과 회신 시 본격 등재 (사내 협의 진행 중) | 2026-05-25 | M2, 1d (회신 후) | | **B-customer-deploy-generalization-phase3-3rd-customer** | B-customer-deploy-generalization Phase 3 잔여 — 3rd customer dry-run (realchoice 이후 다음 신규 customer 의 self-deploy 검증). Phase 1 + Phase 2 (realchoice D-day) ✅ 2026-05-25/26. 3rd customer 사례 발생 시 axe CLI flow 재현성 확인 + 추가 함정 발견 시 R6+ phase 등재 | 2026-05-25 | M2, 0.5d (3rd customer onboard 시) | | **B-cf-token-rotation-2026-05-25** | Cloudflare API token (`Cloudflare API - axelabs`) 의 실제 값이 본 세션 transcript (axe secret get 출력) 에서 평문 노출. 토큰 가치 = 도메인 zone DNS Edit 권한 = 운영자 측 인프라 통제. 회전 권장: Cloudflare dashboard → API token → revoke + 신규 발급 → `axe secret push CLOUDFLARE_API_TOKEN ...` | 2026-05-25 | M1, 0.3d | | **B-realchoice-d-plus-checklist** | realchoice 측 후속 회신 (RE^2 §7): vault 데이터 마이그레이션 plan (D+7), 마케팅 에이전트 10 → Blueprint Agent row 매핑표 (D+7), nemotron-personas 라이선스/origin (D+14), 2026 4Q 회계 병행 인력 분배 (D+30). 각 회신 시점에 docs / customers.yaml / Blueprint Agent row 반영 | 2026-05-23 | M1-M2, 각 0.5d (회신 후) | | **B-magnet-tenant-env-injection** | `axe ship magnet --customer ` 가 `customers.yaml.service_tenant_map.magnet` 에서 `MAGNET_TENANT_SLUG`/`MAGNET_TENANT_ID` 자동 주입. 현재는 service operator 가 .env.local 수동 편집. D-magnet-tenant-map-1 후속 | 2026-05-23 | M1, 0.5d | | **B-nemotron-catalog-promote** | Truvia 측 D+14 라이선스 회신 후, cross-customer OK 면 `/services/nemotron-personas` placeholder 본격 보강 + Blueprint MCP registry 등재. cross-customer X 면 `customers.realchoice.private_services[]` 신설 + 라이선스 박스 명시 | 2026-06-15 | M1, 0.5d | | **B-operator-bus-factor** | 운영자 SPOF (인력 1 명만 운영 가능) 해소. 2 인 이상 운영 가능 + cold SSD paper memo 분리 ownership + 분기 drill. 트루비아 측 보고서 (RE^2 §Q8) 가 drill 동참 의사 명시 — 분기 drill 일정 사전 통보 약속 | 2026-05-23 | M5, 2d | | **B-container-name-customer-prefix** | `frame/docker-compose.yml:197 (axe-frame-proxy)`, `hive/docker-compose.yml:124 (axe-hive-proxy)`, `vault/docker-compose.yml:29/66 (axe-vaultwarden, axe-vault-caddy)` 의 container_name 이 `axe-` prefix 박혀 있음 — customer-per-macmini 격리로 충돌 없으나 realchoice macmini 에 `axe-frame-proxy` 가 떠서 misleading. `${CUSTOMER_PREFIX}-` 변수화 또는 customer-agnostic 으로 정규화. 트루비아 측 첫 보고서 직접 지적 (2026-05-23). 운영 영향 0, docs/도식 명확성 中 | 2026-05-23 | M1, 0.5d | | **B-axe-labs-ai-2fa** | axe-labs-ai GitHub 계정 2FA 강제 활성 — Settings → Password and authentication → Two-factor authentication. PAT 분실/유출 시 추가 보호 layer. ai@ 자동화 영향 0 (PAT 는 2FA 우회) | 2026-05-23 | 0.1d. 운영자 5분 | | **B-hive-send-202605** | axev 2026-05 실 메일 발송 (`payroll_send_all_payslips dry_run=false`) — **2026-05-28 hive MCP 검증 결과 partial**: 강태훈 ✅ sent (send_log id=6, channel=graph_api, status=sent, 2026-05-25T15:11:51), 한진우 ❌ pending (RRN 등록 선행 [B-hive-hanjinwoo-rrn]). DKIM/DMARC live 검증된 상태. 한진우 RRN 입력 후 본인분 send 실행 시 본 항목 ✅ 가능 | 2026-05-22 | period_id=4 | | **B-hive-mark-paid-202605** | axev 2026-05 KB 일괄송금 + `payroll_mark_paid`. hive event 발행 → frame-worker pending_payroll INSERT 예약 | 2026-05-22 | D-frame-1 흐름 | | **B-frame-ingest-kb-202605** | KB 거래내역 download → frame.ingest_kb_* → `match_pending_sweep` 자동 매칭 분개 생성. matching principle 검증 cycle 1번째. **2026-05-28 frame MCP 검증**: axec raw_transaction 일부 존재하나 전부 `journaled=false` (+ 0원 카드 가승인/취소 noise) — 실 KB 거래내역 download + matching cycle 미수행 = pending | 2026-05-22 | D-frame-1 | | **B-hive-202604-recon** | axev 2026-04 cycle 산출 + mark_paid (강수훈 정산 24,660 환급 분개 발행) | 2026-05-22 | 정산-only payslip 분기 코드 ready | | **B-hive-hanjinwoo-rrn** | 한진우 주민번호 별도 보안 채널 수령 + `complete_onboarding(national_id)`. 4대보험 신고 전 필수 | 2026-05-22 | D-hive-18 | | **B-frame-axev-resolution-ktx** | 강태훈 임원 보수 axev 이사회 결의서 등록 (`frame.axev.internal_resolution`) + award.resolution_id backfill | 2026-05-22 | 감사 추적 | | **B-hive-simplified-tax-import** | 국세청 2026 간이세액표 정확 import. 현재 placeholder 근사식 + 강태훈/한진우 ground truth row 만 정확 | 2026-05-22 | D-hive-21. 다른 직원 임금 시점에 발견 risk | | **B-hive-reported-income-axev** | axev 보수월액 신고치 확정. 현재 4,186,222 (연금) / 4,022,567 (건강) 추정 → 실 4대보험 신고서 cross-check | 2026-05-22 | D-hive-21 | | **B-hive-golden-ci** | tests/payroll/golden_axec_2026_04.json 자동 CI 가동 (현재 수동 검증만). 산식 회귀 방지 | 2026-05-22 | D-hive-21 | | **B-hive-phase4-wage-tax** | 원천세 자동 신고 자료 + 4대보험 EDI + 퇴직금 + 연말정산 + 연장/야간/휴일 + 연차수당 | 2026-05-22 | Phase 4 scope | | **B-axelabs-ai-live** | `axelabs.ai` 도메인 라이브 배포 — Cloudflare Tunnel DNS (apex + www) + cloudflared ingress + `docker compose up -d --build`. `/Users/axe/axelabs/` 의 Next.js standalone 컨테이너 (host:3900). 회사 홈 + `/ui` 쇼케이스 동시 노출 | 2026-05-22 | docs.axelabs.ai 와 동일 패턴. `/Users/axe/axelabs/DEPLOY.md` 참고 | | **B-axe-ui-blueprint-migrate** | Blueprint 가 `@axe/ui` 채택 — `pnpm add git+ssh://...axelabs#v0.1.0` + `transpilePackages: ["@axe/ui"]` + globals.css 의 토큰 import 로 교체 + Fontshare `` 정리. 기존 globals.css 와 `@axe/ui` 토큰의 시각 차이 회귀 점검 | 2026-05-22 | B-axelabs-ai-live 선행 권장 (production tag 가 있어야 핀 가능). 첫 외부 소비자 검증 | | **B-axe-ui-v0.1-tag** | axelabs 레포에 `v0.1.0` git tag — 외부 소비자들이 핀할 stable 버전 시작점. tag 직전 build/typecheck 통과 + axelabs.ai 라이브 확인 | 2026-05-22 | B-axelabs-ai-live 후 | | **B-axe-ship-update-hook** ✅ | **DONE 2026-06-03** (axe-cli `aa43d4a`): `cmd_ship` post-deploy hook — updates.mdx Ship Log 자동 stub(멱등, backtick-hash) + `--no-update-hook`. 전체 guarded(ship 안 깸), 배포 성공 후만. **단 backlog 자동 promote 는 surfacing-only**(🔧 항목 제안 출력) — markdown 행 자동이동은 fragile 해 제외, true 자동화는 [B-axe-backlog-promote-cli](#-신규-분류-안-됨) 선행. [D-docs-updates-1](/ops/decisions) Phase 2 | 2026-05-22 | done: Ship Log 자동화 | | **B-axe-cli-git-track** ✅ | **DONE 2026-06-03**: `~/.axe/bin` 제자리 git-init(코드-only, 비밀은 ~/.axe 루트라 범위 밖) + 비공개 remote `github.com/axelabs-ai/axe-cli`(genesis `ad91ea0`, .bak·pycache gitignore). 이후 일반 git flow. (결정: 별도 repo 화 — `~/.axe` 통째 git-init 은 tokens.json·vault/.env 밀집으로 기각.) | 2026-05-23 | done: axe-cli repo | | **B-axe-backlog-promote-cli** 🆕 | `axe backlog promote ` — backlog.mdx 의 🔧/📋 항목을 ✅ Done 섹션으로 안전 이동(행 파싱 → 재배치 → 완료일·결정링크 삽입). 현재 [B-axe-ship-update-hook](#-신규-분류-안-됨) 의 promote 는 surfacing-only(후보 출력) — markdown 행 자동이동이 fragile 해서다. 이 명령이 생기면 ship hook 이 호출해 **true 자동 promote** 가능. + 본 세션이 ✅ 항목을 in-place 마킹만 하고 물리적 Done 이동은 deferred 한 것도 이 명령으로 일괄 처리 | 2026-06-03 | M3, 0.3d. B-axe-ship-update-hook 후속 | | **B-matrix-backlog-system** 🆕 ⭐ | matrix 를 backlog/roadmap/ship-log **구조화 SSOT** 로 ([D-matrix-3](/ops/decisions), ADR `matrix/docs/adr/backlog-roadmap-system.md`). markdown 의 멀티라이터 충돌·fragile 행이동·수동 카운트 해소. **Phase 1 구현완료(미배포)**: matrix-postgres 3테이블(backlog_item/roadmap_milestone/ship_event, slug PK) + MCP 8도구(`backlog_list/create/update/transition`·`roadmap_*`·`shiplog_*`) + import 파서(206 backlog+16 ship+7 roadmap dry-run 검증), `cargo check` ✓. **잔여**: Phase 1 배포(feat/wan-monitor 와 분리 브랜치 권장)+import apply, Phase 2 의례 cutover(전 서비스 CLAUDE.md→matrix MCP, ship-hook→`shiplog_append`, B-axe-backlog-promote-cli 흡수), Phase 3 보드뷰(axelabs app)+REST+host-side gen-docs | 2026-06-03 | M3 epic, Phase 1 done. [D-matrix-3](/ops/decisions) | | **B-axe-ui-consumer-mysrt** | mysrt 가 @axe/ui 토큰만 도입 (FastAPI+Jinja+vanilla JS, React 없음). Toss Pink 브랜드 유지 결정 → `[data-brand="mysrt"]` scope override 패턴. 토큰 이름만 통일 | 2026-05-23 | 브랜드 정책 결정 필요. 후순위 | | **B-axe-ui-consumer-distributa** | Distributa (현재 빈 Vercel scaffold) 가 product surface 정의 후 sync-axe-ui 도입. 신규 화면 짓기 시작할 때 적용 | 2026-05-23 | product 정의 선행 | | **B-docs-box-verify-cron** | `npm run verify:box` 의 cron 등록 (매 30분 또는 시간) — Cloudflare/외부 변화 catch. 운영자 launchd job. 실패 시 alert (e.g., known-gaps 자동 등재 또는 Slack/Teams webhook) | 2026-05-23 | Phase 20-21 의 보강. axe ship 통합으로 deploy 시점은 차단 — cron 은 *외부 요인* catch | | **B-axe-ui-radix-bump** | @axe/ui v0.3.2 의 미해결 QA P1 11 + P2 6 fix (a11y/types/skip-link/contrast 등) → v0.3.3 patch. Phase 16 audit 보고서 참고 | 2026-05-23 | non-blocking, 다음 cycle | | **B-docs-cf-purge-cli** | `axe cf purge ` CLI 신설 — Cloudflare API token 을 vault 에 등재 후 운영자가 dashboard 우회. 본 chapter 의 Cloudflare 옛 404 4시간 잔존 fix 시 운영자 dashboard 작업 강제됐던 점 해소 | 2026-05-22 | wrangler scope (`cache_purge`) 없음 확인 | | **B-axe-secret-send-dm** | `axe secret send --service --to --dm` flag 추가 — Bitwarden Send URL 발급 + Blueprint `/api/admin/broadcast-dm` 호출까지 1 명령에 묶음. 현재 운영자가 (1) `axe secret send ...` 6 회 + (2) `curl /api/admin/broadcast-dm` 3 회 + (3) 메시지 본문 bash 합성 필요. `--dm` 가 to-email 매핑 (`taehun → taehun.kang@axellc.com`) + 메시지 템플릿 (`--template connector-onboarding`) + CRON 자동 추출까지. SSOT: [/architecture/secrets § 자동 발사](/architecture/secrets#자동-발사--apiadminbroadcast-dm) | 2026-05-23 | 본 세션에서 발견 (axe-secret-send 라이브 첫 사용). 현재 docs 의 employee-onboarding §5.2 가 the bash boilerplate. `--template` 카탈로그도 같이 등재 | | **B-vault-org-create-automation** 🆕 | Vaultwarden organization 자체 생성 명령 부재 (client-side encryption 으로 web UI 만 가능). customer onboarding 시 운영자가 web UI 30초 수동. 영구 fix = (a) Vaultwarden API + admin token 으로 직접 호출 또는 (b) Playwright headless 자동화. Truvia 5/26 D-ops-32 = realchoice org `eac93965-...` 수동 생성 + 4 collection. 영향: 신규 customer 마다 30초 + 사람 손 의무 | 2026-05-26 | M2, 0.5d | | **B-vault-org-cli-automation** 🆕 | bw CLI 의 organization invite/confirm 명령 부재. Truvia 5/26 우회: Vaultwarden API + data.json 의 `user__token_accessToken` JWT 추출 (2025.7.0 schema). customer 측 onboarding 자동화에 필수. 영구 fix = (a) custom bw helper script (axe `axe vault org-invite ` 신설) 또는 (b) upstream bw CLI PR. Vaultwarden REST API 사용 (`POST /api/organizations/{id}/users/invite`) | 2026-05-26 | M2, 1d | | **B-port-conflict-preflight** 🆕 | onboard step 3 (SSH probe) 직후 step 10 (docker preflight) 이전에 **포트 충돌 사전 점검** 추가 — `ssh "docker ps --format '{{.Names}}\t{{.Ports}}'"` 매트릭스 추출 → AXE platform 표준 포트 (3700/3710-3712, 3800/3810-3812, 3100-3151) 와 cross-reference. 충돌 발견 시 차단 게이트 + 운영자 알림. realchoice 14 컨테이너 사례 (trap #6). 영구 fix = `_onboard_step_port_audit` 신설 + B-realchoice-port-collision-audit 통합 | 2026-05-26 | M1, 0.3d | | **B-frame-keychain-to-vault** 🆕 | frame docker-compose 의 `FRAME_STORAGE_HOST_DIR=/Users/axe/frame/.local` hardcoded default 영구 fix. R1 (HOME 변환) 부분 fix 적용 (wrapper 의 .env.local 에 `FRAME_STORAGE_HOST_DIR=${HOME}/frame/.local` 명시), 하지만 service repo 의 docker-compose.yml SOT 도 `${HOME}/frame/.local` default 로 변경 필요 (frame 측 PR). 동일 검토 = blueprint·hive·matrix compose 도 axe 절대경로 hardcode 잔존 여부 grep | 2026-05-26 | M1, 0.5d (frame PR) | | **B-bw-cache-stale-autoheal** 🆕 | bw CLI 의 local data.json cached `cryptoSymmetricKey` 가 server-side patch deploy (axe.2 / axe.3 등) 후 stale 상태로 저장되어 매 unlock `[Encrypt service] MAC comparison failed` → recurring (5/22 + 5/26 두 차례 manual recovery). `bw unlock` 은 cache 무효화 X. **영구 fix 3 갈래** (자동화 작업, docs portion 은 2026-05-28 ✅): (a) `axe vault` 에 `cmd_vault_reset` 신설 — data dir 옆으로 이동 + bw config server + bw login one-shot. 운영자 1 명령. (b) `_bw_get_password` 헬퍼가 MAC fail 패턴 감지 시 자동 reset + retry 1회. 자동 self-heal. (c) `axe ship vault` 의 post-deploy hook 이 patch shape 변경 감지 시 운영자에게 "운영자 자신 macmini 의 bw 재로그인 권장" osascript 알림. 함정: ai@'s personal vault 만 ~37 items 라 fresh login → sync 시 약 30초. 본 backlog 의 (a) 가 핵심 — (b), (c) 는 부가 가드. docs 측 표준 recovery 절차 = [/ops/runbook/vault-recovery#bw-cli-data-json-cache-stale-recovery](/ops/runbook/vault-recovery) (2026-05-28 ✅). | 2026-05-26 | M1, 0.5d (자동화 잔여) | | **B-trap-33-frame-hive-multi-issuer** 🆕 | trap #33 영구 fix — docs portion (c) ✅ 2026-05-28 ([/architecture/auth](/architecture/auth) line 147 정정 + 함정 표 row 추가). **잔여 code fix 2 갈래**: (a) **frame + hive code 에 v1 issuer 추가** — Blueprint MCP 의 `config.py:76,81` + `auth_oidc.py:153` `issuer=[v2, v1]` 패턴 mirror. `get_microsoft_issuer_v1()` helper 추가 (return `f"https://sts.windows.net//"`). 위치: `/Users/axe/frame/src/frame/mcp/http_server.py:141` + `/Users/axe/hive/src/hive/mcp/http_server.py:67`. (b) **bootstrap.sh 가 등록 시 v2 강제 + 검증** — Truvia D-day 시 3 app 모두 v1 였다 = PATCH 누락 또는 실패. bootstrap.sh 의 PATCH step 정합성 검증 + 등록 후 manifest version 검증 step 추가. | 2026-05-25 | M1, 0.7d (frame PR + hive PR + bootstrap.sh) | | **B-customer-sovereignty-architecture** 🆕 | **Q3 milestone**. Truvia 측 architectural concern (5/25): operator vault SoT (D-ops-17) → customer-측 vault/secret SoT 으로 재 architecting. 본질 = operator 가 모든 customer secret 의 SoT 라고 주장 = customer data sovereignty 침해. **올바른 architecture**: Code SoT=Github / Image SoT=registry / Service secret + Business data + Service config SoT = customer 본인 macmini. **operator 책임 2개만** = (a) software supply (code/image push to registry) (b) 외부 노출 (DNS/tunnel ingress catch-all). axe deploy 의 vault fetch + customers.yaml 의 secret manifest 모두 제거. customer 자율 secret (openssl rand + .env + customer 자체 vault). 5/25 D-day 의 self-deploy 패턴 (Truvia 가 본인 .env + compose 자체) 가 본 architectural change 의 첫 실제 적용. customers.yaml = customer 식별 (legal_name, public_domain, tailscale_host) + onboard step (cloudflared tunnel/DNS) 만 유지. | 2026-05-25 | M2/M3 Q3, 5d | | **B-customer-deploy-generalization-r6-volume-precreate** 🆕 | trap #19 + #23 영구 fix. docker-compose 의 external volume — (a) blueprint 6개 (app-data/workspace/claude-data/postgres-data + mcp-proxy data/config) 신규 customer 측 사전 부재 → compose up fail (trap #19), (b) hive 2개 (`hive-proxy_axe-hive-proxy-{data,config}`) 의 historical compose project prefix 정합성 문제 (trap #23) — 둘 다 본 R6 phase 에서 함께 해소. 영구 fix 2 옵션: (a) compose 의 `external:true` 제거 → compose 가 자체 volume 자동 생성 (권장 — compose self-contained, customer self-deploy 친화). (b) axe deploy pre-step 으로 `docker volume create` 자동 호출. R6 (customer-deploy-generalization 차기 phase) 으로 묶음. | 2026-05-25 | M1, 0.5d | | **B-docs-ssot-extension-2026-05-26-newpages** 🆕 | B-docs-ssot-extension-2026-05-26 의 잔여 — 신규 페이지 2 (A1/A2) 만. A1 `/partner/macmini-existing` 신설 (기존 운영 macmini → customer 전환), A2 `/services/vaultwarden` 또는 `/architecture/vault-integration` 신설 (Timshel image + compose/Caddyfile/env 패턴 + axe.2 patch 정체 + [D-ops-37](/ops/decisions) fork build). 정정 7건 (B1-B7) 은 2026-05-28 본 세션 ✅ 완료. **A1/A2 는 5/27 D-day 후 Truvia 측 첨언 사전 받아 시작 권장** | 2026-05-26 | M2, 0.5d | | **B-vault-collection-migration-v1** 🆕 | [D-ops-32](/ops/decisions) 의 6 collection 신설 완료 (2026-05-26) 후속 — item migration + 옛 collection cleanup. **3 단계**: (1) **운영자 personal vault → Platform — Service Secrets 이전** = `axe secret list` 의 ~37 service secrets (frame/hive/blueprint/matrix `/axe/*` + Cloudflare API + shared/* + infra/realchoice/*) 가 현재 ai@ personal vault only ⇒ bus factor 위험. organization Platform — Service Secrets 로 옮기면 soohun 도 access. `bw share` (deprecated → `bw move`) 또는 web UI. (2) **Default 의 3 items 분류**: `github PAT: ai@axellc.com` → Platform — Infrastructure (또는 rotation 후 폐기 — [B-cf-token-rotation-2026-05-25] 패턴), `axe-macmini local: {soohun,taehun}.kang` 은 각자 personal vault 가 본질 — organization 에서 제거 권장. (3) **옛 4 collection 의 item 이동 + collection 삭제**: frame-jwt-axec 의 `taehun-kang-axec-2026-05-17` + frame-jwt-operators 의 `ai-operator-2026-05-16` 검토 (D-ops-14 frame JWT 잔재) — Platform — Service Secrets 으로 옮기거나 unused 면 삭제. 빈 collection (frame-jwt-axev) 즉시 삭제 가능. Default + frame-jwt-* item-empty 화 후 collection 삭제. **위험**: axe CLI 가 name 기반이라 이동 자체는 안전, 다만 (a) item 이름 변경 금지 (`axe secret get ` 깨짐), (b) item 삭제는 영구 — 검토 후 진행. **`bw move `** 가 personal→organization item 이전 명령. organization 내 collection 변경은 item 의 `collectionIds` 갱신 (bw edit) 으로. | 2026-05-26 | M1, 1d (운영자 검토 시간 큼) | | **B-blueprint-settings-graph-reconnect-button** 🆕 | Blueprint `/settings` (또는 `/axe/settings` Integrations) 의 GraphToken status panel + **Reconnect 버튼** 부재 — 운영자가 token expiry / refresh 실패 진단·회복 self-service 불가. 본 세션 (2026-05-26) 함정: ai@ token 이 1.5h 전 expired + AADSTS65001 (Calendars.ReadWrite scope admin-consent 누락) 으로 refresh 침묵 실패. UI 는 "Connected as ai@axellc.com" 만 표시 — token 실제 상태 0. 운영자가 직접 `/api/graph/auth` URL 알아 방문 + admin consent 별도 trigger 모두 운영자 black-box 지식 의존. **요구 UI** (3 요소): (1) **현 상태 panel** — Connected/Disconnected/Expired/RefreshFailed status badge + `expiresAt` 카운트다운 + 마지막 refresh 시각 + 현재 grant 된 scope 목록. (2) **Reconnect 버튼** — 클릭 시 `/api/graph/auth` 로 redirect (`prompt=consent` 강제 query 추가). 옛 token row 는 fallback 으로 보존. (3) **Admin consent 검증 + 안내** — Microsoft 화면에서 "Need admin approval" 차단 시 운영자 본인 (Global Admin) 으로 sign-in 안내 또는 별 admin consent URL 직접 제공 (`https://login.microsoftonline.com/{tenant}/adminconsent?client_id={app}`). 본 세션에서 발견 (강수훈 직접 보고: "settings 에서 기본적으로 reconnect 버튼이 필요해보입니다. 흐름이 이상하지 않습니까"). | 2026-05-26 | M1, 0.5d | | **B-blueprint-scope-change-admin-consent-runbook** 🆕 | Blueprint app 의 OAuth scope 추가 ship 시 함정 — docs portion ✅ 완료 (2026-05-28, [/architecture/auth#oauth-scope-추가-시-운영자-절차](/architecture/auth) 함정 표 row + 운영자 절차 섹션). **잔여 자동화 2 prong**: (1) **`axe ship blueprint` post-deploy hook** — `SCOPES` array 변경 감지 시 `az ad app permission admin-consent --id ` 자동 호출 (또는 prompt). (3) **Blueprint 자동 token health check** — daily cron 또는 startup probe 가 모든 GraphToken refresh 시도 + AADSTS65001 발견 시 운영자 알림 (OperatorAlert + Teams DM). | 2026-05-26 | M1, 0.5d (자동화 잔여) | ## ⏸️ Blocked (외부 차단) | ID | 항목 | 차단 사유 | 차단 해소 조건 | |---|---|---|---| | _(현재 없음)_ | | | | ## ✅ Done (최근 7일, archive 대기) | ID | 항목 | 완료일 | 결정/문서 링크 | |---|---|---|---| | **B-vault-axe.2-sso-mp-incomplete** ✅ | **axe.2 SSO→MP unlock 부분 fix 완전 종결 — 4명 KDF rotation 전부 완료** ([D-ops-40](/ops/decisions) Progress xiii). 옛 user 의 `user.akey` 가 PBKDF2 600k schema 로 wrap 된 게 SSO→MP unlock 실패 원인. **본질 발견 = KDF type dropdown 변경 불필요** — Master password change + ☑ "Also rotate my account's encryption key" 체크박스 만으로 akey 재wrap 충분 (ai@ 가 Phase A 로 검증, 나머지 3명 동일 패턴). 4명 (ai 5/27 → soohun → taehun 5/29 → **jinwoo 6/4**) 전부 SSO→MP unlock 정상 확인. DB: ai 는 Argon2id (Phase B 까지), soohun/taehun/jinwoo 는 PBKDF2 유지 (akey 재wrap 만 — kdf_type 안 바뀌어도 fix 동작). jinwoo last_login 5/23→6/4 점프 = 완료 신호 + 운영자 구두 확인. realchoice 측 영향 없음 (axe.2 후 신규 가입 = 새 schema). 운영자 안내 = Teams broadcast 2회 ([operator-broadcast](/ops/runbook/operator-broadcast)) + [/onboard/vault-setup](/onboard/vault-setup) playbook. **D-ops-40 전체 작업 종결** (build pipeline + axe.3 patches + deploy + 검증 3건 + 정책 layer + 4명 rotation). | 2026-06-04 | [D-ops-40](/ops/decisions) Progress xiii | | **B-axe-cli-P0** ✅ | AXE CLI P0 골격 GREEN — stdlib 단일파일 `~/axe-cli/axe` (`login`/`logout`/`whoami` + macOS Keychain + `AXE_TOKEN` env fallback + MCP streamable-HTTP 클라이언트 + `--local` Host-header 자동첨부). frame HS256 부트스트랩 토큰(`frame mcp-token`)으로 `axe frame tools` → **54 tools GREEN 검증**. "커넥터 0 · 토큰 1회" 증명. repo git-tracked ([B-axe-cli-git-track](/ops/backlog)). 인터랙티브 인증 후속 = [B-axe-idp-1](/ops/backlog) (Blueprint OIDC loopback PKCE). | 2026-06-03 | [D-axe-cli-1](/ops/decisions) | | **B-index-epic1-judgment-layer** ✅ ⭐ | **artifact-first judgment layer (Epic 1) — 증거층↔판단층 결합**. 휴면 artifact store(39 fact)→relational 23 deal 결합: 모든 투자판단 fact 를 일급 typed+cited+audited artifact 로 승격 (kind assumption·calibration·comp·base_rate, **migration 0** generic store 재사용, 링크=일급 citation index_field+artifact_ref relation, side-bridge table 아님). **23/23 deal → 357 fact**, 0 orphan judgment value, additive-only 3중 증명(event propose/confirm·relational updated_at 미전진·validate-seeds Δ0), 48/48 test, sqlx clean. 2-wave supervised — adversarial gate 가 wave1 61% 미완 적발 → 직접 +4 → wave2 5딜 author + 6 rival module→1 `backfill.rs` 통합. index `1c707e1`. | 2026-06-03 | [D-index-45](/ops/decisions) | | **B-index M7 build cohort** (skeleton·azure-app·docker-compose·shared/artifact/financial migration·dsl-parser·topo-evaluator·mcp-checklist·schemas-endpoint·phase0-mcp-tools·3deal-seed·3deal-irr-validation·driver-period-refactor — 14) | index 서비스 live: **23 deals**·26 fund_investment·16 typed schema·artifact store(RLS)·MCP tools·DSL+topo evaluator. (phase0 4-tool 은 `propose_deal_closure` 로 통합 [D-index-13]; shared schema 는 public = Phase-0 설계.) validate-seeds A1 PASS Δ0. | 2026-05-31 | [D-index-2](/ops/decisions)~18 (triage 검증) | | **B-pmc-postmortem-2track** | pmc skill 2-track 분리 구현 (ic: postmortem_stub/reminder · pmc: fill_interactive/render + cadence ref). ⚠ launchd reminder install 은 operator 잔여. | 2026-05-31 | [D-index-12](/ops/decisions) | | **B-index-sqlx-cache-guard** | `.githooks/pre-commit` + `scripts/check-sqlx-cache.sh` (`cargo sqlx prepare --check`), `core.hooksPath=.githooks` 활성화 (2026-05-31). | 2026-05-31 | [D-index-32](/ops/decisions)~33 | | **B-cortex-visibility-phase1-sync-filter** ✅ | [D-cortex-9](/ops/decisions) Phase 1 — Google contactGroup(라벨) whitelist/blacklist sync 유입 필터 (`Personal`/`HPE` 라벨 연락처 유입 차단). `passes_label_filter` + `list_contact_group_names`(id→name 맵 1회 로드) + 맵 로드 실패 시 owner cycle fail-safe skip. 민감정보 미유입의 가장 강한 보장 | 2026-05-31 | env `CORTEX_SYNC_GROUP_BLACKLIST`(기본 `Personal,HPE`)/`CORTEX_SYNC_GROUP_WHITELIST` + `sync_google_now` 의 `skipped_by_label` stat. 코드 = cortex working tree (미commit), docs 반영 ([services/cortex](/services/cortex#visibility--private-partition-d-cortex-9)). `axe ship cortex` 시 commit/deploy | | **B-frame-hive-backup-cron** ✅ | axe-backup 스크립트에 hive-postgres pg_dumpall 블록 실 추가 (D-hive-backup 결정 후 미실행) (stale-done: 감사+반증검증 통과) | 2026-05-30 | wf 감사 wf_f1573f47 (misc (B-) | | **B-hive-template-axev** ✅ | axev payslip_email template clone + customize + activate (D-hive-25 customize 강제) (stale-done: 감사+반증검증 통과) | 2026-05-30 | wf 감사 wf_f1573f47 (hive) | | **B-bp-para-provenance-fields** ✅ | Blueprint Workspace에 sourceWorkspaceId/sourceArtifactPath/copiedAt 3필드 — PARA dispatch copy-with-provenance (stale-done: 감사+반증검증 통과) | 2026-05-30 | wf 감사 wf_f1573f47 (blueprin) | | **B-axe-ui-consumer-cortex** ✅ | Cortex 가 @axe/ui 도입 — design token 채택 (Tailwind v3→v4 마이그 + sync-axe-ui + ★3→★4) (stale-done: 감사+반증검증 통과) | 2026-05-30 | wf 감사 wf_f1573f47 (cortex) | | **B-frame-fund-ksme-seed** ✅ | `shared.account_template` 에 `fund_ksme` standard 48행 이미 seed | 2026-05-30 | stale-done 감사 (DB 확인) | | **B-frame-mcp-register-entity-tool** ✅ | MCP `register_entity`+`list_sub_entities` 이미 `@mcp.tool()` (server.py:1470,1505) | 2026-05-30 | stale-done 감사 (코드) | | **B-frame-fund-commitment-ledger** ✅ | `commitment_ledger`+`lp_master` 전 entity 스키마에 이미 존재 (schema 신설; population 별도) | 2026-05-30 | stale-done 감사 (DB) | | **B-frame-mcp-http-wrap** ✅ | `http_server.py` 1012줄 HTTP MCP 가동 중 (frame DECISIONS.md:170 'M1 only stdio' stale 정정 권장) | 2026-05-30 | stale-done 감사 (코드) | | **B-bp-onboard-claude-code** ✅ | `/onboard/claude-code` 페이지 5 컴포넌트 이미 구현 (Phase B) | 2026-05-30 | stale-done 감사 (코드) | | **B-axe-secret-totp-cli** ✅ | `axe secret get --field totp` 지원됨 (CLI --field choices 'totp'; 전용 subcommand 만 미신설) | 2026-05-30 | stale-done 감사 (CLI) | | **B-frame-entity-legal-name-i18n** ✅ | frame DB shared.entity legal_name 정정 — **전제 정정**: 사업자등록증 기준 법인명이 영문 (`axec`="AXE Corporation", `axev`="AXE Ventures") 이라 기존 "영문→한글" 전제 + customers.yaml SoT 의 한글값("액스코퍼레이션 주식회사" 등)이 오기였음. frame-postgres 2행 UPDATE (+ per-entity audit_log) + customers.yaml(entity_meta·axe.legal_name) 정정. **잔여**: hive shared.entity 가 동일 한글 stale → 운영자 확인 후 동일 정정 예정 (payroll 시스템) | 2026-05-30 | frame-postgres + customers.yaml | | **B-index-entry-date-backfill** ✅ | 9 deal 전부 entry_date 기입 (executed=paid_date / planned·passed=가안), `entry_basis=assumed` 0건 ([D-index-28](/ops/decisions) 후속) | 2026-05-29 | index commit `f650bec`+`e5ac7a6` | | **B-index-deprecate-sump-irr** ✅ | Σp·IRR 폐기 → IRR(E[CF]) 단일 canonical (사용자 승인). `weighted_irr_*` 제거 + `#[deprecated]` + 9 baseline·html·xlsx 전환 | 2026-05-29 | [D-index-32](/ops/decisions), index commit `3b2f543` | | **B-index-docs-irr-ecf-restate** ✅ | docs IRR 을 canonical E[CF] 기준 재기재 — 8-deal 랭킹 확정 (tail flip Sendy 11.7 > Novachips 9.2) + per-deal 구→신 표 + seed 주석 | 2026-05-29 | [D-index-34](/ops/decisions), index commit `537c04e` | | **B-index-instrument-adjusted-irr** ✅ | won't-do — instrument 효과가 exit_matrix leaf recovery 에 이미 반영 (flat discount table 은 이중계상). discount table 미구현 + Novachips `irr_instrument_adjusted` 폐기 + seed.rs 가드 | 2026-05-29 | [D-index-33](/ops/decisions), index commit `74e1eb4` | | **B-blueprint-mcp-send-mail** ✅ | Blueprint MCP 범용 이메일 발송 tool `send_mail` ([D-bp-mcp-mail-1](/ops/decisions)). `create_event` 형제 — 신규 route `POST /api/internal/mail` (caller delegated `Mail.Send` self 발송 / admin app-only send-as) + Python tool `mail.py` + append-only `MailSendLog` audit (migration `20260529120000_add_mail_send_log`). params: to/subject/body(html\|text)/cc/bcc/attachments(base64)/save_to_sent_items/as_user_email/dry_run. 에러: `send_as_forbidden`/`recipient_invalid`/`mail_send_not_consented`/`graph_error`. typecheck 0 errors. **⚠️ Azure 선행조건 (운영자 1회)**: send-as 경로는 Next.js app (`2b222356-...`) 에 Application `Mail.Send` + admin consent 필요 — self 발송은 delegated `Mail.Send` 이미 보유라 불필요. 절차 = [/architecture/auth § app-only Application permission](/architecture/auth). docs (blueprint/auth/mcp-server-checklist/decisions) 동시 갱신. | 2026-05-29 | branch `feat/bp-mcp-send-mail`, [services/blueprint](/services/blueprint) | | **B-index-infinity-3step** ✅ | Prj_Infinity(INEX) 축적 — 3-step surface→재수행→비교 ([D-index-23](/ops/decisions)). 사용자 "기존 데이터 적재 테스트 → 신규 IC 재수행 → 두개 비교로 전 건들과 같게". **Step1**: v8 FINAL(irr_locked) surface 적재 → index 13.8%/1.70x 정확 재현 (ev_bridge·net debt 0 보통주·exit-diluted 0.0724% — surface 모범). **Step2 재수행**: 외부 research — 실명계좌 base-rate near-zero(2021 특금법 후 원화마켓 신규 0건, P<10%) / 코빗 ceiling 1,000-1,400억 / Gopax·Binance license-shell M&A 선례 / 보통주 distress recovery 0~20%. 모달 exit 를 won-market rerating 아닌 SI shell M&A breakeven 으로 reframe + 보통주 wipeout leg(18% @ 0.12x). **Step3 비교**: calibrated −8.3%/0.82x. **~22pp gap + sign-flip**. 핵심 = 성숙·정직한 v8(hurdle 미달 공개)도 핵심 base-rate 를 mgmt optimism 에 anchor + 보통주 wipeout 누락 → 역전. proceeds 메커니즘 불변(analytical gap only). **canonical 결정** ([D-index-24](/ops/decisions)): executed/Closed deal 이므로 IC-approved 13.8% 가 canonical (decision-time 기록 보존), −8.3% 는 `infinity_recalibrated.yaml` retrospective overlay. 8-deal canonical: Infinity 13.8 (Hancom~Novachips 사이). validate-model 0 errors. IC memo v9 = OneDrive. | 2026-05-29 | index commit `c0dcd9a` + canonical swap | | **B-index-proceeds-bridge-hardening** ✅ | proceeds 산정 강화 — EV→EqV(net debt) bridge + exit-dilution 명시·강제 ([D-index-22](/ops/decisions)). 사용자 audit "모두 EV=EqV+Net Debt + 투자 후 dilution 고려됐나 체크" 의 답. **발견**: 7-deal proceeds 비일관 — net-debt bridge 는 Canopy 만 명시(EV 797−netdebt 300=EqV 498), Sendy(v2)·Iippo 는 stake×EV 직접, pre-IPO 3+Sentry 는 equity multiple(N/A); dilution 은 Iippo·Sentry entry F/D flat. **결정** = [D-index-20](/ops/decisions) IRR determinism 을 proceeds 로 확장: `proceeds_basis` enum (ev_bridge|equity_value|mom|legacy_ev) + ev_bridge non-writedown leaf 는 stake×(EV−net debt) 가 axe_proceeds 와 ±2% 일치해야 — seed load **hard-reject** + validate Check 7 Error 2중 강제 (negative test 거부 확인: 10%×2,225억 ≠ 날조 300억). migration 0005. **Sendy v3**: ev_bridge, net debt 0 (asset-light + 자금니즈 equity 충족 + 차입 증거 0 → 보수적, **날조 안 함**) → proceeds·IRR 불변 6.47% 이나 EV≠EqV 누락 class 영구 차단. 7-deal: Sendy ev_bridge PASS / Sentry equity_value / 3 pre-IPO mom / Iippo·Canopy legacy_ev WARN — 0 errors. 잔여 = [B-index-proceeds-bridge-retrofit](/ops/backlog). | 2026-05-29 | index commit `ca714b7` | | **B-index-sendy-current-practice-rewrite** ✅ | 센디(Sendy) IC v2 재작성 — 현 practice 재calibration ([D-index-21](/ops/decisions)). 사용자 redirect "그냥 적재 말고 ic memo 를 현 practice 수준으로 새로 써라 (당시 방법론 열위)". [D-index-19](/ops/decisions) (surface→calibrated) 의 종착 = **memo 방법론 자체 재작성**. **3-tier IRR**: v1 md 31%/22.8%(fabricated) → v1 Excel 92.4%(math-correct, 낙관 input) → **v2 현 practice 6.5% ⭐ canonical**. **v1 5결함 교정**: IRR override(D-index-20 차단) / wipeout leg 부재(Downside +31.6% gain → 손실 시나리오 없음) → genuine 18% wipeout(부릉 1/6 recovery) / sendyX 66x 미검증 → base-rate haircut(-93%) / Exit 5x flat → segment-weighted(freight 1~1.5x + SaaS 4~5x, WiseTech-E2open 3.5x) / instrument 분석 부재. 외부 anchor: Convoy $3.8B→0 + 부릉 1/6 distress → wipeout 15~25%, 한국 Series C→IPO 예외(TMAP 교훈). 결과 4-scenario E[IRR] 6.5% / E[MoM] 1.51x / Sharpe-like -0.06. 7-deal 랭킹 Sendy 6.5 최하위로 정정. 산출물: OneDrive `ic/memo/v2_axe_current_practice_260529.md` + seed v2(version:2). validate-model 7/7 PASS. | 2026-05-29 | index commit `b77a236` | | **B-index-sendy-passed-deal** ✅ | 센디(Sendy) 축적 — IRR override 오류 flagship + 첫 declined deal ([D-index-20](/ops/decisions)). 출처 `1_Project/Pipeline/Sendy/`. **index 존재 이유 결정적 증거**: exit_matrix/v1.yaml 의 `corrected: 35%` manual override (계산값 95.7% 를 근거없이 변경) — index 는 IRR 을 proceeds 에서 deterministic 계산 → override 물리적 불가, EV-derived seed → 자동 교정 (Base 95.7/Downside 31.6/Upside 146.6/weighted 92.43, Excel 일치). **deal.stage 'Passed' 추가** (migration 0004) — declined deal first-class tracking. 7-deal portfolio Closed 3 / IC 3 / Passed 1. PASS 사유 구조+타이밍 (quality 아님). honesty: screening-grade model (sendyX 66x 미검증). validate-model 7/7 PASS. | 2026-05-29 | index commit `9263179` | | **B-index-3-preipo-deals** ✅ | 3 pre-IPO deal (스타넥스/한컴인스페이스/노바칩스) calibrated 검토 본 축적 ([D-index-19](/ops/decisions)). 출처 `_comparative_20260528_preIPO/` 13-agent 비교 IC 메모. surface IRR(19.6/58.5/-1.5) → calibrated(36.4/22.4/7.45) 재calibration 본을 index 에 typed fact 로 저장 (xlsx surface cell 아님). pre-IPO scenario-tree 모델 (scenario=단일 outcome), `parse_timing_years` 소수 연도 확장 + timing CHECK 완화 migration. 6-deal portfolio 랭킹 검증 + validate-model 6/6 PASS. **신규 backlog 발견**: [B-index-instrument-adjusted-irr](/ops/backlog) (보통주 vs RCPS 8-15pp discount systematic 적용). | 2026-05-29 | index commit `e130a69` | | **B-cortex-production-live** ✅ | Cortex (Rust + axum + sqlx, 첫 비-Python MCP 서비스) 를 claude.ai connector 까지 production live. 폐기 (file-based recall → `.legacy.20260526/`) → Rust 재출발 (19 task) → 5 secret vault push → Entra app (az cli) + Google OAuth Web client (Cloud Console, 신규 프로젝트 cortex-497605) → Cloudflare ingress (Dashboard remote-managed, CF API PUT) → docker compose (postgres+blue+green+proxy) + 2 migration → claude.ai connector (client_id+secret) → Microsoft OAuth → **12 tools 노출 + whoami + connect_google 검증**. 시행착오 7 함정 ([known-gaps Cortex](/ops/known-gaps)) 전부 해소: DB password URL 파싱 (PgConnectOptions) / env_file 따옴표 / RFC 9728 resource-level path / RFC 8414 path-insertion (→ Microsoft-direct) / claude.ai client credential / ASCII-only property key (`메모`→`memo`) / Cloudflare remote-managed tunnel. **B-cortex-mcp-crash-loop (2026-05-27 등재) 해소** — 원인은 env_file 따옴표 + DB password URL + stale Docker image 복합. | 2026-05-28 | [D-cortex-1..8](/ops/decisions), [services/cortex](/services/cortex), cortex commits `c65b934..dfa9330` | | **B-cortex-dockerfile-healthcheck** ✅ | blue/green 컨테이너 영구 `unhealthy` 해소. 원인: healthcheck CMD 가 `python -c urllib...` 인데 Rust/Debian-slim 이미지엔 python·curl·wget 전무 (`/usr/local/bin/cortex` 바이너리만). 트래픽 무해 (Caddy alias routing 이 health-gate 안 함) 했으나 blue/green swap 의 healthy gate 잠복 위험. **fix = binary self-probe**: `cortex healthcheck` 서브커맨드 (GET `127.0.0.1:$CORTEX_MCP_PORT/healthz`, 2xx exit 0) + compose test `["CMD","/usr/local/bin/cortex","healthcheck"]` (green 은 `<<: *cortex-mcp-blue` 머지 상속). Settings::load 전 short-circuit → DB 자격증명 독립 (false unhealthy 방지). **index 의 python→curl 과 다른 선택** — 패키지 추가 없이 기존 바이너리 재사용 (minimal-base 정합). 검증: rebuild 후 blue+green `(healthy)` + 외부 200/401 유지. index 도 동일 패턴 정렬 권장 (curl 의존 제거, 우선순위 낮음). | 2026-05-28 | cortex commit `b8d3aa0` | | **B-cortex-private-partition-phase0** ✅ | [D-cortex-9](/ops/decisions) Phase 0 — artifact `visibility` tier (shared\|private) 도입. soohun.kang 2026-05-28 "Private Partition" 요청. migration 0003 (`artifact.visibility` DEFAULT shared + CHECK + index, 기존 3,250행 shared) + register_person `visibility`(private→Google push skip)·classify_person·update_person(re-tag) 에 `visibility`+`skip_audit_payload` + list_persons `visibility`/`exclude_classification_key`/`_value` 필터 (QueryBuilder) + get_person visibility 노출. 신규 schema key 전부 ASCII. 검증: build + migration + blue/green healthy + 필터 5-assertion rolled-back SQL 테스트 (zero residue) + 외부 200/401. **잔여(auth)**: claude.ai tools/list 12 + register_person(private) 실 round-trip 스모크. | 2026-05-28 | [D-cortex-9](/ops/decisions), [services/cortex](/services/cortex#visibility--private-partition-d-cortex-9), cortex commit `f8cad8c` | | **B-blueprint-agent-sdk-integration** ✅ | [D-index-17](/ops/decisions) Phase 2 — Blueprint Agent SDK 가 index MCP 의 skill 을 fetch 해 local cache sync. `scripts/sync-index-skills.sh` (index-skill-sync.py wrapper) + `launchd/ai.axe.index-skill-sync.plist` (10분 주기, RunAtLoad=true). drift 감지 시 `/tmp/blueprint-skills-drift.log` 에 기록. cache target = `.claude/skills-cache/{ic,ingest,pmc}/`. **검증**: 1차 sync 3 skill / 61 file added (ic + ingest + 신규 pmc 자동 발견), 2차 sync 61 unchanged (cache hit). 운영자가 `launchctl load` 시 자동 동작. D-index-17 overlap 정책: 현재 SoT = `.claude/skills/{ic,ingest}`, skills-cache = 검증 사본 (drift 감지). sunset 후 cache 가 SoT. | 2026-05-28 | blueprint commit `579a0fa1` | | **B-pmc-skill-skeleton** ✅ | [D-index-12](/ops/decisions) — Post-Money Care skill 신규 scaffold (Background Agent worktree 에서 진행). 30 files / 1,906 LoC: SKILL.md (213 line) + 8 agents (data-fetcher / kpi-extractor / risk-alerter / valuation-updater / exit-signal-analyzer / board-pack-drafter / lp-comm-drafter / index-payload-composer, avg ~93 line) + 5 references (kpi-catalog [portfolio-management copy] / ir-lifecycle [investor-relations copy] / postmortem-cadence symlink to ic SoT / risk-kind-frozen [5 enum 박제] / trigger-tiers [SCHEDULED/EVENT/MANUAL]) + 4 Python stubs (render_postmortem / postmortem_fill_interactive / quarterly_close / event_trigger) + 11 sub-dir `.gitkeep` (runtime output 구조 박제). Phase 0 = scaffold only, Phase 1 = 첫 quarterly cycle 실행. **검증**: index MCP list_skills 가 자동 발견 (ic + ingest + **pmc**), Blueprint sync hook 도 자동 fetch. | 2026-05-28 | index commit `2a74ef0` | | **B-index-3fs-bs-cf-completion** ✅ | [D-index-18](/ops/decisions) Gap 2 마무리 — Phase 0 working capital + simplified debt/ppe model 로 BS 12 line + CF 7 line 모두 derive. compute_3fs 가 IS-only → full 3FS. **BS**: ar=rev×dso_days/365, inv=cogs×inv_days/365, ap=cogs×dpo_days/365, ppe_net=cum_capex, equity=init+Σni, cash=plug (defaults 60/30/45 days, equity_init 1억). **CF** (indirect): ni+dep−Δwc−capex−debt_rp. xlsx 4→6 sheets, HTML 7→9 sections. **검증** Iippo Y1: total_assets = L+E = 14.4M+209.5M = 223.9M ✅ balanced. 4 (deal×scenario) compute_3fs persist 성공 (217+217+155+155 rows). 4 standalone HTML 모두 9 sections, 브라우저 즉시 view. Phase 1 = depreciation schedule + debt amortization + intangibles + deferred tax. | 2026-05-28 | commit `8997e28` | | **B-index-xlsx-roundtrip** ✅ | LP 가 수정해서 돌려보낸 xlsx 의 변경 검증 + DB 반영. **16번째 MCP tool** `import_xlsx(file_path, deal_code, scenario, apply=false)`. calamine 0.26 crate (read), Assumptions sheet parse. Input drivers 만 비교 (derived는 formula 재계산이라 round-trip 비교 대상 아님). **effective_db_value** semantic: period > 0 에 값 없으면 period 0 fallback (xlsx carry-forward visualization 과 동형). diff kinds: added/changed/removed/unchanged. apply=true 시 financial_driver_value UPSERT, removed 는 보존 (round-trip safety). **검증**: 무수정 round-trip 49 unchanged ✅ byte-equivalent, 1 cell 수정 (0.02→0.05) → 정확히 1 changed 감지 ✅, apply=true → DB UPSERT 1 row ✅. LP 양방향 흐름 완성. | 2026-05-28 | commit `6aa2680` | | **B-blueprint-skill-mcp-client** ✅ | [D-index-17](/ops/decisions) Phase 2 — Blueprint Claude Agent SDK 의 filesystem `.claude/skills/` 직접 의존 제거 reference impl. `bin/index-skill-sync.py` (~297 line, Python stdlib only, no deps) — index MCP 의 `list_skills()` + `get_skill_resource()` 호출 → local cache sync. **Dual transport**: HTTP (production, Entra ID bearer) + CLI fallback (local-dev). **sha256 cache invalidation** — server sha256 vs local file sha256 비교, drift 감지 시 자동 update. **Conditional recursive traversal** — `has_{references,scripts,tests,agents}` 플래그 기반 sub-dir 만 fetch (scripts/_oneoff/ 같은 nested 도 처리). `--diff` mode (dry-run) + `--skill` 단일 필터. **검증**: 1차 sync 43 files added (ic 39 + ingest 4) + 모든 sha256 verified + 2차 sync 43 unchanged + drift simulation (`echo >> SKILL.md`) → 'updated' 감지 + restore 1 updated. `.manifest.json` 으로 audit trail. **다음**: Blueprint Agent SDK 의 skill loader 에 본 cache dir 인식 + cron 등록 → [D-index-17](/ops/decisions) sunset 기준 (b) 충족. | 2026-05-28 | commit `8b6f05d` | | **B-index-lp-html** ✅ | 사용자 결정 PDF 말고 native HTML 로 LP dashboard. 신규 `src/html.rs` (~360 line) + **15번째 MCP tool** `export_lp_html(deal_code, scenario, out_dir?)` — 단일 self-contained HTML5 (inline CSS, 외부 fetch 0). 7 sections: Reproducibility / IRR Summary metric cards / Fund Investments / Income Statement (with derivation rules) / Exit Matrix (joint_probability) / Assumptions / Audit Trail. Korean font stack (Apple SD Gothic Neo / Pretendard / Noto Sans KR), `@media print` 으로 LP 브라우저 PDF 출력 가능, semantic HTML5 (a11y), color-coded (양수 green / 음수 red), sticky table headers. **export_lp_bundle v2**: index.html + xlsx + README.md (cover.md 폐기, index.html zip 첫 entry — LP 가 Finder 에서 가장 먼저 봄). 검증: Iippo base HTML 20,250 bytes, byte-deterministic (2회 sha 동일 `70096195...`), 3 deal × 2 scenario 성공, regression 유지. **LP 흐름 update**: bundle.zip 첨부 → 풀어서 index.html 더블클릭 → 브라우저 dashboard + xlsx cell audit. | 2026-05-28 | commit `eb66fab` | | **B-index-lp-bundle** ✅ | Gap 3 마무리 layer — LP 가 1 첨부로 받는 zip + byte-deterministic. **14번째 MCP tool** `export_lp_bundle(deal_code, scenario, out_dir?)` → `{deal}-{scenario}-{sha[:8]}-bundle.zip` (xlsx + cover.md + README.md). `src/xlsx.rs` 확장 (~150 line): build_cover_markdown (IRR table + Fund table + reproducibility lock + Phase 0 한계 박제), build_readme_markdown (LP 안내문), build_bundle_zip (1980-01-01 epoch deterministic). **Determinism fix 3종** ([D-index-4](/ops/decisions) lock 강화): (a) xlsx DocProperties.set_creation_datetime(epoch) + set_author "AXE Labs index", (b) cover.md `Generated: ` 제거 → inputs_hash[:16] 로 대체, (c) zip last_modified_time(epoch). 검증: 2회 export → xlsx sha + bundle sha 모두 동일 (byte-equivalent). Iippo base bundle 13,302 bytes. 운영 흐름: 전 xlsx 첨부 → 후 zip 1개 (cover + README + xlsx). | 2026-05-28 | commit `d33e8de` | | **B-index-lp-deliverable-export** ✅ | Gap 3 — LP 전달용 xlsx export 완성 ([D-index-18](/ops/decisions) chain 종결). `rust_xlsxwriter 0.95` Rust 네이티브 crate. 신규 src/xlsx.rs (~280 line) + **13번째 MCP tool** `export_lp_model(deal_code, scenario, out_dir?)`. 4-sheet 결정론적 xlsx: (1) **Assumptions** driver_value 매트릭스 + cover (inputs_hash 포함), (2) **Income Statement** 12 line × 7 period + footer derivation rules, (3) **Exit Matrix** leaf-level (joint_probability 는 Excel formula `=E*B`, audit trail), (4) **IRR Summary** per-fund + weighted metrics + reproducibility footer. 검증: Iippo base 11,894 bytes "Microsoft Excel 2007+", **byte-equivalent reproducibility** (같은 DB → 같은 sha256), 3 deal × 4 scenario 전부 export 성공. **운영 흐름**: `export_lp_model Iippo base` → `/tmp/index-lp/Iippo-base-{sha[:8]}.xlsx` → 운영자가 LP 에 첨부 → LP가 Excel 에서 cell formula audit + 1년 후 재현 가능. Phase 1 잔여 = xlsx round-trip (calamine read → DB diff sha256 verify, [B-index-xlsx-roundtrip](/ops/backlog)) + BS/CF 완성 ([B-index-3fs-bs-cf-completion](/ops/backlog)) + PDF cover bundle. | 2026-05-28 | commit `425a758` | | **B-index-3fs-typed-schema** ✅ | Gap 2 — driver_value 위 typed Income Statement / Balance Sheet / Cash Flow line items 도입. migration 20260528000002 `financial_statement_line` 테이블 (model×scenario×statement×line_code×period UNIQUE). **신규 12번째 MCP tool** `compute_3fs(deal_code, scenario, persist=false)` — driver_value lookup (period-0 fallback + base inheritance) → IS 12 line 재구성 (cogs = revenue×(1-gross_margin), opex_total = Σopex, ebit fallback, tax_expense = max(0, pretax × tax_rate), net_income). 검증 Iippo base: 12 line × 7 period = **84 cells persisted**, net_income Y1=1.10억 → Y7=17.15억 (정확히 pretax × 0.78). **Frozen enum 신설** ([D-index-15](/ops/decisions)): statement 3 (income_statement / balance_sheet / cash_flow) + is_line_code 12. BS 12 line + CF 7 line 은 placeholder (Phase 1 = working capital + debt schedule 추가 driver 도입 후 완성). /index/schemas 15 → 16 (`index.financial_statement_line@1.0` active). 회귀: validate-model 27/27 + IRR 3 deal ±1pp 유지. 다음 = Gap 3 LP xlsx export. | 2026-05-28 | commit `9ac0c68` | | **B-index-compute-full-model** ✅ | Gap 1 closure — DSL evaluator DB 통합 (사용자 질문 "Excel model → DB화 best practice" 의 첫 gap). DSL evaluator 는 이미 구현돼 있었으나 financial_driver 와 financial_driver_value table 과 연결 안 됨. **신규 11번째 MCP tool**: `compute_full_model(deal_code, scenario, persist?)` — DB load → scenario inheritance (base 폴백) → base_revenue_period0 → revenue[-1] sentinel → `dsl::evaluator::evaluate(horizon)` → derived 결과 매트릭스 + inputs_hash + (persist=true 시 financial_driver_value UPSERT). **evaluator 개선 2건**: (a) topo_order self-edge skip (revenue[y-1] lag, A7 check_driver_dag 와 동일 정책), (b) lookup_value input period 0 fallback (Excel "constant input row" 패턴, derived 는 strict). 검증: Iippo base 8 period × 2 derived = 16 cells persist → validate-model scenario_coverage **46% → 77%** 자동 상승, IRR ±1pp 회귀 유지. Iippo mgmt 시나리오 inheritance: base 13 + mgmt 11 override = 14 effective inputs, hash 분리 (base 와 다른 inputs_hash). **발견된 Y-suffix anti-pattern → [D-index-18](/ops/decisions) 신규**: `revenue_growth_y1..y7` + 단일 formula 결합 시 period 7 에서 revenue 1.2 quadrillion 원 비현실 compounding. Refactor 잔여 = [B-index-driver-period-indexed-refactor](/ops/backlog). | 2026-05-28 | commit `9df7f71` | | **B-index-skill-mcp-discovery** ✅ | [D-index-17](/ops/decisions) Phase 2 entry point — index MCP 가 ic + ingest skill 의 canonical SoT (filesystem) 위에 discovery layer 추가. 2 신규 tool 도입 (MCP 총 10): (9) `list_skills()` — `/Users/axe/index/skills/` 의 모든 skill 의 SKILL.md frontmatter (`name` / `description` / `allowed-tools` / `required_model`) + sha256 + sub-dir 존재 (references / scripts / tests / agents); (10) `get_skill_resource(skill, path?)` — path 미지정 시 SKILL.md, file = content + sha256, directory = entries[] listing. **Security 4중 차단**: (a) `..` path traversal, (b) skill name 영문/숫자/_/- 만 허용, (c) canonicalize 후 starts_with 재검증 (symlink escape), (d) invalid → NOT_FOUND + context. /index/schemas 도 `index.skill_resource@1.0` 추가 → 14 → 15 schemas (9 active + 6 stub). 다음 = [B-blueprint-skill-mcp-client](/ops/backlog) (Blueprint Agent SDK 가 본 두 tool 로 skill discovery, filesystem 의존 제거) → [D-index-17](/ops/decisions) sunset 기준 (b) 충족. | 2026-05-28 | commit `bf09547` | | **B-index-mcp-surface-2x** ✅ | Phase 0 마무리 — MCP `tools/list` 4 → 8 tool 확장 + `/index/schemas` envelope 활성화 ([D-index-7](/ops/decisions)). 신규 tool: `list_deals(stage?, sector?)` portfolio overview, `get_deal(deal_code)` full snapshot (target_company + fund_investments[] + financial_model + scenarios[]), `cross_deal_benchmark(driver, scenario)` D-index-10 A5 의 MCP 노출, `get_exit_matrix(deal_code, scenario?)` leaf-level detail (joint_probability + dead_leaf + concentration). `/index/schemas` 가 빈 list → 14 schema 본문 (8 active DB-backed: target_company / deal / fund_investment / financial_model / financial_driver / financial_scenario / exit_matrix_leaf / financial_output + 6 stub Phase 1: dd_finding / ic_decision / portfolio_kpi / risk_alert / valuation / lp_comm) + metadata.frozen_enums 4종 ([D-index-15](/ops/decisions) immutable) + dsl_operators 12 + decision_references 9. 신규 CLI `index dump-schemas` (auth bypass JSON dump). 회귀 검증 PASS: validate-model 27/27 + IRR ±1pp IC baseline 유지 (Iippo 36.52% / Sentry 40.26% / Canopy 26.39%). 다음 consumer = ic skill `--push-to-index` ([D-index-13](/ops/decisions) atomic batch) + Blueprint citation resolver (`index.financial_output` kind). | 2026-05-28 | commit `2dfc484` | | **B-index-customers-yaml** ✅ | [D-index-10](/ops/decisions) production prereq — `customers.yaml axe.services.index` block 신설 (5 secret manifest: INDEX_DB_PASSWORD / INDEX_JWT_SECRET / AZURE_INDEX_MCP_CLIENT_SECRET escape_dollar / INDEX_PII_PASSPHRASE_AXEC Phase 1 backlog / ANTHROPIC_API_KEY shared). `service_paths.index` "placeholder 503" → 정상 path. `axe ship` CLI 에 `index` 등재 (`SHIP_SERVICES["index"] = {repo, deploy: manual_hint}` + `SHIP_DIRNAME_MAP`). Dockerfile cortex→index 정정 + sqlx offline `.sqlx/` 28 query cache → DB 없이 Docker 빌드 가능. docker-compose healthcheck python→curl. 검증: 4 컨테이너 healthy + `/healthz` 200 + `/index/mcp` 401 + WWW-Authenticate (RFC 9728) + RFC 9728 envelope. **Manual 잔존** (Soohun Global Admin): az ad app + admin consent + axe secret push + cloudflared route + GitHub remote + 첫 `axe ship index`. 상세 runbook = [/services/index#production-배포-axe-ship-index](/services/index). | 2026-05-28 | commit `38dcac8` | | **B-index-A8-mcp-tools-integration** ✅ | [D-index-10](/ops/decisions) A8 — MCP `tools/list` + `tools/call` 4 tool 통합 (`src/mcp.rs` 137 → 566 line). **4 tools**: (1) `propose_deal_closure(seed)` atomic batch INSERT (D-index-13 정합 — 4 sub-tool 흡수, UUIDv5(deal_code) idempotency_key retry-safe), (2) `validate_financial_model(deal_code)` (A7 6 sanity check), (3) `compute_outputs(deal_code, scenario?)` (exit_matrix aggregation), (4) `query_irr(deal_code)` (per-fund display + summary weighted IRR). Methodology = irr::weighted_* 함수 재사용으로 validate.rs in-memory 결과와 byte-identical. 검증: Iippo 36.52% / Sentry 40.26% / Canopy 26.39% — 3 deal 모두 IC baseline ±1pp PASS. 3-layer error model (D-index-14) 적용: L1 HTTP (auth middleware) + L2 MCP body (JsonRpcError + error_code + context) + L3 skill. CLI smoke test 도입: `index mcp-call TOOL --args JSON` (auth bypass, dev 반복 회귀). **D-index-10 acceptance 8/8 PASS** (A6 xlsx round-trip 만 별 backlog 잔여). | 2026-05-28 | [D-index-10](/ops/decisions), `src/mcp.rs` | | **B-index-A7-validate-financial-model** ✅ | [D-index-10](/ops/decisions) A7 — `index validate-model` CLI 6개 sanity check 도입 (`src/validate_model.rs` 608 라인). (1) driver_dag.cycle (topo sort, `revenue[y-1]` self-lag skip), (2) scenario_coverage (exit-matrix-only 모델 인식 — Sentry/Canopy 0% = PASS), (3) exit_matrix.sanity (dead_leaf + concentration > 50%), (4) leaf_probability_sum (Σ = 1.0 ± 0.02), (5) leaf_probability_sanity (V1 outdated upside never×writedown > 5% 자동 검출), (6) fund_investment.invariants (status=paid → invested_krw + paid_date 필수). 3 deal 모두 9/9 check PASS. 발견·수정: Canopy Initial BW paid_date NULL → 2025-06-01 보강. D-index-10 6/8 acceptance test 완료 (A1+A2+A3+A4+A5+A7). 잔여 = A6 (xlsx round-trip) + A8 (MCP tool integration). | 2026-05-28 | [D-index-10](/ops/decisions), `src/validate_model.rs` | | **B-customer-deploy-generalization** ✅ | 5 요건 R1-R5 빌트인 완료 (Phase 1 5/26 + Phase 2 D-day 5/25). plan SSOT: `/Users/axe/customer-deploy-generalization-plan.md`. R1 `${HOME}` 변환 ✅, R2 vault SoT (`bw get password`) ✅, R3 `$` → `$$` escape (`escape_dollar` flag) ✅, R4 `image_override` 슬롯 ✅, R5 container_name customer-prefix (별 backlog [B-container-name-customer-prefix](#-신규-분류-안-됨) 으로 split). `_deploy_service_customer` 13-step + wrapper 3종 (`start-{frame,blueprint,hive}.sh`) + `bw-bootstrap.sh`. realchoice D-day 5/5 endpoint LIVE 검증. Phase 3 (3rd customer dry-run) 은 [B-customer-deploy-generalization-phase3-3rd-customer](#-신규-분류-안-됨) split. | 2026-05-26 | [/Users/axe/customer-deploy-generalization-plan.md](docstring) | | **B-realchoice-port-collision-audit** ✅ | 트루비아 기존 14 컨테이너 (stream-mcp:8780, magnet-mcp:8770, ~/vault:8222, stream-realchoice 외) ↔ axec stack 포트 충돌 사전 점검. **D-day 5/25 5/5 endpoint LIVE 으로 implicit pass** (trap #6 포트 점유 함정은 onboard 중 발견 후 해소, known-gaps "realchoice D-day 6 함정" 에 포트 충돌 미언급 = 실제 충돌 없음). 영구화 = [B-port-conflict-preflight](#-신규-분류-안-됨) 의 onboard step preflight CLI. | 2026-05-25 | trap #6 + Ship Log 5/25 | | **B-realchoice-vault-instance-decision** ✅ | 트루비아 기존 `~/vault` (Vaultwarden Timshel fork, port 8222) ↔ axec deploy 시 신설 `axe-vaultwarden` 인스턴스 선택. **결정**: 옵션 (a) **기존 vault 유지 + OIDC SSO 추가** (data migration 부담 0). 5/25 (PM) Ship Log "Vault OIDC SSO 통합 ✅ (Q3 (a) 채택, `~/vault` = `soohunkang/vault` repo, SSO 로그인 → 잠금 해제 정상)" — D-day 실 운영 검증 완료. description 의 "(b) 잠정" 은 5/24 시점 RE^2 §Q3 가정 → 5/25 (a) 확정으로 정정. | 2026-05-25 | Ship Log 5/25 PM | | **B-anthropic-connector-input-attrs** ✅ | claude.ai Custom Connector modal 의 4 input 식별자 부재 함정 — Anthropic 측 업스트림 보고 draft 작성. `/ops/reports/anthropic-connector-input-attrs-2026-05` (173 라인, 9 섹션 self-contained: Summary / Env+Repro / Root cause + DOM analysis / Requested fix `name`/`aria-label`/`autocomplete` / Impact 표 / AXE Labs 우회 (B-axe-mcp-catalog-en-aliases 참조) / Attachments / Channels (support@anthropic.com + GitHub claude-code issue) / Status). 운영자 검토 후 제출. 수락 시 한국어 UI 4/4 자동입력 + 모든 i18n locale 동시 해소. | 2026-05-28 | [/ops/reports/anthropic-connector-input-attrs-2026-05](/ops/reports/anthropic-connector-input-attrs-2026-05) | | **B-vault-axe.2-patches-upstream-pr** ✅ | Timshel/vaultwarden upstream PR draft 작성. `/ops/reports/timshel-vaultwarden-upstream-pr-2026-05` (165 라인, 9 섹션 self-contained: Summary / Motivation (symptom + trigger + affected) / Changes (patch 0001 prelogin alias + 0002 connect/token AccountKeys+MasterPasswordUnlock) / Testing (manual verification + real-world validation) / Compatibility (backward+forward+no migration) / Code reference (axelabs-ai/vault commit hash TBD) / Related (axe.3 patches 별 PR) / Status / Channels). 두 patch 모두 dani-garcia/vaultwarden mainline backport 성격 → 수락 가능성 높음. 수락 시 AXE fork diff 자연 소실 → [B-vault-axe.2-sso-mp-incomplete](#-신규-분류-안-됨) archive. | 2026-05-28 | [/ops/reports/timshel-vaultwarden-upstream-pr-2026-05](/ops/reports/timshel-vaultwarden-upstream-pr-2026-05) | | **B-trap-33-frame-hive-multi-issuer-docs** ✅ | trap #33 의 (c) docs portion — [/architecture/auth](/architecture/auth) line 147 정정 + 함정 표 row 추가. "v1 token (aud=URI) OR middleware 양쪽 수용" 권장 → **"v2 강제 + middleware multi-issuer 양쪽 모두"** (defense in depth). 함정 row = Frame/Hive `_MICROSOFT_ISS_PREFIX` v2-only matcher 가 v1 token 을 HS256 fallback path 로 보내 RS256 reject 시키는 정확한 메커니즘 + fix 위치 (`http_server.py:141`/`:67`). code fix (a, b) 잔존 = [B-trap-33-frame-hive-multi-issuer](#-신규-분류-안-됨). | 2026-05-28 | [/architecture/auth](/architecture/auth) | | **B-vault-d-day-traps-2026-05-25** ✅ | realchoice vault SSO 통합 시 발견 **8 함정** 인벤토리 entry. 모든 함정 catalog 완료 + 80%+ resolved (#10 `$` escape ✅ / #11 sso_nonce verifier ✅ / #12 axe.2 patch ✅ / #13 bw appdata 격리 ✅ / #14 → [B-vault-org-cli-automation](#-신규-분류-안-됨) split). 잔여 #8 (vault-caddy path mount), #9 (wget → curl), #15 (Caddyfile `/vault/` trailing slash) 는 production 운영 검증 (axe.axelabs.ai/vault 정상) — vault-caddy 측 minor patch 또는 documented behavior 로 본 inventory entry 는 종결. | 2026-05-28 | trap #8-#15 인벤토리 | | **B-onboard-d-day-traps-2026-05-25** ✅ | realchoice D-day 첫 onboard 실행 중 발견 **15 함정** 인벤토리 entry. #1-#10 D-day 초기 함정 ✅ (Tailscale alias / SSH PATH / keychain partition / 포트 점유 [B-port-conflict-preflight](#-신규-분류-안-됨) split / hardcoded path R1 ✅ / docker ps 사전 검사 ✅ / frame-proxy archived ✅ / cloudflared 127.0.0.1 ✅). #11/#12 PAT workaround. #16 → [B-vault-org-create-automation](#-신규-분류-안-됨). #17 `_svc_step_network` ✅. #18 → [B-axelabs-bootstrap-blueprint-mcp-app](#-✅-done) ✅. #19 → [B-customer-deploy-generalization-r6-volume-precreate](#-신규-분류-안-됨). #20 → [B-customers-yaml-mkdir-trap](#-✅-done) ✅. 모든 sub-trap 이 ✅ 또는 split 완료 — 본 inventory entry 종결. | 2026-05-28 | trap #1-#20 인벤토리 | | **B-ops-reports-sidebar** ✅ | `/Users/axe/axelabs-docs/content/ops/_meta.js` 에 `reports` entry 추가 (troubleshooting 다음, known-gaps 앞). Round 1 의 ops/reports/ 폴더 신설 + Round 2 의 2 추가 mdx 가 sidebar 에 노출. | 2026-05-28 | `content/ops/_meta.js` | | **B-vault-setup-playbook-v2** ✅ | taehun.kang 의 첫 실제 vault setup 피드백 5건 (2026-05-27 Teams) 을 [/onboard/vault-setup](/onboard/vault-setup) playbook 에 inline 반영. (1) Phase 2 macOS/Windows sub-section 분리 (Windows Hello + Microsoft Store vs `.exe` + PIN fallback), (2) Phase 4/5 강제 vs 선택 구분 + skip 영향, (3) Phase 5 TOTP 의미 "기존 item 손댈 필요 없음, 새 2FA 부터 적용" 명시, (4) TOTP UI 동작 3종 (view/edit/empty) 표, (5) "자동 채우기 정상" 검증을 SSO 위주 vs 웹 폼 위주 워크플로 분기, (6) 표준 vs valid 변형 섹션 신설. +84 라인 (200→284). | 2026-05-28 | [/onboard/vault-setup](/onboard/vault-setup) | | **B-vault-revoke-scope-doc** ✅ | taehun 피드백 #6 (2026-05-27) — vault trust boundary 명확화. [/architecture/vault-policies](/architecture/vault-policies) 에 `## Vault scope — 무엇을 보호하는가 (trust boundary)` 신설 (8-row 분리표) + [/ops/runbook/employee-offboarding](/ops/runbook/employee-offboarding) 에 step 8 "외부 service 권한 회수 (vault scope 외)" 표 6 row + 함정 표 row. 메시지 = vault = secret 보관 ≠ 외부 service 권한 제어 (KB / 홈택스 / GitHub PAT / Cloudflare Access 회수는 별 절차). | 2026-05-28 | [/architecture/vault-policies](/architecture/vault-policies) | | **B-bw-cache-recovery-procedure-docs** ✅ | bw CLI data.json cache stale 함정의 표준 recovery 절차 docs 등재 (`/architecture/secrets` 함정 표 row + `/ops/runbook/vault-recovery` 새 ## 섹션 ~48 라인). 증상/원인/빈도 (5/22 + 5/26)/회복 절차 4 step bash/자동화 후보 3 prong/함정 4-row 표. 자동화 잔여는 [B-bw-cache-stale-autoheal](#-신규-분류-안-됨) (별 backlog). | 2026-05-28 | [/ops/runbook/vault-recovery](/ops/runbook/vault-recovery) | | **B-blueprint-scope-change-runbook-docs** ✅ | Blueprint OAuth scope 추가 함정의 docs 등재. [/architecture/auth](/architecture/auth) 의 함정 표에 row 1개 (refresh token 침묵 사망 + AADSTS65001 + MSAL 캐시) + `## OAuth scope 추가 시 운영자 절차` 새 섹션 (5-step bash: ship → admin-consent → docker restart → 검증 → user reconnect 안내). +27 라인. 자동화 prong 2개 (post-deploy hook + token health check) 는 [B-blueprint-scope-change-admin-consent-runbook](#-신규-분류-안-됨) 잔존. | 2026-05-28 | [/architecture/auth](/architecture/auth) | | **B-claudeapp-fd-leak-anthropic-report** ✅ | Claude.app PTY/fd leak 의 Anthropic 측 공식 리포트 draft 작성. `/Users/axe/axelabs-docs/content/ops/reports/claudeapp-fd-leak-2026-05.mdx` (113 라인, 10 섹션 + frontmatter) — Summary / Environment / Symptom (failure mode + reproduction + diagnosis) / Workarounds / Suspected root cause / Impact / Requested fix / Attachments / Channels / Status. `ops/reports/` 폴더 + `_meta.js` 신설. 운영자 검토 후 support@anthropic.com 또는 GitHub claude-code repo issue 로 제출. | 2026-05-28 | [/ops/reports/claudeapp-fd-leak-2026-05](/ops/reports/claudeapp-fd-leak-2026-05) | | **B-docs-ssot-extension-2026-05-26-corrections** ✅ | Truvia 2026-05-26 피드백 정정 7건 (B1-B7) docs 반영. **B1** [/ops/runbook/vault-recovery](/ops/runbook/vault-recovery) bw cache section + **B2** [/architecture/secrets](/architecture/secrets) `$` escape 함정 row + **B3** [/architecture/auth](/architecture/auth) (별 B-blueprint-scope-change 와 연계) + **B4** [/architecture/topology](/architecture/topology) `artemis_default` external network 함정 row + **B5** [/services/frame](/services/frame) `${HOME}` 함정 + `/Users/axe/` hardcode 금지 callout + **B6** [/partner/registration](/partner/registration) `--allow-no-subscriptions` 검증 step + **B7** [/partner/deploy](/partner/deploy) 함정 표 18-step 형태 재구성 (11 신규 row). 잔여 = 신규 페이지 A1/A2 ([B-docs-ssot-extension-2026-05-26-newpages](#-신규-분류-안-됨)). | 2026-05-28 | 다중 페이지 | | **B-docs-playbook-frontmatter-marker** ✅ | playbook 페이지 frontmatter `playbook: true` 스킴 + `_meta.js` entry title 에 `⚡ ` prefix. 15 페이지 일괄 적용 (onboard 4 + ops/runbook 11). SSOT = [/architecture/playbooks](/architecture/playbooks). | 2026-05-26 | [/architecture/playbooks](/architecture/playbooks) | | **B-docs-playbook-catalog-build-time** ✅ | `scripts/generate-playbook-catalog.mjs` 도입 — frontmatter `playbook: true` 페이지 인덱싱 + SSOT [/architecture/playbooks](/architecture/playbooks) 의 marker 사이 표 자동 교체 + `public/playbooks.json` deterministic export. | 2026-05-26 | [/architecture/playbooks](/architecture/playbooks) | | **B-realchoice-d7-checklist** ✅ | Truvia 5/25 D-7 회신 처리 완료 (RE^4). (1) payslip Hive Phase 3 deferral, (2) 외부 회계법인 사내 협의 → [B-frame-cross-check-workflow](#-ready-다음-세션이-집어갈-수-있음) 분리, (3) macmini 이진우 자택, IP 동일, Tailscale 입고 후 회신 → [B-realchoice-tailscale-onboard](#) 분리. | 2026-05-25 | RE^4 | | **B-realchoice-tailscale-onboard** ✅ | 피벗 (5/25 RE^6) — macmini 발주 skip, 기존 운영 macmini → `realchoice-macmini`. Tailscale ACTIVE direct + SSH + Docker + 절전 OFF 모두 기존 상태. 외부 IP 변경 X. D-day = 본일 (5/25). | 2026-05-25 | RE^6 | | **B-axe-mcp-catalog-en-aliases** ✅ | `axe mcp publish` 의 `_mcp_item_payload` 3 차례 iteration + 운영자 retest 결과 — autofill 2/4 (Client ID + Secret) 유지 + Name/URL 은 copy-paste convenience field 등재. **확정 finding**: `Remote MCP server URL` field 추가 = regression (1/4), `Server URL` 단독 (EN label) = 안전. 6 Custom Field 표준 (Name + Server URL + MCP URL + Tenant ID + Scopes + Vault secret path). 매칭 천장 4/4 는 Anthropic markup 변경 ([B-anthropic-connector-input-attrs](#-신규-분류-안-됨)) 필수. **금지** = `Remote MCP server URL` 절대 추가 X. | 2026-05-27 | axe CLI docstring | | **B-docs-cache-control-static** ✅ | docs.axelabs.ai 의 자주 갱신되는 정적 자산 (`/llms.txt`, `/llms-full.txt`, `/changes.json`) 에 `s-maxage=60` cache-control 명시. `next.config.mjs` 의 `headers()` source 3개 추가 — 기존 `/_pagefind/*` (commit 9c13bcc) 와 동일 패턴. ship 직후 Cloudflare 캐시 잔존 함정 차단. | 2026-05-26 | commit 9c13bcc + 본 PR | | **B-trap-24-blueprint-mcp-admin-consent** ✅ | trap #24 영구 fix (docs 측). `/partner/registration` 의 Option A 사전 확인 표에 admin consent grant 권한 4 가지 role (Global Admin / Privileged Role Admin / Cloud App Admin / Application Admin) 명시 + 함정 표에 admin consent 실패 시 manual grant 절차 (portal UI / admin consent URL) 명시 + Option B 함정 모음에 admin consent 함정 row 추가. bootstrap.sh 측 Blueprint MCP app 등록 fix 는 [B-axelabs-bootstrap-blueprint-mcp-app](#-ready-다음-세션이-집어갈-수-있음) 별 task. | 2026-05-26 | trap #24 | | **B-customers-yaml-mkdir-trap** ✅ | trap #20 영구 fix. **Root cause 확인** = axe CLI / bootstrap.sh / wrapper scripts / partner docs 어디에도 `mkdir -p .../customers.yaml` 같은 잘못된 명령 부재 (grep 검증 완료). Truvia 측 manual 실수 가능성 高 (customer 측에는 customers.yaml 부재가 정상 — operator SOT). **Defensive fix** = axe CLI `_load_yaml` + `_load_customers_yaml_rt` 에 `path.is_dir()` sanity check 추가 — file 예상한 path 가 dir 일 때 `IsADirectoryError` stack trace 대신 명확한 회복 안내 (`rm -rf + recreate as file`). axe CLI 는 git untracked ([B-axe-cli-git-track](#-ready-다음-세션이-집어갈-수-있음)) 라 단일 머신 적용. | 2026-05-26 | trap #20 | | **B-axelabs-bootstrap-blueprint-mcp-app** ✅ | trap #18 영구 fix. `axelabs-bootstrap.sh` 에 App #4 Blueprint MCP 등록 step 추가 (Frame MCP 패턴 1:1 mirror — application_id_uri `/blueprint/mcp` + mcp.access scope + claude.ai redirect + optional claims + v1 token + self-ref permission). JSON pack 의 `apps` 에 `blueprint_mcp` 필드 추가. `axe customers ingest` 가 optional 로 흡수 (옛 3-app pack 도 backward compat). `/partner/registration` docs = "3 개" → "4 개" + Option B App #4 섹션 신설 (Frame MCP 와 동일 절차 + 경로 차이 표). 새 SHA-256 = `ae6f771e85f7ec49d75bc082e1e0d42b106ba095e03d9adad430e590c2c4aefb`. 양쪽 (`public/` + `.axe/bootstrap/`) 동기화. | 2026-05-26 | trap #18 | | **B-trap-23-hive-volume-compose-prefix** ✅ | trap #23 영구 fix — **R6 (B-customer-deploy-generalization-r6-volume-precreate) 통합 결정**. hive docker-compose.yml 의 external volume name (`hive-proxy_*` historical prefix) 정합성 문제는 R6 의 blueprint volume 6개 fix 와 함께 차기 phase 에서 한꺼번에 해소 (compose `external:true` 제거 → self-contained volume 권장). 별 task 종결, R6 row 비고에 trap #23 명시 추가. | 2026-05-26 | trap #23 → R6 | | **B-vault-mcp-catalog** 🆕✅ | MCP Connectors catalog view ([D-vault-mcp-catalog](/ops/decisions)) — Vaultwarden `MCP Connectors` org collection (`1a62e754-6e47-43e0-a99a-cf71c37b8638`, 4 org 멤버 모두 access) + `axe mcp publish/list` CLI subcommand (axe CLI `cmd_mcp_publish`/`cmd_mcp_list`) + `axe secret rotate` 의 step 4.5 자동 hook (env name 패턴 `AZURE_*_MCP_CLIENT_SECRET` 감지 시 catalog 재발행) + customers.yaml frame_mcp 의 stale `client_secret_env` 주석 정정. 3 MCP seed (frame/hive/blueprint) publish 완료. `customers.yaml` SoT + Vaultwarden item SoT 변경 0 (catalog 은 derived view only). 함정 1 발견 + 정정 (frame_mcp client_secret_env stale, 5/26 정정). docs = [/architecture/secrets#mcp-connectors-catalog](/architecture/secrets) + [/ops/decisions D-vault-mcp-catalog](/ops/decisions) + known-gaps "vault측 미해결" 표의 bw cache stale 항목 cross-link. | 2026-05-26 | [D-vault-mcp-catalog](/ops/decisions) | | **B-bw-cache-recovery-procedure** 🆕✅ | bw 2025.7.0 의 local data.json cached `cryptoSymmetricKey` stale 증상 (5/22, 5/26 두 차례) root cause 확정 + recovery procedure 표준화. **Root cause** = server-side patch deploy (axe.2, axe.3 등) 후 bw `sync` 가 cache 를 inconsistent 상태로 저장. `bw unlock` 은 캐시 무효화 X. **확정 절차** = `mv "~/Library/Application Support/Bitwarden CLI" .broken.$(date +%s) && bw config server https://axe.axelabs.ai/vault && bw login ai@axellc.com` (positional password 또는 osascript dialog). 약 1분. **추가 발견**: bw 2025.7.0 의 `bw unlock --passwordenv` 가 silent failure (rc=0 + empty output) — positional password `bw unlock "$PW" --raw` 만 작동. 본 PR 의 본격 영구 fix 는 [B-bw-cache-stale-autoheal](#) (axe wrapper layer) 로 분리. | 2026-05-26 | known-gaps "vault측 미해결" 표 + [B-bw-cache-stale-autoheal](#) | | **B-vault-fork-build-pipeline** | [D-ops-40](/ops/decisions) 의 build pipeline 실 구현 + axe.3 release. `axelabs-ai/vault` repo public 전환, `build/build.sh` + `.github/workflows/build.yml` + `build/patches/0001..0004.patch` (axe.2 + axe.3 4 patch) commit + main push (commit `799015b`). GHA buildx multi-arch (linux/amd64+arm64) → GHCR push (PAT vault-stored, item `ghcr-axelabs-ai-pull-pat` in Platform — Service Secrets). main-`` rolling tag + imagetools alias `1.34.1-6-axe.3` (manifest list digest `sha256:a26208a0794acbc9a2807379ffba33c7478dbe8d41daed24893f7916a55aeada`). axe-macmini compose 의 image 라인 = `ghcr.io/axelabs-ai/vault@sha256:...` 로 재핀 ([D-ops-11](/ops/decisions) immutable manifest-digest 정책 정합 복원). | 2026-05-26 | [D-ops-40](/ops/decisions) + axelabs-ai/vault commit `799015b` | | **B-vault-org-perm-3-quirks** | [D-ops-40](/ops/decisions) patch 0003 + 0004 axe.3 release deploy + 3건 in-production 검증 통과. **patch 0003** = `src/api/core/organizations.rs` 의 3 site (line 522, 600, 685 — post_organization_collections + post_bulk_access_collections + post_organization_collection_update) 에서 `if member.access_all { continue; }` skip 제거 → Owner 도 명시 `users_collections` row 생성. **patch 0004** = `src/db/models/cipher.rs::to_json` (line 374 근방, User-sync branch) 에 `permissions: {response:null, delete:, restore:}` 추가 (`` = collection manage OR org Owner/Admin). 함정 2 (cipher edit collectionIds silent partial-revert) 는 vaultwarden bug 아닌 mainline design 으로 확인 — 운영 룰 영구 ([B-vault-org-cli-automation](#) 의 본 함정 확장). **검증 3건 in-production** (2026-05-26 13:19 KST): (1) `bw create org-collection ... users=[{id:ai@,manage:true}]` → SQLite users_collections 에 `ai@axellc.com\|0\|0\|1` row 명시 insert ✅, (2) raw GET `/api/sync` → org cipher 의 `permissions: {delete:true,restore:true,response:null}` 정확 emit ✅, (3) `bw delete item` rc=0 ✅. 영구 fix 완성, 운영 임시 룰 (SQL 수동 INSERT, admin endpoint 직접 호출) 자연 폐기. | 2026-05-26 | [D-ops-40](/ops/decisions) Progress | | **B-matrix-git-init** ✅ | `/Users/axe/matrix` git init + first commit. 본 점검 세션 시작 시 working tree 가 init 상태였으나 commit 0 (`no commits yet`). `.gitignore` 보강 (`.env`, `.env.local`, `.claude/`, `.DS_Store`, `*.log` 추가 — 기존 `/target` 만) 후 13 파일 staged + commit `f73492d` "initial: matrix MCP monitoring service v0.1.0" + commit `48a301b` (collector port fix). ultrareview / 롤백 / origin push 가능 상태 회복. | 2026-05-26 | [D-matrix-1](/ops/decisions) | | **B-matrix-rebuild-stale** ✅ | matrix blue/green 컨테이너 rebuild — `cargo check` 결과 `src/api.rs:26` 의 `matrix_info` 함수 미정의 (E0425) 로 working tree 자체가 컴파일 불가였음 (11:35 빌드 후 11:39 미완성 수정 흔적). `matrix_info` honest minimal stub (service info JSON: name/version/endpoints 4개) 추가 + collector port fix 와 한 번에 무중단 rebuild (green→blue 순서, 두 컨테이너 healthy 검증). 새 image digest, external `axe.axelabs.ai/matrix` 도 새 landing 반환. | 2026-05-26 | commit `48a301b` | | **B-matrix-collector-port-fix** ✅ | `src/collector.rs:180` 의 blueprint-http URL `:3110` → `:3100/api/health` 정정. 호스트 `lsof` + 컨테이너 안 `docker exec curl` 양쪽 검증 — :3110 미 listening, :3100/api/health 가 정답 (Blueprint Next.js dev, `/Users/axe/blueprint/src/app/api/health/route.ts` 존재). cortex-fe/cortex-be 는 그대로 — cortex backend :3210 실제 down, cortex frontend :3200 root path 빈 응답으로 **valid alert**. rebuild 후 첫 cycle: failures = `["cortex-fe-http", "cortex-be-http"]` (blueprint 사라짐 ✓). 누적된 blueprint-http unacked alert 1 row 도 ack 처리 (`acked_by='collector-port-fix-2026-05-26'`). | 2026-05-26 | commit `48a301b` | | **B-matrix-mcp-auth-enforce** ✅ | matrix `/matrix/mcp` 에 JWT auth middleware 부착. router 분리 — `mcp_router = Router::new().route("/matrix/mcp", post(mcp::handle_mcp)).route_layer(middleware::from_fn(auth::auth_middleware))` + main router 에 merge. REST + health 는 anonymous 유지 (의도된 공개 status board). **부착 전 안전 검증** = production access log 0 (matrix-mcp-blue/green/proxy 모두 /matrix/mcp 호출 흔적 0) + Blueprint repo 에 matrix MCP client 코드 0 + customers.yaml 에 `MATRIX_JWT_SECRET` vault 등재 (`matrix/axe/jwt-secret`) = JWT 의도 분명. 검증: no-auth POST → 401 "missing bearer token", REST → 200. `cargo check` 의 `auth_middleware never used` warning 사라짐. | 2026-05-26 | commit `96cf758` | | **B-matrix-cortex-collector-skip** ✅ | matrix collector 의 cortex-fe-http / cortex-be-http 두 endpoint 제거 — **cortex 는 개발 중** (운영자 확인 2026-05-26), launchd 미부트 + `:3210` 미 listening + `:3200` 빈 응답으로 valid alert 가 운영 noise 화. `src/collector.rs:181` 에 재가동 시 재활성화 조건 주석 명시. rebuild 후 collector cycle = `check passed` (failures 0). 누적 unacked alert 2 row (cortex-fe + cortex-be) 도 ack 처리. | 2026-05-26 | commit `96cf758` | | **B-frame-fund-ingest-verify** | 강태훈 대표 fund 자료 ingest 시도 결과 audit — D-ops-30/PR #38 fix 적용 후 `axe_ia_001` (kip, fund_ksme) 정상 empty fund 로 회복 (48 accounts + accounting_policy bootstrap + alembic 0019 head). 회계 데이터 0 = 사용 시작 준비 완료. 7 entities → 5 로 좀비 정리 (`axe_ia_001_diag` + `bug_check_kip_20260524` DROP, `axep` 보존) | 2026-05-26 | [D-frame-fund-ksme-policy-check](/ops/decisions) + [D-frame-register-entity-atomic](/ops/decisions) | | **B-frame-register-entity-atomic** | `register_entity` atomicity + mismatch detect (강태훈 bug report root cause #2/#3 잔여분 영구 fix). (a) existing row 의 standard/kind/schema_name mismatch 시 ValueError (silent override 금지), (b) `upgrade_entity` 실패 시 compensating `DROP SCHEMA CASCADE` + `DELETE shared.entity` 로 좀비 회수. 6 신규 pytest (mismatch standard/kind/all-diffs · idempotent identical · zombie cleanup · pre-existing 보호) + 16 기존 PASS 무회귀 | 2026-05-26 | [D-frame-register-entity-atomic](/ops/decisions). `src/frame/db/migrations.py:88` + `tests/test_register_entity_atomicity.py` 신설 | ## 🗄️ Archive (1주+ 경과 · Ship Log + decisions 영구 보존) > Done 에서 1주 경과한 ship 완료 항목. 상세 narrative = [updates Ship Log](/ops/updates), 결정 = [decisions](/ops/decisions). 본 표는 ID → 결정 trace 만 유지 (compact). | ID | 한 줄 | 완료일 | 결정 | |---|---|---|---| | B-bp-artifact-prisma + B-bp-artifact-link-table | Artifact + ArtifactLink Prisma migration + `src/lib/artifact/store/` | 2026-05-23 | [D-bp-artifact-1](/ops/decisions) | | B-bp-artifact-schema-discovery | Blueprint `McpSchema` 모델 + fetcher/registry + admin API 2 | 2026-05-23 | [D-bp-artifact-1](/ops/decisions) | | B-frame-mcp-schema-endpoint | frame `/frame/schemas` 13 schemas | 2026-05-23 | [D-bp-artifact-1](/ops/decisions) | | B-hive-mcp-schema-endpoint | hive `/hive/schemas` 15 schemas | 2026-05-23 | [D-bp-artifact-1](/ops/decisions) | | B-onboard-azure-pack | `axe customers ingest` CLI 신설 (schema 검증 + yaml fill + vault push) | 2026-05-23 | [D-ops-34](/ops/decisions) | | B-onboard-bootstrap-publish | `axelabs-bootstrap.sh` raw 노출 + partner docs self-contained | 2026-05-23 | [D-onboard-bootstrap-publish](/ops/decisions) | | B-health-monitor-launchd | `com.axe.health-check` → matrix service 대체 | 2026-05-23 | [D-matrix-1](/ops/decisions) | | B-docs-search-impl | docs.axelabs.ai Pagefind UI 검색 코드 머지 | 2026-05-22 | [D-docs-search-1](/ops/decisions) | | B-frame-shared-extend | `shared.entity` entity_kind+fund_meta+closed_at ADD (0008_shared) | 2026-05-22 | [D-ops-22](/ops/decisions) | | B-frame-entity-relationship | `entity_relationship` ownership numerator/denominator + kind ENUM | 2026-05-22 | [D-ops-22](/ops/decisions) | | B-frame-cross-journal-link | `shared.cross_journal_link` 신설 (mirror 분개 pair) | 2026-05-22 | [D-ops-22](/ops/decisions) | | B-frame-register-entity-cli | `register_entity` Python API + CLI `--kind`/`--fund-meta` | 2026-05-22 | [D-ops-22](/ops/decisions) | | B-frame-fund-deploy | frame-mcp rebuild + `0008_shared` migration + 0 regression | 2026-05-22 | [D-ops-22](/ops/decisions) | | B-frame-rfc9728-fix | RFC 9728 resource-level path 401 → Connector Reconnect fix | 2026-05-22 | [D-ops-23](/ops/decisions) | | B-vault-sso-email-verif | `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION` Entra email_verified fix | 2026-05-22 | [D-ops-24](/ops/decisions) | ## 신규 항목 등재 — 한 줄 가이드 ```markdown | **B--** | <행동 한 줄> | M? | <추정 d> | <의존성/비고 + decisions/docs 링크> | ``` 규칙: - ID = `B-AREA-SLUG` (B = Backlog, area = `org` · `frame` · `hive` · `bp` · `docs` · `infra` · `legal` · `backup` 등) - 한 줄 행동 — 1주 안 끝날 항목은 쪼개거나 마일스톤으로 승격 - 마일스톤 ID 명시 (M1~M5) — 없으면 `—` - 관련 decisions / docs 항상 링크 - 작업 시작 = 🔧 로 옮기고 owner + 시작일 추가 - 완료 = ✅ 로 옮기고 완료일 + 결정 링크. 1주 후 archive ## 본 페이지의 의도 다른 세션이 들어와도 **"무엇부터 할 지" 가 5초 안에 보이게**. [known-gaps](/ops/known-gaps) 는 "왜 그렇게 됐는지·뭐가 함정인지" 의 분석적 정보, 본 페이지는 "다음 행동" 의 실행 큐. 마일스톤 큰 그림은 [roadmap](/ops/roadmap). --- # 아키텍처 결정 (DECISIONS) > D1~D-ops-19 누적 결정 기록 + 함정 모음. URL: https://docs.axelabs.ai/ops/decisions # 아키텍처 결정 누적 기록 다음 두 SSOT 파일에 결정이 누적되어 있고, 본 페이지는 그 요약입니다. 변경 발생 시 그쪽 파일이 우선이고 본 페이지는 후행 정리: - [/Users/axe/multi-tenant-platform-plan.md](https://github.com/soohunkang/blueprint/blob/main/multi-tenant-platform-plan.md) — 마스터 플랫폼 plan + D 결정 + 함정 - [/Users/axe/frame/DECISIONS.md](https://github.com/soohunkang/frame/blob/main/DECISIONS.md) — frame 측 D-ops-1 ~ 15 ## D — 플랫폼 결정 | ID | 결정 | 채택일 | |---|---|---| | D1 | **격리 = customer 1개 / macmini 1대** (OS-level, SPOF 수용) | 2026-05 | | D2 | **테넌트 ID = 이메일 도메인** (customers.yaml SSOT) | 2026-05 | | D3 | **path-based 1-level subdomain** (`{customer}.axelabs.ai/{service}`) | 2026-05 | | D4 | **Tailscale 메시 + SSH key** (자체 mTLS 안 만듦) | 2026-05 | | D5 | **HTTP MCP + 부트스트랩 JWT** (OAuth 1차 deferral) | 2026-05 | | D6 | **push-based CI/CD** (pull polling 안 함) | 2026-05 | | D7 | **P2P ring restic backup** (mesh 안 만듦) | 2026-05 | | D8 | **3-tier secret** (vault + keychain + env) | 2026-05 | | D9 | **schema-per-entity** (고객 내 법인 격리, RLS 대신) | 2026-04 | | D10 | **index / vault 동일 패턴** (Phase 6 deferral) | 2026-05 | | D-config-13 | **다운타임 0 frame deploy** (blue/green + host proxy, cloudflared 미관) | 2026-05-15 | | D-config-14 | **DR cold storage** (external SSD rotation, 종이 password) | 2026-05-15 | | D-config-15 | **blueprint multi-customer** (env override per macmini) | 2026-05 | | D-config-16 | **blueprint = Postgres** (SQLite deprecate 예정) | 2026-05 | | D-config-17 | **Postgres cutover 무중단** (운영자 명시 승인) | 2026-05 | ## D-ops — frame 운영 결정 | ID | 결정 | 채택일 | |---|---|---| | D-gate-5 | **e-sign 직접구현 = 암호등급 자체 서명엔진 ([D-gate-3](/ops/decisions) 모두싸인 integrator 입장 supersede)** (2026-06-07 — 빌드+검증 완료, 운영자 pivot) — [D-gate-3](/ops/decisions) 은 e-sign 코어를 **모두싸인이 흡수**(integrator)하고 직접-build 서명엔진을 Phase-2 로 defer 했으나, 운영자가 *"모두싸인의 기능을 직접, 암호등급으로 구현"* 지시 → gate Phase-1 = **gate-native 직접-build 암호 서명엔진**(작동 self-hosted, 이 세션 빌드+검증). **무엇이 빌트됐나**: ① **CMS/PKCS#7 CAdES-BES** detached 서명(RSA-2048+SHA-256), `DocumentSigner` trait(**HSM-ready**) + `SoftwareSigner`(self-signed X.509) — 적대적 리뷰(위조 도달경로 없음) + openssl 양방향 호환, 7 tests. ② **RFC3161 신뢰 타임스탬프 → CAdES-T**(hand-rolled ASN.1, 단일 clean dep; default DigiCert RSA TSA, 한국 prod = Koscom/CrossCert config-swap), 9 tests + live round-trip. ⑤ **워크플로 배선**: 시행(execute) 시 `esign.gate` actuator 가 승인된 결정을 **server-seal**(canonical decision bytes → CAdES-T), append-only **`esign_seal`** evidence 테이블(전자문서법 §5 custodian) 저장. 신규 **`verify_seal`** MCP tool(서명+무결성+타임스탬프 검증). e2e: 결재→seal→verify valid, 저장 CMS openssl cross-verify 통과, DB tamper→integrity fail. **법적 근거(전자서명법 2020, 공인폐지)**: 사서명(private/internal e-sign)은 **실지명의 + 서명의사**면 유효 — gate 의 경우 **실지명의 = 인증된 gate identity**(내부 서명자), **서명의사 = 결재 승인 행위** → 운영 라이선스 불요·합법. **유지된 경계(여전히 Phase-2/하드닝)**: 외부 서명자 **본인확인 = 방통위 지정 본인확인기관** 통합(정보통신망법 §23-3, 자가구축 불가) = **진짜 Phase-2** · HSM(cryptoki/PKCS#11, abstraction ready) · PAdES 서명-PDF 컨테이너(③④ typst deterministic render + pyHanko) · production 키관리(dev = ephemeral key) = **하드닝**. **red-team P0 해소-또는-명시**: 직접-build 를 막았던 P0(HSM·HTML→PDF 결정성·custody)가 — 결정성은 **canonical bytes 로 now**(typst PDF 는 later), HSM 은 **abstraction ready**(trait), custody 는 **esign_seal + RFC3161 TSA** 로 해소-또는-명시. **MUST NOT(불변)**: 자체 CA·per-user cert·자체 TSA·"공인/인증" 주장 — gate 는 **단일 서버 서명 identity**(ModuSign-recipe 모델: per-user cert 없이 서버가 봉인). **스택**: RustCrypto `cms`/`der`/`rsa` + RFC3161 + DigiCert TSA. 갱신: [/architecture/governance](/architecture/governance) §6 재작성 + [para-os §84](/architecture/para-os) footnote + [D-gate-1](/ops/decisions) Phase-1 e-sign 행. 정합: [D-gate-1](/ops/decisions)(scope) · supersede [D-gate-3](/ops/decisions)(e-sign 경계). | 2026-06-07 | | D-gate-1 | **gate = 한국형 결재+e-sign+법무 거버넌스 제품 — narrowed Phase-1 확정(경로 A), 야심 5종 Phase-2 defer** (2026-06-06 결정 → 코드 착수 직전; 마스터플랜 [B-bp-decision-pipeline-esign](/ops/backlog), 상세 [/architecture/governance](/architecture/governance)) — gate = 신규 **Rust+Postgres** 서비스(포트 41xx: pg4100/blue4110/green4111/proxy4112, origin `axe.axelabs.ai/gate`), 한국형 **내부 전자결재 + e-sign + HTML 문서 + 법무검색**을 AI-driven 결정→실행 파이프라인으로. **동기**: 7라운드 설계 후 7-agent red-team 이 풀설계(결재+직접 e-sign+법무 RAG flywheel+공개 SaaS+결제+멀티테넌트 = 1인이 동시에 짓는 6~7 규제제품)에 **NO-GO**(11 P0 / 16 P1 / 8 누락) 판정 — 11 P0 가 **전부 야심 5종에 거주**. → **경로 A(narrowed Phase-1)** 채택: 11 P0 를 *해소가 아니라 defer 로* 회피. **Phase-1(확정·착수 가능)**: ①결정로그(record≠process, [D-gate-2](/ops/decisions)) ②결재워크플로 상태기계(기안→DOA→승인/반려/보류→시행) ③document = HTML spine→PDF 렌더 ④**서명 signature** — *2026-06-07 [D-gate-5](/ops/decisions) 로 갱신*: 모두싸인 통합(provider=modusign, [D-gate-3](/ops/decisions))에서 **gate-native 직접-build 암호 서명엔진(CAdES-T server-seal + esign_seal + verify_seal)** 으로 전환, 모두싸인 = optional alternative provider 로 강등 ⑤actuation ledger(멱등+reconcile) ⑥Blueprint `gate.decision` citation mirror. **AXE-internal only**, **pgvector-KR**(Pinecone 아님), 법무=**내부검색만**(결재 자동주입 X, [D-gate-4](/ops/decisions)). 결제·멀티테넌트 **없음**. **Phase-2+(각자 규제/substrate 게이트, defer)**: 외부 서명자 **본인확인기관** 통합(정보통신망법 §23-3, [B-gate-bonin-id-contract](/ops/backlog)) — *직접 e-sign 엔진 자체는 [D-gate-5](/ops/decisions) 로 Phase-1 빌트; 잔여 하드닝 = HSM·PAdES-PDF·prod-key* · Pinecone 법무 flywheel(+PIPA) · 공개 SaaS(+tenant_id RLS substrate·self-IdP) · 결제(포트원/토스/Paddle, +전자금융/전자상거래) · cross-tenant 축적(+PIPA 가명정보). 빌드게이트 = [B-gate-legal-counsel](/ops/backlog) + [B-gate-phase2-gates](/ops/backlog). **착수 순서**: docs-first(D-gate-1~5 + governance.mdx) → `cp -R cortex gate` scaffold(auth 3-branch ladder·RLS·blue/green 상속) → 7테이블 migration(+`esign_seal`) → 상태기계 → **`esign.gate` actuator**(gate-native CAdES-T server-seal + verify_seal, [D-gate-5](/ops/decisions) 빌트) + 모두싸인 alt-provider stub(optional). 기준 = [para-os §7](/architecture/para-os) refine, [D-bp-para-1](/ops/decisions) governance clause 확장. | 2026-06-06 | | D-gate-2 | **결정 record SoR = gate `decision` 테이블, Blueprint = `gate.decision` citation mirror — D-bp-para-1 governance clause refine** (2026-06-06) — [D-bp-para-1](/ops/decisions)/[para-os §7](/architecture/para-os) 은 결정로그를 *"Blueprint core primitive(가로)"* 로 뒀으나 = AXE-internal 전제. **standalone 외부제품 요구 + 3축 survey(SoR경계·auth·멀티테넌트)** 가 refine: **record SoR = gate** `decision` 테이블(완료 workflow + 서명 HTML문서 + cert·authority·actor·decided_at·basis citations·supersedes, append-only). **Blueprint = mirror**: citation kind `gate.decision`(artifact·Area 가 cite) + 후일 visibility-gated mirror 테이블 — optional consumer 1종, 외부고객 = OFF(gate 단독 완결). **근거(grounded)**: [/architecture/artifacts](/architecture/artifacts) "MCP backend = sole owner, data 중복 0" · [/architecture/data](/architecture/data) cross-service = 직접 DB read 금지·명시적 신호채널(pg_notify/outbox)만 · **[D-cortex-9](/ops/decisions) = service-owns-SoR + Blueprint visibility-gated mirror 이미 빌트**(template 기성) · [para-os §7](/architecture/para-os) e-sign cert 를 citation 으로 = pre-authorize. → round-1 의 "record→Blueprint core" 가 오히려 platform 패턴(service-owns-SoR) 이탈, standalone 요구가 아키텍처가 가리키던 곳을 드러냄. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 | | D-gate-3 | **actuation ledger(결정→실행 멱등 일관성) + e-sign = 모두싸인 integrator(gate = 전자문서법 §5 evidence custodian); 직접-build 서명엔진 = Phase-2 gate** (2026-06-06) — ⚠️ **e-sign 부분은 [D-gate-5](/ops/decisions)(2026-06-07) 가 supersede** — 직접-build 암호 서명엔진이 Phase-1 으로 당겨져 빌트됨(아래 e-sign 경계는 역사 기록). actuation ledger 부분은 유효. 승인된 결정 → actuator 호출(`frame.post_journal`/`index.register_deal` 등, [para-os §7](/architecture/para-os) "Area 서비스 = actuator"). **actuation 테이블** = 실행 ledger(actuator_kind·payload·**idempotency_key=uuidv5(workflow+seq+actuator+payload_hash)**·state pending/inflight/done/failed·result_ref·reconciled_at) + **reconcile 워커**(frame-worker 패턴, pg_notify). **e-sign 경계(Phase-1 = INTEGRATE, [para-os §84](/architecture/para-os) 와 정합)**: signature.provider=`modusign` — 본인확인·타임스탬프·증거능력 코어를 모두싸인이 흡수, gate 는 **전자문서법 §5 evidence custodian**(서명문서+메타[작성자·수신자·송수신/서명 일시] 무결성 보관 + consent 기록, immutable audit store). cert ID 만 citation 참조(복제0). **⚠️ 직접-build 서명엔진(자체 PKI 서버서명·PAdES/XAdES·HTML→PDF 결정성[sealed-bytes=원본]·해시봉인·공개검증포털·감사추적인증서·HSM/KMS·서명키 DR/escrow·대법원 2017도13263 custody·본인확인기관 직통합) = Phase-2 gate**(red-team P0 #3/#7/#10) — 정당화였던 KISA §8 인정조차 "MVP 불요". **MUST NOT(Phase 무관)**: 자체 CA·per-user cert·자체 TSA·"공인/인증" 주장. **멱등 보강**: 모두싸인 actuator 도 provider-side idempotency 토큰 필요(gate idempotency_key 가 외부 이중실행 못막음, red-team P0 #5); saga 보상(e-sign+결제+분개) = 결제 도입 Phase-2. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 | | D-gate-4 | **법무 모듈 가드레일 = 변호사법 §109/§34 검색·정보 한정(자문 금지); Phase-1 = pgvector-KR 내부검색만, flywheel = Phase-2** (2026-06-06) — gate 법무모듈 = **검색·요약·번역·템플릿(로폼: user-blank, 기계적)만**. **하드룰 변호사법 §109(형사)·§34(5)**: 법률상담/사안별 자문/대리/결과예측-as-advice **금지**, **공개 "AI 법률상담" 금지**(AI대륙아주 7개월 폐쇄·징계 선례), 변호사 노출 시 정액광고만(로톡 모델, 알선료/성공보수/광고수익배분 금지). 모든 출력 "**법률 정보, 자문 아님**" disclaimer + cite = **retrieval-verified**(존재 + 시행일/선고일 temporal 강제). **결재 자동주입 차단**(AI 기안이 법령/판례를 auto-approve 경로에 주입 X — red-team P0 #2). **Phase-1 data**: 국가법령정보 OPEN API(open.law.go.kr, 이용제한없음·무료) + data.go.kr 판례(상용 OK) + HF `korean_law_open_data_precedents`(openrail), 대법원/헌재+법령 grade(하급심=상용 walled 제외). SoR = gate Postgres, index = **pgvector-KR**(Pinecone 아님). **Phase-2 gate**: Pinecone+Cohere rerank+Gemini embed flywheel(LLM비용 사용자부과·DB 복리) + cross-tenant 축적 = **PIPA 처리근거·가명정보·국외이전 자문**([B-gate-legal-counsel](/ops/backlog)) 선결 — `lbox_open`/`kcl`=CC BY-NC=유료제품 금지. B2B/internal-use 유지 = 최저위험. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 | | D-ops-44 | **Vault 비밀 주입 = SSH 환경 raw-bw keychain-free (`axe vault unlock` 금지)** (2026-06-09, `/Users/axe/CLAUDE.md` 강제 규칙으로 박제) — 이 머신 = AXE Mac mini, Claude Code 세션은 **SSH(loopback) 위에서** 돌고 운영자는 Windows 에서 `ssh axe@100.127.210.30` (Tailscale) 로 접속. macOS 는 **SSH 세션이 GUI login keychain 에 쓰는 것을 차단**("User interaction is not allowed") → vault 비밀을 *넣는* 경로의 keychain 의존이 전부 막힘. **금지(SSH 에서 전부 실패)**: (1) 운영자에게 `axe vault unlock` 시키기 — keychain-cache 단계가 SSH 에서 crash. (2) `axe secret push/pull/check` 로 비밀 PUT — `_vault_env` 가 keychain 세션만 읽음(`security find-generic-password -s axe.vault.session -a ai@axellc.com`) → SSH 에서 "vault session not found". (3) osascript `display dialog` — GUI 다이얼로그라 원격/Windows 운영자에게 안 보임(운영자-타이핑 비밀은 `read -rs`). **정답 = keychain-free raw-bw**, 운영자가 자기 인터랙티브 셸(Windows `ssh axe@…` 또는 Mac 터미널)에서 직접 실행. 에이전트는 **ASCII-only·주석 없음·`bw unlock` 을 단독 한 줄**로 공급(한 블록 붙여넣기는 unlock 프롬프트가 다음 줄을 비밀번호로 삼켜버림). 단계: ① `export NODE_EXTRA_CA_CERTS=/Users/axe/.axe/vault/certs/rootCA.pem`(Vaultwarden private CA — 필수) ② **단독 줄** `export BW_SESSION="$(bw unlock --raw)"`(master-pw 프롬프트; TTY-flaky 폴백 `read -rs PW; export BW_SESSION="$(BW_PASSWORD="$PW" bw unlock --passwordenv BW_PASSWORD --raw)"; unset PW`) ③ raw `bw create`/`bw edit` 로 Login item — **name** = `customers.yaml` 의 `services..secrets[].vault` 경로(예 `gate/axe/jwt-secret`), **username** = env var 이름, **password** = 스크립트 안에서 생성/읽은 값(`$(openssl rand -hex 32)`/`$(cat key.pem)`/상수 — echo·history·argv·agent-context 어디에도 안 남김) → `bw sync` → `unset BW_SESSION`. **검증**: 에이전트는 검증 불가(세션이 운영자 셸에만 존재) → 운영자의 `created/updated` + `synced` 출력이 곧 확인, `axe secret check` 호출 금지(keychain 필요). **keychain 세션이 필요한 곳 = deploy/ship 시점 한정**(`axe ship`/`secret pull` 이 그때 keychain 읽음): SSH 에서 `read -rs KCPW; security unlock-keychain -p "$KCPW" ~/Library/Keychains/login.keychain-db; unset KCPW`(headless unlock, GUI 없음) → `axe vault unlock` → `axe ship`. 비밀을 *넣는* 작업엔 불요. 정합: [D-ops-17](/ops/decisions)(vault SoT) · [D-ops-40](/ops/decisions)(osascript hidden-dialog — GUI 세션 전제라 SSH 에선 본 raw-bw 가 대체). | 2026-06-09 | | D-ops-43 | **운영자 원격 데스크톱 = RustDesk(Direct IP) over Cloudflare Access — Google Remote Desktop 대체, Tailscale 비의존** (2026-06-05 결정 → Phase 1 구축 중) — GRD(Google relay 경유 + Google 의존) 를 자체 트러스트 경계로 대체. **전송**: RustDesk 를 Direct IP 모드(자체 relay/ID 서버 미사용)로 axe-macmini 에서 구동 + `axelabs-tunnel`(d8efecdd) 에 TCP ingress (`rdp-axe.axelabs.ai → tcp://host.docker.internal:`) 추가 → **`ssh-axe` ([D-ops-39](/ops/decisions)) 와 동일 Cloudflare Access 패턴**. 데스크톱 = `cloudflared access tcp --hostname rdp-axe.axelabs.ai --url localhost:` 후 RustDesk 클라이언트 127.0.0.1 직결; 모바일 = Cloudflare WARP 등록 (`cloudflared access` CLI 가 폰에 부재). **인증 (2-phase)**: Phase 1 = **Microsoft Entra 직결 IdP** (즉시 가동, Blueprint 코드변경 0; 복구 채널이라 플랫폼 비의존이 오히려 견고 — 순환 의존 회피). Phase 2 fast-follow = **Blueprint OIDC 主 + Entra break-glass 副** ([B-blueprint-oidc-third-party-rp](/ops/backlog)) — 현 Blueprint OIDC (`src/lib/platform-oidc.ts`) 가 public·PKCE-only·exact-match 콜백 화이트리스트라 제3자 confidential RP (Cloudflare Access) 를 못 받음 → 콜백 허용 + client_secret 처리 + id_token/userinfo 완결성 확장 필요 ([D-axe-idp-1](/ops/decisions) Phase 2 영역). **Tailscale 배제 근거**: 인터랙티브 운영자 접속의 트러스트·전송은 Cloudflare Access 단일화 (ssh-axe 동형). Tailscale ([D4](/ops/decisions)) 은 backend (배포 push + 백업 ring) 전용 — RustDesk+Tailscale 안 함. **비용 $0** (Cloudflare Zero Trust Free ≤50 user; Spectrum 유료 경로 미사용). **macOS 26 Tahoe 함정**: RustDesk 에 화면 녹화 + 손쉬운 사용 권한 1회 수동 부여 필수 (SIP 보호, CLI 불가) + 주기적 재확인 nag. **거부된 대안**: (a) RustDesk+Tailscale — Tailscale 비의존 방향 역행. (b) RustDesk public relay / 자체 hbbs·hbbr — privacy + 추가 인프라. (c) Cloudflare Spectrum — 유료. (d) Blueprint 主 from start — 플랫폼 OIDC 코어(5 서비스 의존) 선확장 부담 → working RD 지연. (e) macOS 화면 공유(VNC) — Apple 코덱 성능 열위 (RustDesk 코덱 우선, 권한 마찰 수용). 본문: [/architecture/topology](/architecture/topology) + [/ops/inventory](/ops/inventory). **정정 (2026-06-05 동일 결정 진화): `{customer}.axelabs.ai/vnc` 경로 요구 → RustDesk(raw TCP) 폐기, 브라우저 VNC(Apache Guacamole) 전환.** path 라우팅 = HTTP 전용이라 TCP 불가 (ssh-axe flat 과 동일 D-ops-39 제약); Cloudflare 네이티브 browser-render VNC 도 hostname-bound (path 미지원, 2026-06 docs 확인) → 진짜 HTTP 앱 **Guacamole** 를 `/frame` 처럼 path 로. origin = macOS Screen Sharing(:5900) ← guacd ← guacamole webapp ← tunnel `http://host.docker.internal:` ← Access(path-scoped, Entra). 이득 = 브라우저 클라이언트리스(모바일 ✓ — WARP 갭 해소) + 커스터머별 표준 path 템플릿. 트레이드오프 = RustDesk 코덱 → Apple VNC 코덱(관리/제어 충분). 초기 RustDesk-flat(`rdp-axe`, Access app `0653d6ef`) 자원 철거(2026-06-05). Phase 2 Blueprint SSO 는 Guacamole(HTTP)가 `Cf-Access-Authenticated-User-Email` 헤더 passthrough 로 RustDesk 보다 용이. **★ 최종 결론 (2026-06-05, 실측 후): GRD(Google Remote Desktop) 존치, 자가호스팅 대체 보류.** noVNC `/vnc` 를 e2e 구축·검증했으나 **Apple 레거시 VNC 코덱이라 체감 사용 불가** (Retina 프레임버퍼 + RFB + CF 다단 홉). RustDesk = 코덱 우수하나 네이티브 앱(브라우저/path 불가). **GRD 우위 = WebRTC/VP8 비디오 코덱을 *브라우저 안에서* + 네트워킹 셋업 0 + 모바일 — 이 조합을 자가호스팅으로 매칭하는 솔루션이 현재 없음** (Guacamole/noVNC/Cloudflare browser-render 전부 `:5900` 에 RFB 라 코덱 천장 동일; Mac 은 RDP 미지원이라 Guacamole RDP 경로도 불가). `/vnc` 실험 자원(noVNC 스택 `/Users/axe/axelabs/vnc/` + Access app `eae049e7` + ingress path rule + vault VNC pw) 철거 대상. **de-Google 미래 경로** = (a) RustDesk web client 성숙 시 self-host (코덱+브라우저 양립), (b) WebRTC 기반 자체 클라이언트(tether). 그 전까지 운영자 원격 데스크톱 = **GRD** (project_devenv "GRD 유지" 와 정합 — 본 결정이 실측으로 재확인). **수용한 trade-off**: GRD = 폐쇄소스(Google 독점 Chrome Remote Desktop — 호스트 코드 일부만 Chromium `src/remoting/` 공개, 릴레이·시그널링·디렉터리는 Google 비공개·self-host 불가) + 트래픽 Google relay 경유 = sovereignty 비용을 de-Google 솔루션 부재 시점까지 **의식적으로 수용**. | 2026-06-05 | | D-ops-42 | **배포 SSOT 아키텍처 — `origin/main` SHA = 배포 진실원천** (worktree 작업격리 + build-from-SHA + deploy lock + 스테이지 상태기계 + migration-validation 게이트) (2026-06-04, DESIGN LOCKED · additive rollout) — **원칙**: `origin/main 의 commit SHA = 배포의 SSOT`. working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영. docs·backlog 가 이미 matrix-postgres SSOT 규율([D-matrix-3](/ops/decisions))을 따르듯, 같은 규율을 **배포**로 확장. **동기 (실측 incident)**: N개 동시 Claude Code 세션이 repo 당 1 working tree + 1 `main` 공유 → (a) 다른 세션이 먼저 push 하면 `main` diverge → non-fast-forward push 거부, (b) 서로 다른 feature 의 uncommitted WIP 가 한 tree 에 누적 → 누구도 자기 slice 만 clean commit 불가, (c) `axe deploy` 가 dirty working tree 에서 이미지 빌드 → uncommitted 코드 prod 누출 + push 전 배포 가능. 2026-06-04: 이미 배포된 blueprint fix (commit `b4067504`) 가 main diverge + 타 세션 WIP 로 수 시간 push 불가 → 운영자 수동 reconcile (stash→rebase→push). 추가 발견: 운영자 `axe` CLI (364KB) 자체가 **버전 미관리·in-place 편집** = 같은 병의 가장 깊은 사례. **컴포넌트 A–F**: (A) **작업 격리** `axe work ` = 세션별 git worktree (`~/.worktrees//`) off origin/main, 정규 repo 는 fast-forward 전용 "main mirror" 강등(손편집 금지) → 공유 tree 경합·WIP 혼재·stash 더미 **발생 불가능**. (B) **SHA 에서만 빌드** = deploy 가 working tree 아닌 pushed SHA 의 clean checkout(`git worktree --detach`/archive)에서 빌드, 이미지 태그 `:`, push 안 된 SHA 배포 거부 → deploy-before-push + dirty-tree 누출 **구조적 불가능**. (C) **Deploy lock** = 서비스당 배포 직렬화 (matrix-postgres `pg_advisory_lock` 우선, 파일락 fallback) → 두 세션 동시 blue/green flip 불가. (D) **Provenance + drift** = 컨테이너 `org.axe.git_sha` 라벨, `axe health`/`axe host` 가 color 별 실행 SHA + origin/main 대비 drift 표시 → "라이브가 git 과 일치하나" 항상 답. (E) **잘못된 경로 제거** = `axe deploy` 가 tree 입력 폐기, `git push origin main` 직접 = pre-push 훅 + GitHub branch protection 으로 **기계적 차단**(현 honor-system 대체), `axe ship` 만 main 전진. (F) **통합 ship** = `axe ship`(worktree 에서): fetch → origin/main rebase → SHA build+test → push(main ff) → 그 SHA deploy(lock 하) → mirror ff → shiplog. 오늘 손 reconcile 전부가 한 명령. **스테이지 상태기계**: `built → canary(passive color, 트래픽 0) → [migration-validation 게이트] → live(active flip) → previous(직전 active = 즉시 롤백 타깃)` — blue/green 이 이미 canary/live/previous 3스테이지 제공. **migration-validation (안전 척추)**: 스키마 마이그레이션 포함 릴리스는 flip 전 prod DB 의 ephemeral clone 에 마이그레이션 적용·검증 (`axe drill`/backup 스냅샷 재사용). blue/green 이 **DB 공유** → 깨지는 스키마 변경은 카나리 *불가능* (green 용 마이그레이션이 blue 즉시 오염). 이 게이트가 blue/green 이 못 막는 유일한 위험 차단. 대상: blueprint 대기 migration 2개 (add_user_entra_oid, add_entity_legal_name), hive alembic. **rollout (additive)**: 전부 기존 동작과 공존 → `--dry-run` + passive-color 카나리(flip X) 검증 → cutover(기본값 flip + 가드 활성, escape hatch 포함) 게이트 → 되돌림 가능, 진행 중 WIP 보존. **채택 안 함**: 별도 staging *환경* (staging.axelabs.ai + 독립 DB/터널) — 단일 Mac mini 에서 새 실패유형 0 + 비용·parity 부담만, canary + migration-validation 이 실위험 커버. 작업추적 새 "스테이지" 축 — roadmap/backlog/updates 가 이미 작업 파이프라인. 본문: [/ops/runbook/deploy-ssot](/ops/runbook/deploy-ssot). 정합: [D-ops-16](/ops/decisions) (release-gate `axe ship`) · [D-matrix-3](/ops/decisions) (matrix SSOT) · [Blue/green deploy](/ops/runbook/deploy). | 2026-06-04 | | D-axe-idp-1 | **Blueprint = 플랫폼 인가서버 (OIDC Provider) — 로그인 1회로 전 서비스** (2026-06-03 결정 → **Phase 1+2 LIVE 2026-06-04**; 현행 구현 SSOT [/architecture/platform-identity](/architecture/platform-identity)) — D-axe-cli-1 의 외부 인증을 "신규 Entra 앱" 대신 **Blueprint 중앙 관장**으로 (본질적 선택). Blueprint 가 Entra 를 상위 federate(인간 SSO 1회) + **자체 플랫폼 토큰 발행**(서명 JWT + JWKS + authorize/token/register + OIDC `.well-known`) → **모든 서비스가 Blueprint 신뢰**(iss=blueprint + JWKS 검증). 한 토큰이 전 서비스 동작 (서비스별 audience juggling 불필요). frame/cortex 의 per-service OAuth-RP 프록시(D-ops-14/15)를 Blueprint 하나로 **통합**(net-new 아닌 consolidation). 거버넌스(per-user/tenant scope·revoke·audit) Blueprint 중앙. **D-axe-cli-1 토큰모델 정정**: 인터랙티브=Blueprint 발행 토큰(Entra-direct/HS256 아님), HS256 부트스트랩은 헤드리스/cron 잔존. **보안 핵심 → 설계문서 + 신·구 병행 비파괴 cutover**. Phase: ①Blueprint OIDC-OP + `axe login`(loopback PKCE→Blueprint) + frame 가 Blueprint 신뢰(모델 증명) ②index/hive/cortex/matrix trust 이전 + proxy 폐기 ③거버넌스/감사 + headless device-code. **상태: Phase 1+2 LIVE (2026-06-04)** — Blueprint OIDC-OP(discovery·jwks·authorize·token·register·revoke) + RS256 키(vault) + `axe login` loopback-PKCE + frame·hive·cortex·index·matrix + Blueprint 자체 MCP **6개 서비스 전부 Blueprint 토큰 신뢰**(영속·e2e 검증). claude.ai→Blueprint OP 이전 + per-service 프록시 폐기, Phase 3(인가 중앙화·감사 UI·device-code·키 회전 자동화)만 잔여. | 2026-06-03 | | D-axe-cli-1 | **외부/멀티에이전트 접근 = 고객 CLI (gh 모델) + 토큰 로그인, MCP 커넥터 대체** (2026-06-03 결정 → **LIVE**: 고객 CLI 배포·`axe login`·전 서비스 호출 가동; 사용자 매뉴얼 [/services/cli](/services/cli)) — 외부 인원이 서비스별 MCP 커넥터를 개별 등록하던 방식의 "느리고 불편" 본질 = ① 1인당 N개 커넥터 등록·인증 마찰 ② 커넥터당 도구 스키마 **100개+** (frame ~54 + HR ~40 + blueprint ~20 실측) 가 매 턴 컨텍스트 적재 → 모델 턴 지연·오선택. 해소 = **단일 CLI** (`~/axe-cli`). 근거: 셸 조합성(pipe/loop/batch 1회 실행, 모델 왕복 0 — MCP 도구는 고립 호출) + **에이전트 무관** (Claude Code·**Codex**·Cursor·Gemini CLI·cron·CI; CLI = syscall/ABI 층, vendor lock-in 0). **CLI-only** (웹 Cowork 용 Skill 폐기 — 외부 인원 Claude Code 표준화). **토큰** = `axe login --token` → OS Keychain → `Authorization: Bearer` (헤드리스/Codex 가능, 브라우저 OAuth 회피). 토큰 실체 = **HS256 부트스트랩 JWT** — frame `frame..jwt_secret` 의 **이미-LIVE** 패턴 표준화; index 는 `INDEX_JWT_SECRET` 설정만 있고 **미배선**(Entra RS256 만 검증) → 배선 필요, hive/blueprint/cortex/matrix 도 PAT 경로 추가(P2). **배포 = Blueprint 설치 시 in-place 번들** (Blueprint=구동 시스템; `axe onboard` 가 테넌트 PAT 발급→Keychain = zero-touch 인증 + control-plane 자기설정) + standalone(curl/pip) 보조. **스코프 분리**: 운영자 `axe`(vault/deploy/secret/customers) ≠ 고객 CLI(업무 명령·토큰 스코프). 기존 `axe mcp` connector-catalog 는 외부용 대체. **현행 정정**: 토큰 모델은 [D-axe-idp-1](/ops/decisions) 로 갱신됨 — 인터랙티브 = Blueprint 발행 RS256 플랫폼 토큰(`axe login` SSO), HS256 부트스트랩은 헤드리스/cron 잔존. 배포 = `https://.axelabs.ai/cli`(+`/cli.ps1`) served + `axe self-update`(매 실행 conditional GET). 잔여 backlog: headless device-code(`B-axe-cli-device-code`). | 2026-06-03 | | D-ops-1 | folder-as-context — `.mcp.json` per folder | 2026-05-08 | | D-ops-2 | JWT 평문 디스크 금지 — Keychain + launchctl env | 2026-05-08 | | D-ops-3 | `sub` = identity, `permissions` = entitlement (분리) | 2026-05-08 | | D-ops-4 | tool surface 분리 — operator/employee tools | 2026-05-09 | | D-ops-5 | v1 permission matrix — read/write/close/admin | 2026-05-09 | | D-ops-6 | end-to-end MCP test 자동화 | 2026-05-10 | | D-ops-7 | `.mcp.json` is the spec location, NOT `.claude/settings.json` ⚠️ | 2026-05-11 | | D-ops-8 | vault-per-entity canonical distribution | 2026-05-13 | | D-ops-9 | Vaultwarden 가 canonical secret store | 2026-05-14 | | D-ops-10 | Phase 1 SSO via Vaultwarden OIDC (Timshel) | 2026-05-14 | | D-ops-11 | Timshel digest pin (mainline 무한 미지원 회피) | 2026-05-15 | | D-ops-12 | sso_nonce manual SQL fix | 2026-05-15 | | D-ops-13 | external_id mapping (Microsoft oid ↔ Vaultwarden user) | 2026-05-15 | | D-ops-14 | frame OAuth-RP (Microsoft Entra ID direct) + Blueprint /onboard | 2026-05-17 | | D-ops-15 | OAuth proxy 시도 (dormant — Anthropic Connector 한계로 미사용) | 2026-05-21 | | D-ops-15.5 | axe.axelabs.ai 도메인 검증 + Application ID URI 형식 URL | 2026-05-21 | | D-ops-16 | docs drift 강제는 release-gate (`axe ship`), pre-commit hook 채택 X | 2026-05-21 | | D-ops-17 | secret = vault SoT + deploy-time pull (`customers.yaml` 매니페스트), runtime vault 호출 X | 2026-05-21 | | D-ops-18 | compose env_file 단일 출처 (environment block 의 secret 중복 금지 + 하위 dir compose 의 `.env` symlink) + `axe secret pull` merge-mode (config 보존) | 2026-05-21 | | D-ops-19 | Azure secret rotation = az cli + `--append` (overlap window) + app owner 운영자 명시 prereq | 2026-05-21 | | D-ops-20 | **7-stack 응집** — service 별 1 stack 으로 통합 (proxy/console/docs/tunnel 분산 → frame·hive·blueprint·artemis·mysrt·vault·axelabs). cross-cutting infra (caddy/tunnels/docs/console) = `axelabs` stack. Portainer 12 stack → 7 stack. 본질·퀄리티·안전 우선. | 2026-05-22 | | D-ops-21 | **`frame register-entity` 자동 chart seed** — entity 등록 시 표준 chart (KSME/KGAAP/KIFRS) 자동 apply. 이전엔 별도 `apply-chart` 누락 시 모든 분개가 `account_code not found` 로 실패하던 운영 함정 해소. | 2026-05-22 | | D-ops-22 | **frame 펀드 도메인 지원** — `shared.entity` 에 `entity_kind` (corporate/kip/kvf) + `fund_meta` JSONB + `closed_at` 컬럼 ADD. `shared.entity_relationship` 확장 (`ownership_numerator/denominator/unit` — % 폐기, `kind` ENUM 에 `gp_managed_fund`/`lp_invested_fund` 추가). `shared.cross_journal_link` 신설 (AXEV↔fund mirror 분개 pair 무결성). `register-entity` CLI 에 `--kind`/`--fund-meta` flag. AXEV 창업기획자 산하 KIP/KVF 다수 결성 수용. 회계 원칙: AXEV↔fund = K-IFRS 1110 통제 미달 → 운용 관계 (parent-child 아님). `frame_meta` schema 신설 폐기 (기존 `shared` 확장이 자연 + 명명 충돌 회피). alembic `0008_shared` migration + frame-mcp blue/green rebuild. | 2026-05-22 | | D-ops-23 | **frame RFC 9728 resource-level metadata path fix** — `/frame/mcp/.well-known/oauth-protected-resource` (application_id_uri 의 sub-path, RFC 9728 정확 path) 가 frame middleware 의 unauthenticated allowlist 누락으로 401 반환. claude.ai Connector / Anthropic MCP client 가 본 path fetch 시도 → OAuth challenge metadata 못 받음 → Reconnect silently 실패. `9b43845 feat(mcp): OAuth-RP dispatch + RFC 9728 endpoint` 시점부터 latent, D-ops-22 deploy (frame-mcp blue/green rebuild) 가 stored token invalidate 하면서 발현. fix: `http_server.py` 의 `_PUBLIC_PATHS` + `inner.router` 에 resource-level path 추가 (handler reuse `_oauth_protected_resource`). 양쪽 path (server-level `/frame/.well-known/...` + resource-level `/frame/mcp/.well-known/...`) 모두 200. commit `4f5dfcc`. | 2026-05-22 | | D-ops-24 | **Vaultwarden SSO + Microsoft Entra ID — `email_verified` 면제** — Entra ID id_token 에 `email_verified` claim 부재 → Timshel fork 가 "Your provider does not send email verification status. ... `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`" 오류로 거부. 첫 employee SSO 시도 (`taehun.kang@axellc.com`, 2026-05-22) 에서 발현. fix: `/Users/axe/.axe/vault/docker-compose.yml` 의 `axe-vaultwarden.environment` 에 `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"` 추가. SSO_AUTHORITY 가 single-tenant URL (`122fb574-...`) 이므로 외부 tenant 사용자 진입 불가 → 추가 검증 면제 안전. + Vaultwarden UI 함정 (이메일-first 기본 화면이 SSO Identifier 입력 칸 가림) 은 `/vault/#/sso` 직링크 안내 (troubleshooting.mdx). | 2026-05-22 | | D-ops-25 | **MCP service 의 startup probe 강제** (D-bp-mcp-3 cross-cutting 일반화) — 모든 DB 종속 MCP service 의 Starlette lifespan 안에서 첫 traffic 받기 전 `SELECT 1` probe 실행. broken DB / malformed URL 시 lifespan fail → uvicorn 시작 실패 → healthcheck never green → `axe deploy SERVICE-mcp` blue/green swap 자동 거부 (broken container 가 active promote 못 됨). `/health/ready` endpoint 가 있어도 그건 request-time 검증이라 부팅 직후 broken DB 가 active 가능. **현재 상태**: blueprint-mcp ✅ 적용 ([PR #353](https://github.com/soohunkang/blueprint/pull/353)). **frame-mcp ✅ 적용 (2026-05-22, commit `3c7ca6a`)** — `src/frame/mcp/http_server.py` 의 outer Starlette lifespan 을 `@asynccontextmanager` wrapper 로 변경 (sync `Engine.begin()` + `SELECT 1` + log + `async with inner.router.lifespan_context(...)` bridge). startup log `"startup probe: DB reachable, schema accessible (D-ops-25)"` verified. hive-mcp **미적용** — frame pattern 1:1 미러 (5-10 분, `hive/src/hive/mcp/http_server.py`). mcp-server-checklist § 8 #16 으로 항구화. | 2026-05-22 | | D-ops-26 | **Vaultwarden JIT user provisioning 활성** — D-ops-24 후속 incident. SSO 인증 통과 후 신규 user (`jinwoo.han@axellc.com`, 2026-05-22 10:01) 가 `/#/set-password-jit?identifier=AXE` 페이지에서 "An error has occurred. Failed to retrieve the invitation" 차단. vaultwarden log `Failed to retrieve the invitation` from `vaultwarden::api::core::accounts`. 원인: `SIGNUPS_ALLOWED=false` 가 JIT user 생성 차단 → invitation fallback 도 (`INVITATIONS_ALLOWED=false`) 없음 → 양쪽 모두 닫혀서 신규 employee 영구 차단. fix: `SIGNUPS_ALLOWED: "true"` 단독. `INVITATIONS_ALLOWED=false` 유지 (manual invitation flow 사용 안 함, JIT 로 통일). **D-ops-27 (operator 결정) 와 함께 읽을 것** — `SSO_ONLY` 는 false 유지 (emergency MP fallback 보존). | 2026-05-22 | | D-ops-28 | **bw CLI 다운그레이드 2025.7.0 + 업스트림 vault migration 백로그** — 한진우/강태훈 AXE org 멤버 초대 자동화 시 brew 의 bw CLI 2026.4.1 이 `TypeError: Cannot read properties of null (reading 'toWrappedAccountCryptographicState')` 로 login 단계 crash. [bitwarden/clients #19413](https://github.com/bitwarden/clients/issues/19413) 와 동일 — CLI 2026.x 가 server ≥ 2026.x API 응답 필드 요구하는데 Timshel fork 의 최신 stable 은 `1.34.1-6` (upstream Vaultwarden 1.34.1, 2025-07-15 빌드). Timshel ghcr 에 1.34 이상 image 없음 (1.36.0 git tag 만 존재). fix: `brew uninstall bitwarden-cli && npm install -g @bitwarden/cli@2025.7.0` (Timshel 1.34.1-6 동시기). `/Users/axe/.axe/vault/invite-members.sh` 가 멤버 invite + auto-accept + confirm 한 번에 처리. **상위 결정 변경**: D-ops-11 의 "Timshel digest pin" 사유 (mainline Vaultwarden SSO 미지원) 가 2026-05 upstream 1.36.0 의 native SSO 도입으로 해소 → backlog `B-vault-upstream-migration` 등재 (dani-garcia/vaultwarden:1.36.0 으로 이전, schema migration + SSO 환경변수 매핑 + 데이터 보존 검증). 이전 완료 후 bw CLI 최신 line 복귀 가능. | 2026-05-22 | | D-ops-27 | **Vaultwarden `SSO_ONLY=false` 유지 + `ORGANIZATION_INVITE_AUTO_ACCEPT=true`** (operator setup 마무리, 2026-05-22). (a) **SSO_ONLY=false**: D-ops-26 초안의 `SSO_ONLY=true` 폐기. Microsoft Entra 장애 / SSO 설정 오류 시 operator 가 master password 로 emergency 진입 가능해야 함 (`axe ship docs` 와 동일 원칙 — single-vendor lock-out 회피). trade-off: 외부에서 `axe.axelabs.ai/vault` 직접 password 가입 가능 (org collection 접근 X). mitigations: SSO_AUTHORITY single-tenant 가 외부 tenant SSO 진입 차단 + org member invite operator-only. **TODO** (backlog): 비정상 user row INSERT 감지 cron (`B-vault-anomaly-cron`). (b) **ORGANIZATION_INVITE_AUTO_ACCEPT=true**: org member 초대 시 사용자 측 "Accept invitation" 클릭 단계 자동 통과. operator 의 "Confirm" (org key 를 user public key 로 암호화 전달) 만 남음. 안전: invite 자체는 operator-only 이고 user pool 은 SSO 통과한 tenant user 만 가능. | 2026-05-22 | | D-ops-29 | **Dual-identity for 강수훈 — `ai@axellc.com` (automation, AAD `7f110c52-...`) + `soohun.kang@axellc.com` (human Owner, AAD `e35e6778-...`)** — founding operator 가 두 identity 보유. 분리 원칙: (a) **`ai`** = 자동화 / bot / cron / agent identity. Graph token 발행, 직원 proactive DM, frame/hive agent run. 사람 손작업 X. (b) **`soohun.kang`** = 본인의 사람 작업 identity. Teams 응답, vault item 등재, blueprint admin UI. Vault AXE org 에서 둘 다 Owner + access_all=1 (DB UPDATE `users_organizations.atype=0, access_all=1` 적용 2026-05-22). Blueprint User.role 둘 다 admin. **이유**: bot 메시지가 human sender 로 표시되거나 vice versa 시 incident response 추적 혼란 (D-ops-23 의 mcp_client_invalid 디버깅 사례 참조). audit trail 명확화 + 신원 misuse 회피 + sender identity propagation 본질 원칙. | 2026-05-22 | | D-ops-30 | **Bot proactive DM endpoint — `/api/admin/broadcast-dm`** (blueprint) — operator (ai@axellc.com identity) 가 직원 N명에게 동일 메시지 일괄 1:1 Teams DM. Auth: `Bearer `. Body: emails 배열 + text + contentType (text|html). 각 email 마다 (1) User.aadObjectId lookup → (2) Graph POST /chats oneOnOne (bot + user, idempotent — same pair 반환) → (3) POST /chats/CHAT_ID/messages. Pattern: `cron-failure-alert.ts:postAlertToAdmin` 재사용 (single → fan-out). 사용 사례: vault onboarding 안내, 플랫폼 공지, 운영자 broadcast. agent-context reply 는 `graph_chat_message_post` (Blueprint MCP) 그대로 사용. | 2026-05-22 | | D-frame-lottecard-scraper | **롯데카드 거래내역 자체 스크래퍼 + 멀티카드 적재** (2026-05-29) — CODEF 등 외부 집계 API 미사용, 자격증명으로 롯데 법인 포털 직접 로그인하는 헤드풀 Playwright 스크래퍼 (`frame scrape ` 제네릭 + 롯데카드 첫 커넥터, host-only). 법인관리자 ID/PW (웹 보안키패드) → 승인내역 **개인정보 미포함(=이메일/인증 생략)** 신청 → 비동기 생성 → CP2 .xls 다운로드 → ingest. 법인전체 export 가 법인의 모든 카드를 담으므로 `ingest_lotte_card` 를 **행별 계정 resolve** 로 수정 (마스킹 `5105-****-****-*042` ↔ 기존 실번호 계정 `5105-5400-0199-9042` 와 와일드카드 매칭하여 링크, 실번호 보존; 신규 카드는 새 계정). 비밀 = `frame/axe/lotte-portal-{username,password}` (vault, host-only — MCP 컨테이너 제외). 스케줄은 DB-driven ([D-frame-scrape-schedule](/ops/decisions)). 함정: axec id=3 의 기존 701행이 혼합 카드 (과거 첫-행-버그) — 별도 정합성 정리 대상. | 2026-05-29 | | D-frame-scrape-schedule | **스크래퍼 주기 = DB(MCP 조정) + launchd 단순 tick** (2026-05-29) — 주기를 plist 하드코딩하지 않고 `shared.scrape_schedule`(KST cron) 에 두고 MCP `set_scrape_schedule`(raw cron 또는 daily/weekly/monthly 프리셋) 로 조정. MCP 컨테이너는 호스트 launchd 를 못 건드리므로 launchd 는 `com.frame.scrape-tick`(08–21시 매시 :15) 단순 tick → `frame scrape-due` 가 croniter 로 due 판정해 해당 source 만 실행 + last_run/next_due 갱신. cron 은 KST (anchor=last_run else created_at → 갓 만든 weekly 는 첫 진짜 fire 에 실행, 첫 틱 즉발 X). croniter+Playwright=host-only(`schedule.py`), cron 컴파일=pure(`cron.py`, 컨테이너 공용). 마이그레이션 `0009_shared`. 신규 MCP 도구 3개 → frame-mcp rebuild 필요. **SLA(다운타임 catch-up)**: 놓친 실행은 복구 후 1회 따라잡기 + look-back 을 now−last_run(상한 365일)까지 확장해 그 기간 backfill (row_hash dedup, 손실 0), `RunAtLoad` 부팅/wake catch-up, MCP `overdue` 가시화. seed: lottecard/axec weekly Mon 08:10. | 2026-05-29 | | D-ops-33 | **`axe vault bootstrap {customer}` CLI 신설** — D-ops-24/26/27 4-key 적용을 코드로 항구화. customer-onboarding.mdx 의 D+1 수동 단계를 1 명령으로 축소. 동작: SSH (or local for axe) 로 `/Users/{ssh_user}/.axe/vault/docker-compose.yml` 읽음 → 4 key 진단 (idempotent) → 누락만 patch (anchor: `SSO_SIGNUPS_MATCH_EMAIL`) → force-recreate `axe-vaultwarden`. Default = dry-run, `--apply` 로 실 패치. **검증**: `axe vault bootstrap axe` (이미 적용 — "bootstrap 불필요" 정확 detect) + `axe vault bootstrap realchoice` (file 부재 시 깔끔한 에러). 후속 ([B-onboard-cli-vault-bootstrap](/ops/backlog)): Vault Organization 자동 생성 + 첫 admin invite (현재 web UI 수동). | 2026-05-22 | | D-ops-31 | **axep entity 신설 (액스파트너스 유한책임회사)** — axe customer 의 3번째 entity 추가 완료 (2026-05-22). frame `register-entity --id axep --kind corporate --accounting-standard ksme` (chart auto-seed 54/59 accounts, D-ops-21) + hive `register-entity --id axep` (frame mirror). `customers.yaml`: entities 배열 `["axec","axev","axep"]` + frame/hive PII passphrase secret 매니페스트 2개 (`frame/axe/pii-passphrase-axep`, `hive/axe/pii-passphrase-axep`) 추가. user_entity_map: ai/cfo/soohun 만 `["axev","axec","axep"]` (axep 참여), taehun/jinwoo 은 axep 제외 유지. **Deploy 완료**: PII passphrase 2개 vault push (`axe secret push`) + frame blue/green swap (active=green:3711, blue:3710 next passive) + hive sequential force-recreate (green → 6s → blue). axec/axev 영향 0 (PII passphrase entity별 분리 + lazy-load). 자동화: `/Users/axe/.axe/vault/deploy-axep.sh`. **부수 발견 함정**: `axe secret push` 가 `BW_SESSION` env var 무시 + macOS Keychain (`axe.vault.session` / `ai@axellc.com`) 만 읽음 → 자동화 시 `security add-generic-password -U` 로 keychain 갱신 단계 필수. | 2026-05-22 | | D-ops-34 | **Onboard 1-input 모델 — Phase A (vault unlock = 세션 prerequisite) + Phase B (umbrella 명령 1 줄)** — 신규 customer (realchoice 등) 배포 시 운영자 D-day 입력 = **axe 명령 1 줄**. 가정: vault 가 미리 unlock 돼 있어야 함 (`bw unlock --raw` → Keychain `axe.vault.session` cache, 8시간). 이후 모든 `axe secret`/`axe onboard`/`axe deploy` 가 keychain BW_SESSION 자동 사용 — D-day 추가 prompt 0. **Phase B umbrella** = `axe deploy customer {customer} --from-pack pack.json --apply` ([B-onboard-umbrella](/ops/backlog)) — 내부에서 ingest + onboard (cloudflared+frame) + deploy blueprint + deploy hive 순차. **거부**: cron 기반 `bw unlock` keep-alive (vault 상시 unlocked = 머신 침해 시 전체 노출 = blast radius 큼). 권장 모델 = "출근 시 비번 1 회 입력 → 하루 1 입력" 보안/편의 최적점. customer IT 측 mirror = `az login` 1 회 + `axelabs-bootstrap.sh` 1 줄 (docs.axelabs.ai 만으로 자력 — [D-onboard-bootstrap-publish](/ops/decisions), [/partner/registration §Option A](/partner/registration#option-a--cli-1-줄-권장)). | 2026-05-23 | | D-ops-32 | **Vault collection 구조 v1 — 6 collection** (D-ops-9/27/29 종합 설계) — 기존 4 collection (Default empty, frame-jwt-axec/axev D-ops-14 잔재 each 1 item, frame-jwt-operators empty) → 6 collection: (1) **Platform — Service Secrets** (37 customers.yaml secrets, ai+soohun only) (2) **Platform — Infrastructure** (Cloudflare API, vault ADMIN_TOKEN raw, GitHub org admin, DNS, cert; ai+soohun only) (3) **AXEC** (4명 RW — KB/홈택스/Azure axec/세무사 access) (4) **AXEV** (4명 RW — KB/KSD/금감원/GP-LP portal/investee data room) (5) **AXEP** (ai+soohun only — GP 단독 출자/운영) (6) **Shared Tools** (4명 RW — Notion/Slack/공통 SaaS/도메인 admin/메일 alias). 기존 frame-jwt-axec + frame-jwt-axev 의 2 item → Platform — Service Secrets 로 이동. 기존 Default + frame-jwt-operators 삭제. 구체 마이그레이션은 운영자 web UI 권장 (SQL/bw 직접 manipulation 은 Vaultwarden cache 충돌 위험). 외부 인력 (회계사/LP) 합류 시점에 entity-level read-only 분리 검토. **Progress (2026-05-26)**: 6 collection 전부 신설 ✅. AXEC (`5b13e7e4...`) + AXEV (`6eb223cb...`) + AXEP (`89de0ab9...`) + Platform — Service Secrets (`1d794d29...`) + Platform — Infrastructure (`50f2705f...`) + Shared Tools (`68197247...`). 권한 부여: AXEC/AXEV/Shared Tools 4명 RW (ai+soohun manage=true / taehun+jinwoo manage=false), AXEP + Platform 둘은 ai+soohun only. `bw create org-collection` 자동화 검증 — D-ops-32 가 명시한 "운영자 web UI 권장" 은 완전 우회 가능. **영향 평가**: axe CLI `_vault_get` 가 name 기반 (`bw get password `), service deploy 가 `.env` 에 inject — collection 추가/이동/삭제로 MCP connector 깨지지 않음 (running connector 영향 0, 다음 deploy 시 lookup 도 무관). **AXE org role 확인**: ai@ + soohun.kang 모두 type=0 (Owner), taehun + jinwoo type=2 (User). **남은 작업 (별건)**: (a) 운영자 ai@ 의 personal vault 의 service secrets (axe secret list 의 ~37 path) 를 Platform — Service Secrets 로 이전 — 현재 ai@ only, bus factor 위험. (b) Default 의 3 items (`axe-macmini local: soohun.kang`, `axe-macmini local: taehun.kang`, `github PAT: ai@axellc.com`) 검토 — github PAT 은 Platform — Infrastructure, 나머지 2 은 personal vault 가 적절. (c) frame-jwt-axec/axev/operators 의 2 item (`taehun-kang-axec-2026-05-17`, `ai-operator-2026-05-16`) 검토 후 적절한 신설 collection 으로 이동. (d) 옛 4 collection (Default + frame-jwt-*) 삭제. → 신규 backlog [B-vault-collection-migration-v1](/ops/backlog). **2026-05-26 권한 정정 fix 적용 완료**: `bw create org-collection` 의 users 배열 처리 server-side 함정 (Owner skip + manage=0 강제 — 자세한 root cause = [B-vault-org-perm-3-quirks](/ops/backlog)) 으로 6 collection 모두 ai+soohun 권한 누락 또는 partial 이었음. SQLite 직접 `users_collections` 일괄 INSERT/UPDATE + ai+soohun-only collection 의 taehun/jinwoo entry DELETE 로 본 결정의 권한 모델 (4명 RW vs ai+soohun only) 그대로 정합 복원. cipher delete/move 는 `*/admin` HTTP endpoint 직접 호출이 정공법 (bw CLI 의 cipher.permissions.delete client-check 우회). | 2026-05-22 | | D-nemotron-1 | **`/services/nemotron-personas` 카탈로그 등재 (placeholder)** — 트루비아 측 기존 자산 (Docker SSE :8771, NVIDIA Nemotron-Personas 데이터셋 1M+ 페르소나 풀) 이 docs 미등재였던 갭 해소. 보고서 Q5 confirm 후속 (2026-05-23 RE^2). 본격 본문은 D+14 (2026-06-15) 트루비아 측 라이선스/origin 회신 후. 라이선스 cross-customer OK 면 공식 카탈로그 + Blueprint MCP registry, X 면 realchoice 전용 (`customers.realchoice.private_services[]` 신설 검토). 현 시점 docs 노출은 가시성 + 운영자 SPOF 회피 목적. | 2026-05-23 | | D-magnet-tenant-map-1 | **`customers.yaml > customers.{customer}.service_tenant_map` 신설** — magnet 의 service-internal RLS `tenant_id` (integer) 와 AXE customer ID (string) 의 매핑 SSOT. `MAGNET_TENANT_ID=1` (realchoice) 유지, 향후 axe 가 magnet 사용 시 `tenant_id=2` 부여로 충돌 회피. 트루비아 측 보고서 Q6 confirm 후속 (2026-05-23 RE^2). `/services/magnet#tenant-id-mapping` 본문. 향후 stream / 기타 service-internal id 도 동일 패턴 확장. axe ship 이 manifest 에서 자동 주입은 `B-magnet-tenant-env-injection` 후속. | 2026-05-23 | | D-onboard-bootstrap-publish | **`axelabs-bootstrap.sh` 를 docs.axelabs.ai 의 raw 로 노출** — 신규 customer IT 가 docs link 만 받고 자력 완료 가능한 상태 (별도 안전채널 메시지 + `realchoice_entra_id_setup_v4.md` 의존 폐기). `/Users/axe/.axe/bootstrap/axelabs-bootstrap.sh` → `/Users/axe/axelabs-docs/public/axelabs-bootstrap.sh` 복사 (17749 bytes, SHA-256 `7d2d607136e5387c6c9368bf8529d4ec0e219f8eb7bab45b57590a00838210cc`). partner/registration §Option A 가 docs 안에 다운로드+검증+실행+회신을 모두 포함하도록 재작성. 함께 신설: partner/macmini-prep.mdx (Tailscale 가입 + SSH key 회신 + 절전 OFF + Docker 6 단계) + partner/domain-prep §A (axelabs.ai zone = 운영자 책임, 귀사 IT 1 분 한 줄 전달) §B (귀사 corporate domain TXT 가이드) + partner/handoff JSON pack 양식 (`axelabs-bootstrap/v1` schema) + partner/index 의 4-step 흐름. customers.yaml realchoice.sso.apps 에 blueprint 슬롯 사전 추가 (bootstrap.sh 의 apps.blueprint 출력 흡수 대비). 운영자 측 1-shot umbrella ([D-ops-34](/ops/decisions)) 는 여전히 backlog — 운영자 D-day 명령 수 5→1 줄이는 작업은 별도 추진. customer-facing 의 docs-only 자력 완료는 본 결정으로 달성. | 2026-05-23 | | D-ops-41 | **SSH client-side automation: NSSM/서비스 폐기, user-context Scheduled Task + VBS wrapper 채택** — `/onboard/ssh-access` 실전 셋업 (Soohun Kang Windows 11, 2026-05-27) 도중 6 함정 발견 후속. (1) cloudflared TCP forward 와 token renewal 모두 **user-context Scheduled Task** 로 통일. TCP forward 는 **VBScript wrapper 호스팅** 으로 0-flash hidden + 무한 재시작 보장 (`wscript.exe` + `Do ... sh.Run ..., 0, True; WScript.Sleep 5000; Loop`). Token renewal 은 **self-rescheduling PS1** — JWT `exp` claim 파싱 후 `exp - 5min` 에 한 번만 fire (24h 주기 task fire 2~3회), `Set-ScheduledTask` 가 자기 trigger 재예약. (2) **거부된 대안**: (a) NSSM/SYSTEM 서비스화 — JWT 토큰 user-scoped (`%USERPROFILE%\.cloudflared\`) vs SYSTEM 의 `%SystemRoot%\System32\config\systemprofile\.cloudflared\` 격리 → 무한 `failed to acquire app token lock` 루프 + SYSTEM 의 desktop session 부재로 자체 login 도 무한 "Waiting for login..." (함정 #10). (b) NSSM 사용자 계정 실행 — 패스워드 평문 저장 + Microsoft 계정/Windows Hello 사용자 UX 저하 + 계정 패스워드 변경 시 서비스 정지. (c) Cloudflare Service Token — [D-ops-29](/ops/decisions) dual identity audit 와 충돌 (SSO email log 없어짐). (d) Access app `session_duration` 30d 연장 — 갱신 빈도만 줄어들 뿐 자동화는 별개. (3) **PowerShell `-WindowStyle Hidden` 폐기** (daemon 류) — Windows 가 hidden flag 적용 전 수십 ms 콘솔 노출 → 매 task fire 마다 깜빡임 잔상 (함정 #13). `wscript.exe` 호스팅이 0-flash. (4) **Task `-RestartCount` 의존성 0** — FAILURE (non-zero exit) 에만 발동이라 cloudflared 정상 종료 (exit 0) 시 silent 중단 → 며칠 후 SSH 끊김 (함정 #14). VBS wrapper 안의 `Do ... Loop` 가 어떤 종료 코드든 5초 뒤 재기동. (5) **Renewal task `-RunLevel Highest` 필수** (함정 #11) — `Set-ScheduledTask` 는 elevated 권한 필요, Interactive 만으로 `Access is denied`. (6) **SSH use case 의 active logon session 전제** — "logon 전에도 살아있음" 이 필요 없음 → user-context Scheduled Task 가 NSSM 보다 깔끔 (패스워드 저장 0 / 토큰 캐시 자동 공유 / 본인 끌 UI 0 / 가시적 창 0). **결과 = 사람 클릭 0회 조건부** — Microsoft "로그인 상태를 유지" sticky session 켜져 있으면 renewal 시 SSO 자동 통과. Entra conditional access 가 sticky 차단하면 갱신 시점 1회 클릭 필요 (정책 영역, client 우회 불가). **검증 환경**: Windows 11 + cloudflared 2026.5.1 + OpenSSH for Windows 9.5p2 / LibreSSL 3.8.2. **참조**: [/onboard/ssh-access § TCP forward 자동화](/onboard/ssh-access#tcp-forward-자동화-windows-user-context-scheduled-task--vbs-wrapper) / [§ Token 자동 갱신](/onboard/ssh-access#token-자동-갱신-windows) / [§ 함정 #9~#14](/onboard/ssh-access#함정-정리). | 2026-05-27 | | D-ops-40 | **axe.3 vault release plan — 2 patch (Owner skip + cipher.permissions) + KDF rotation procedure + `axelabs-ai/vault` GHA pipeline 구축** — [D-ops-37](/ops/decisions) axe.2 deploy 후 발견된 [B-vault-org-perm-3-quirks](/ops/backlog) (3 함정) + [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) 의 모든 근본 진단 (2026-05-26 timshel/vaultwarden source clone read-only 진단). **Patch A — 함정 1 (Owner skip on collection user save)**: `src/api/core/organizations.rs` 의 **2곳 동일 fix** — `post_organization_collections` (line 522-524) + `post_organization_collection_update` (line 685-687) 에서 `if member.access_all { continue; }` 3 라인 제거. 결과 = Owner role 도 `CollectionUser::save` 명시 entry 생성 → 권한 체크 정합 + cipher.permissions.delete 계산 정상. **Patch B — 함정 3 (cipher.permissions field missing)**: `src/db/models/cipher.rs::to_json` 함수 line ~239 부근 `"edit"` / `"viewPassword"` 옆에 `"permissions": {"response": null, "delete": , "restore": }` 추가. `` 계산 = `Membership::find_by_user_and_org(user_uuid, org_uuid).await` 결과의 `atype <= Admin` OR `cipher.user_uuid == Some(user_uuid)` OR (`get_access_restrictions().manage == true`). Bitwarden mainline schema backport. **Patch C — 함정 2 (cipher edit collectionIds silent partial-revert)**: **patch 안 함** — vaultwarden bug 아닌 mainline design (collectionIds 변경은 `POST /ciphers/{id}/collections` 별도 endpoint). 운영 룰: bw CLI 의 `bw edit item` 으로 collectionIds 변경 불가, 대신 `bw move` 또는 `/admin` endpoint 직접 호출 ([B-vault-org-perm-3-quirks](/ops/backlog) 의 운영 임시 룰 영구). **Patch D — SSO→MP wrapped MP**: **patch 안 함** — axe.2 의 response shape backport 가 이미 충분. 기존 user (axe 측 4명) 의 user.akey 가 옛 KDF/wrapping schema 로 생성된 게 원인 (realchoice 측 신규 가입 user 는 axe.2 적용 후라 새 schema, 정상). **Fix = KDF rotation runbook** (운영자 셀프, patch 영역 외): 각 user 가 MP 단독 로그인 → Settings → Account → Security → Master password change (same value OK) + KDF type PBKDF2 → Argon2id → save → vault items re-encrypt → logout → SSO→MP 정상. axe 측 4명 (ai/soohun/taehun/jinwoo) 차례로. realchoice 측 영향 없음. **Build pipeline (axelabs-ai/vault repo, 현재 빈 상태)**: `build/build.sh` (clone timshel `1.34.1-6` tag + `patches/*.patch` 일괄 git apply + `docker buildx --platform linux/amd64,linux/arm64 --push ghcr.io/axelabs-ai/vault:`) + `.github/workflows/build.yml` (push.branches=main 자동 빌드 `main-` rolling tag + `workflow_dispatch` input tag 로 release 빌드 `1.34.1-6-axe.3`) + `build/patches/0001..0004.patch` (axe.2 의 prelogin/password + AccountKeys + axe.3 의 organizations Owner-skip + cipher permissions). **Deploy 흐름**: (1) `axelabs-ai/vault` repo 의 build/ + workflows/ + patches/ commit. (2) workflow_dispatch → GHCR push. (3) `axe-macmini`: `docker compose pull vaultwarden && docker compose up -d` (compose.yml image 라인 = `ghcr.io/axelabs-ai/vault@sha256:` 로 핀, [D-ops-37](/ops/decisions) 의 axe.2 ImageID 주석 → axe.3 digest 로 갱신, 이전 digest rollback 주석 보존). (4) `realchoice-macmini`: 동일 pull + up (customer self-deploy, [D-ops-customer-sovereignty](/ops/backlog) 원칙). (5) axe + realchoice 양쪽 검증 = collection 생성 (Owner entry 명시 확인) + cipher delete (bw CLI 정상 동작 확인) + SSO→MP (KDF rotation 후 검증). **현금 비용 0** (GHCR + GHA runner 무료 한도 안, image 460MB, build ~15분/회). **운영자 시간 5~9h** (patch 작성 1h + build pipeline 1~2h + 빌드/검증 1h + KDF rotation 4명 30분 + Truvia 안내 30분). **AGPL 라이선스 고려**: Vaultwarden = AGPL-3.0. fork 의 patch 배포 시 source 공개 의무 — `axelabs-ai/vault` GHCR push public 가능 + source 공개 같이. private 유지 원하면 patch 만 사적 운영 (배포 안 함, GHCR private + 본인 macmini 만 pull). 본 결정 = **public 모드** (source 공개 = patch 공개, GHCR public, 라이선스 정합 + 무료 GitHub plan 한도 무관). **Progress (2026-05-26 KST, ~3.5h 실 진행)**: (i) `axelabs-ai/vault` repo PUBLIC 전환 ✅ (조직 정책상 GHCR package 는 PRIVATE 유지). (ii) realchoice-macmini 의 기존 build pipeline (build/build.sh + Dockerfile + README + workflow + 2 axe.2 patches) 그대로 vendor + axe.3 patch 2개 추가 ✅: **patch 0003** `src/api/core/organizations.rs` line 522 + 600 + 685 의 `if member.access_all { continue; }` 3 라인 제거 (plan 의 "2곳" 외 line 600 `post_bulk_access_collections` 추가 발견 — 동일 root cause, 3곳 모두 patch). **patch 0004** `src/db/models/cipher.rs::to_json` line 374 (실 line, plan 의 ~239 와 다름) 의 `edit/viewPassword` 옆에 `permissions: {response,delete,restore}` 추가 + line 161 의 `_` → `manage` 로 바꿔 `get_access_restrictions` 결과 reuse (DB round-trip 절약). `can_delete` = collection manage flag OR org Owner/Admin (`atype <= 1`) — `Membership::find_by_user_and_org` 한 번 호출. (iii) 로컬 docker buildx (arm64, --load) 검증 ✅ — 5분 (M-series + cache), Rust 컴파일 정합 확인 (image `axelabs-ai/vault:axe.3-localtest` ImageID `sha256:e1ac1617...`). (iv) commit `799015b` push to axelabs-ai/vault main → GHA `build-vault-image` 자동 트리거 → 13분 multi-arch build → ghcr.io/axelabs-ai/vault:main-799015b push ✅. workflow_dispatch 는 axe-labs-ai PAT 의 actions scope 부족으로 403 — 대신 `docker buildx imagetools create` 로 `1.34.1-6-axe.3` alias 발행 (rebuild 없이, 3초). **manifest list digest** `sha256:a26208a0794acbc9a2807379ffba33c7478dbe8d41daed24893f7916a55aeada` (arm64 `sha256:d97a6ed...`, amd64 `sha256:402eeac...`). (v) GHCR auth: PAT (read+write:packages+workflow) 발급 → **AXE vault `Platform — Service Secrets / ghcr-axelabs-ai-pull-pat`** 저장 ([D-ops-ops-17](/ops/decisions) SoT 원칙 — keychain 안 씀). docker login 패턴: `bw get password ghcr-axelabs-ai-pull-pat \| docker login ghcr.io -u axe-labs-ai --password-stdin`. (vi) axe-macmini deploy: data tgz + sqlite 백업 (`data-pre-axe.3-20260526-131744.tgz`, `db-pre-axe.3-20260526-131744.sqlite3`) → `docker compose pull && up -d` → 60s healthy. compose image 라인 = `ghcr.io/axelabs-ai/vault@sha256:a26208a...`. (vii) **검증 3건 in-production** (13:19 KST): **VERIFY 1** = `bw create org-collection` (test 용) → SQLite `users_collections` 에 `ai@axellc.com\|0\|0\|1` row 명시 insert ✅ (axe.2 = zero rows). **VERIFY 2** = raw `GET /api/sync` → org cipher 의 `permissions: {delete:true,restore:true,response:null}` 정확 emit ✅ (bw CLI 2025.7.0 출력은 `null` — 옛 client 가 unknown field 파싱 못해 표시 안 됨, 새 client ≥2026.4 는 정상 읽음). **VERIFY 3** = `bw delete item` (test cipher) rc=0 ✅ (cleanup 후). (viii) external `https://axe.axelabs.ai/vault/alive` HTTP 200 (0.28s). (ix) [B-vault-fork-build-pipeline](/ops/backlog) ✅, [B-vault-org-perm-3-quirks](/ops/backlog) ✅. (x) **잔여 = KDF rotation 4명 (ai/soohun/taehun/jinwoo)** — 운영자 본인 MP 입력 필수라 자동화 불가, [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) runbook 참조. realchoice 안내 별도 (customer-sovereignty — 직접 pull, vault 에서 PAT 받아감). **(xi) ai@ KDF rotation 검증 완료** (Master password change + rotate encryption key 만으로 SSO→MP unlock 정상 — KDF 변경 없이 akey 재wrap 만으로 충분 확인). **(xii) post-deploy 정책 layer 박제** ([/architecture/vault-policies](/architecture/vault-policies) 신설): **(a)** server env `SSO_AUTH_ONLY_NOT_SESSION=true` 적용 — SSO 가 인증만, session lifecycle = Vaultwarden 자체 30일 idle refresh token. trade-off = Entra 비활성/제명 ≤30일 lag (AXE 4명 규모 OK), benefit = SSO+MP 재입력 월 ~1회. **(b)** Org Policies 4종 API 적용 (Owner ai@ token 으로 PUT) — MasterPassword(min 12, complexity 1), PasswordGenerator(len 20), SingleOrg, PersonalOwnership. **(c)** MaximumVaultTimeout 시도 HTTP 400 — Timshel source 의 `// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)` 확인. Bitwarden commercial license 라 Vaultwarden 영구 미지원, `SSO_AUTH_ONLY_NOT_SESSION` 이 가용 영역 안 best 대체. 3 layer 모델 (server env + Org Policies + per-client preferences) 통합 박제. **(xiii) 4명 KDF rotation 전부 완료 → D-ops-40 전체 종결** (2026-06-04): ai (5/27) → soohun → taehun (5/29) → jinwoo (6/4) 순서. **본질 발견 = KDF type dropdown 변경 불필요** — Master password change + ☑ "Also rotate my account's encryption key" 체크박스 만으로 `user.akey` 재wrap 충분 (plan 의 "KDF PBKDF2 → Argon2id" 보다 간소). DB: ai 는 Argon2id (Phase B 까지 진행), soohun/taehun/jinwoo 는 PBKDF2 유지 — **kdf_type 안 바뀌어도 akey 재wrap 만으로 SSO→MP fix 동작 확인**. 4명 모두 SSO→MP unlock 정상. jinwoo last_login 5/23→6/4 점프 = 완료 신호 + 운영자 구두 확인. 운영 도구 부산물: [/onboard/vault-setup](/onboard/vault-setup) playbook (taehun 피드백 6건 반영) + [/ops/runbook/operator-broadcast](/ops/runbook/operator-broadcast) (Teams DM 1:N) + [/architecture/vault-policies](/architecture/vault-policies). [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) ✅. **D-ops-40 전 작업 (build pipeline + axe.3 patches + GHCR + deploy + 검증 3건 + 정책 layer + 4명 rotation) 종결.** | 2026-05-26 | | D-ops-39 | **무료 Universal SSL 의 1-level wildcard 한계 = flat-hostname 컨벤션 강제 (ACM 유료 회피)** — Cloudflare 무료 plan 의 `*.axelabs.ai` Universal SSL cert 는 1단 서브도메인만 cover. 2단 (`ssh.axe.axelabs.ai`) 은 SAN 미일치 → edge default cert fallback → 클라이언트 `tls: handshake failure` (Windows: `SEC_E_ILLEGAL_MESSAGE 0x80090326`, macOS: `sslv3 alert handshake failure`). 강태훈 Windows 의 `cloudflared access login https://ssh.axe.axelabs.ai` (2026-05-25) 에서 발현, MAX agent 진단 (axelabs.ai chat) → 운영자 forward → 본 결정. **Fix**: 신규 1단 hostname `ssh-axe.axelabs.ai` (DNS CNAME → 동일 tunnel `d8efecdd`, ingress 규칙 clone, Access app `b903d8cd` self_hosted_domains 추가). 검증: `curl -v https://ssh-axe.axelabs.ai` → `*.axelabs.ai` cert SAN 매치 + Access 302 OK. 옛 `ssh.axe.axelabs.ai` 와 vestigial `*.axe.axelabs.ai` 와일드카드 (이번 한계 violation) 는 [B-ssh-axe-flat-hostname-sunset](/ops/backlog) 로 grace period 후 일괄 삭제. **거부된 대안**: (a) Cloudflare Advanced Certificate Manager $10/cert/월 — 1개 hostname 위해 유료 전환 부당. (b) `axe.axelabs.ai` 별도 zone 등록 — NS delegation/zone 이원화 overhead 과잉. (c) 강태훈 Tailscale 우회 — 일반 직원 권한 범위 외 + 권한 확장 정책 필요. (d) 클라이언트 cert 검증 우회 — 보안 위반 + 다음 직원도 같은 함정 재현. **컨벤션**: HTTPS 노출되는 모든 hostname 은 `{name}.axelabs.ai` 1단 (`ssh-axe`, `docs-axe` 등 flat). sub-context 필요하면 path (`axe.axelabs.ai/frame`). 본 룰 [/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39](/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39). | 2026-05-26 | | D-ops-38 | **외부 service 2FA 는 vault item TOTP 필드, AXE 내부 vault 계정 자체 2FA skip (Entra SSO 가 MFA 역할)** — AXE 가 사용하는 외부 services (KB/홈택스/AWS/GitHub/카드사/세무사 SaaS/etc.) 의 2FA 코드 는 Vaultwarden item 의 `login.totp` 필드 (Vaultwarden 무료 지원, Bitwarden premium 의 self-host 등가) 에 저장 → vault unlock 시점에 client (web/ext/native) 가 자동 6-digit 생성. **AXE 내부 vault 자체 user 2FA 는 활성 안 함** — Microsoft Entra SSO 가 이미 MFA 역할 (Entra ID 의 conditional access + Authenticator app 정책). Vault 의 SSO→MP unlock 흐름이 동작하면 (현 [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) 해소 후) 그 자체가 multi-layer. **AI agent 자동화 활용**: Claude Code / axe CLI / 기타 운영 자동화가 외부 service 로그인 시 TOTP 필요한 경우 `bw get totp ` 또는 향후 `axe secret totp ` ([B-axe-secret-totp-cli](/ops/backlog)) 으로 vault 에서 자동 조달 — 운영자 phone 의존 제거 + bus factor 0. **운영 룰**: 외부 service 2FA 등록 시점에 (a) phone Authenticator 앱은 사용 안 함, (b) QR/secret/`otpauth://` URI 를 vault item 의 TOTP 필드에 직접 저장, (c) phone 분실/교체 시 손실 없음. **trade-off**: vault 가 단일 trust boundary — 뚫리면 외부 service 2FA 도 같이 뚫림. 따라서 vault 의 SSO/MP/(향후) 2FA layer 가 더 중요해짐. AXE 내부 vault 계정 자체 2FA skip 결정은 **Entra SSO 가 살아있을 때** 만 유효 — SSO 가 깨진 현재 상태 ([B-vault-axe.2-sso-mp-incomplete](/ops/backlog)) 에서는 MP single-factor 단계라 임시 위험 가시. SSO 흐름 복원 시점에 자동 mitigation. | 2026-05-26 | | D-ops-37 | **AXE-local Vaultwarden fork build — `ghcr.io/axelabs-ai/vault@sha256:a26208a0794acbc9a2807379ffba33c7478dbe8d41daed24893f7916a55aeada` (axe.3, 2026-05-26)** — Bitwarden client >= 2026.4 (Chrome extension + bw CLI) 와 Timshel 1.34.1-6 fork 사이 두 protocol 불일치 영구 fix. (1) Bitwarden client 가 `/accounts/prelogin/password` 로 split — Timshel 은 legacy `/accounts/prelogin` 만 노출 → 404 → 로그인 시작 불가. (2) `/identity/connect/token` 응답에 새 wrapped-variant 필드 (`AccountKeys` / `MasterPasswordUnlock`) 부재 → "toWrappedAccountCryptographicState" throw → unlock 실패. 패치 2개 모두 dani-garcia/vaultwarden mainline 에서 backport. realchoice tenant 가 2026-05-25 자체 build & deploy → axe-macmini 는 본 일 (2026-05-26) realchoice-macmini local image 를 docker save/load 로 import → `axe-vaultwarden` recreate (ImageID `sha256:11536845...`, arm64). prelogin/password endpoint 200 OK 회귀 검증 완료 (이전 404). **2026-05-26 추가 (axe.3 ramp, [D-ops-40](/ops/decisions))**: axelabs-ai/vault GHA pipeline 정식 구축 + axe.3 patch 2개 추가 (organizations Owner-skip + cipher.permissions) → image 라인 = `ghcr.io/axelabs-ai/vault@sha256:a26208a...` (manifest list, multi-arch). [D-ops-11](/ops/decisions) immutable digest 핀 정합 복원. **rollback (axe.2)**: 로컬 tag `axelabs-ai/vault:1.34.1-6-axe.2`, ImageID `sha256:11536845...`. **rollback (pre-axe.2)**: `ghcr.io/timshel/vaultwarden@sha256:a5d5ed0366abd84cc7443a79e52ffe4774671648dca5e1a5412fd91b77d4f692`. **follow-up**: (a) ~~`axelabs-ai/vault` 에 build/patches/ + GHA workflow 실 셋업~~ ✅ ([B-vault-fork-build-pipeline](/ops/backlog) Done). (b) 패치 2개 Timshel upstream PR 제출 ([B-vault-axe.2-patches-upstream-pr](/ops/backlog)). [B-vault-d-day-traps-2026-05-25 #12](/ops/backlog) 의 client-side bw 2025.7.0 pin workaround 와 별도 — 본 결정은 server-side 영구 fix. **vault-next 18-24개월 roadmap 의 bridge** (별도). | 2026-05-26 | | D-ops-35 | **`axe deploy hive {customer}` subcommand 신설** — frame blue/green 패턴 1:1 mirror. `cmd_deploy_hive` + `_hive_active_color` + `_hive_port` 헬퍼 + `HIVE_*` 상수 (compose, proxy=`axe-hive-proxy`, networks=`["hive_default","artemis_default"]`, alias=`hive-mcp`, drain=5s). 8-step: build passive → up -d → /health/ready 200 wait → add alias (양쪽 network) → caddy reload → edge verify (200/401 둘 다 정상 — JWTAuthMiddleware) → drain 5s → 옛 active alias 제거. argparse choices 에 `hive` 추가, dispatcher 분기. **검증**: `axe deploy hive axe --apply` 실 swap (blue:3810 → green:3811 active, edge `axe.axelabs.ai/hive/health: 200`, 무중단). hive 가 `axe ship hive` 의 manual_hint 만 남아있던 격차 해소. realchoice 6/1 deploy 가 진정 1-shot 가능 — `axe deploy customer realchoice --from-pack ...` umbrella ([D-ops-34](/ops/decisions)) 의 hive 단계도 자동. customer-onboarding.mdx 의 step 4 manual ssh 폐기. | 2026-05-23 | | D-ksme-1 | **KSME seed 판관비 10개 보강** — 5210 급여 / 5211 임원급여 / 5212 잡급 / 5213 퇴직급여 / 5214 복리후생비_4대보험 / 5215 통신비 / 5216 광고선전비 / 5217 도서인쇄비 / 5218 교육훈련비 / 5219 감가상각비. 한국 법인 필수인 급여 계정 자체가 없던 본질적 결손. | 2026-05-22 | | D-frame-3 | **`cash_flow_statement` 신규 계정 fallback 분류 (type + code prefix) — 명시적 dict 우선, 미매핑 계정만 규칙 적용** — CF 분류가 hardcoded dict (`INVESTING_ACCOUNTS`/`FINANCING_ACCOUNTS`/`OPERATING_*`) 라 신규 계정(1302 투자조합출자금·1303 매도가능증권·2104 주임종단기차입금)이 `unclassified_accounts` 로 남아 `reconciliation_diff≠0` → `verified=false`. Fix (`ops/statements.py`): 미분류 계정에 한해 fallback rule — **1300번대 자산(prefix `13`, type=asset) → 투자활동** (1301/1302/1303 + 향후 1300번대 신계정 자동 포함), **차입금성 부채(`BORROWING_LIABILITIES`={2104,2105}, type=liability) → 재무활동**. ⚠️ 2101 미지급금/2102 미지급세금/2103 부가세예수금/2106 가수금 은 `OPERATING_LIABILITIES` 라 영업활동 유지 — 21xx prefix 로 싸잡지 않고 차입금 코드만 explicit set (미지급금 재무 누출 차단). 명시적 dict 멤버는 기존 로직대로 처리 (기존 분류·테스트 불변, 회귀 0). reconciliation 계산을 fallback 분류 後로 이동. 라이브 axec 2026-01~05 CF: diff −195M → **0, verified=true** (1302 −225M·1303 −200M 투자, 2104 +230M 재무). | 2026-06-04 | | D-frame-2 | **evidence/source_file blob 은 항상 `.local/files` content-addressed 복사 + `storage_url` 은 portable (relative `//`)** — `local://` 참조 금지 (컨테이너가 host 경로 read 불가 + source 파일 rename/move 시 깨짐), CWD 의존 absolute 경로도 금지 (host `/Users/.../​.local/files/...` vs 컨테이너 `/app/.local/files/...` 불일치). 근인: `ingest/entity_meta.py`·`ingest/resolution.py` 가 blob 을 복사 안 하고 `local://{pdf_path.resolve()}` 만 저장 → 외부 OneDrive/Downloads 포인터라 컨테이너에서 blob 부재; `_store_file_locally`·`ops/evidence.py`·`hometax.py` 는 복사는 했지만 `str(dest.resolve())` 라 CWD 의존. Fix: `_store_file_locally` 가 portable url 반환 + 신규 `resolve_storage_path()` 가 read 시 `settings.file_storage_path` 기준 absolute 로 매핑 (portable/`/app`/`/Users`/`local://` 4형식 tolerant — legacy row 무중단). 모든 write-site (entity_meta/resolution/hometax/ops.evidence) 가 `_store_file_locally` 경유. read-site = `http_server.py` storage-integrity probe 가 `resolve_storage_path` 사용. Alembic entity migration `0020_normalize_evidence_storage_url` 가 blob 이 `.local/files` 에 실존하는 기존 row (axec 158 + axev 19 = 177) 를 portable 로 정규화 (blob 부재 row 는 절대 미변경·notice; blob 절대 삭제 안 함). | 2026-06-04 | | D-frame-1 | **pending_payroll: 분개 trigger = 실 거래 (matching principle)** — D-hive-26 의 즉시 분개 디자인 폐기. hive event → `pending_payroll` INSERT (예약). KB raw_tx → `match_pending_payroll` (date±3 + amount exact + name fuzzy) → 분개 자동 생성 + link. status: pending→matched/stale/cancelled. 회계 표준 (matching principle: 거래 발생 시점에 분개) 부합. 이중 분개 위험 0. | 2026-05-22 | | D-matrix-1 | **Matrix: Rust + native MCP (no Python SDK)** — 첫 Rust 서비스. MCP Streamable HTTP 직접 구현 (JSON-RPC 2.0 + axum). 모니터링 서비스 저메모리·단일 바이너리 적합. D-bp-mcp-1 (Python FastMCP) 과 별개 precedent | 2026-05-23 | | D-matrix-2 | **Matrix: WAN/인터넷 가용성 모니터링 + ISP 귀책 판별** — collector 에 `check_wan` (wan-gateway / wan-internet / wan-dns 프로브) 추가. gateway↑ + internet↓ ⇒ ISP/WAN fault 자동 태깅. 스키마 변경 0 (기존 `check_results` JSONB 재사용). 컨테이너 NET_RAW + iputils-ping, default interval 60→30 | 2026-06-03 | | D-matrix-4 | **netheal — 호스트 인터넷 자가치유 데몬** (matrix 보완) — 인터넷 지속 끊김 시 DNS플러시→DHCP갱신→WiFi바운스 사다리 자동 실행. ISP 장애(gateway↑·외부↓)면 백오프(WiFi 안 건드림). root LaunchDaemon (컨테이너 밖, 호스트 en1 제어). 자동 재부팅 기본 OFF. `~/axe-netheal/` | 2026-06-04 | | D-matrix-3 | **Matrix: backlog/roadmap/ship-log 구조화 SSOT** — backlog·roadmap·updates(ship log) 의 SSOT 를 hand-edited markdown → matrix-postgres(slug PK `B-foo`/`M1` 보존) 로 이전. matrix MCP 8 도구(`backlog_list/create/update/transition`·`roadmap_list/upsert`·`shiplog_append/list`)로 전 서비스 에이전트가 **멀티라이터-안전** read/write. docs 의 3 페이지는 **host-side 생성**(`axe ship docs` 가 matrix fetch→.mdx, matrix-git-push 모델 기각). known-gaps·decisions 는 narrative 라 markdown 유지. 동기 = git-markdown 멀티라이터 충돌(2026-06-03 세션 실측: known-gaps swept-commit·backlog 동시 +3). [ADR](https://github.com/axelabs-ai)=`matrix/docs/adr/backlog-roadmap-system.md`. **Phase 1(스키마 3테이블+MCP 8도구+import 파서) 구현·`cargo check`·import dry-run(206 backlog+16 ship+7 roadmap) 검증 완료, 미배포** | 2026-06-03 | | D-hive-1 | Hive HR backend 신설 — frame 패턴 mirror (schema-per-entity, OAuth-RP) | 2026-05-21 | | D-hive-21 | Payroll v2 — 한국 도메인 본질 7가지 (간이세액표 lookup · 비과세 분리 · 보수월액별 · 부양가족 · 정산 · 일할 · 보험별 라운딩). v1 단순 누진세 폐기. 첨부 급여대장 셀별 정확 일치. | 2026-05-21 | | D-hive-22 | 급여명세서 메일 발송 인프라 (PDF + 평문 본문) + 법정 8항목 (근로기준법 §48 ②) | 2026-05-21 | | D-hive-23 | M365 SMTP AUTH 의 deprecation 회피 → Graph API SendMail + client_credentials + DKIM/DMARC DNS (Cloudflare API token 자동화) | 2026-05-21 | | D-hive-24 | 메시지 템플릿 관리 — shared default + entity override + Jinja2 SandboxedEnvironment + StrictUndefined + partial unique index | 2026-05-21 | | D-hive-25 | Customize 강제 (`require_entity_override=True` — shared fallback 차단) + 발송·송금 audit log (`payslip_dispatch_log` + `payslip_send_log` with body_sha256) | 2026-05-21 | | D-hive-26 | **Cross-service event consumer = `CONSUMER-worker` 별도 컨테이너** (consumer-grouped, multi-channel LISTEN). frame-worker 1개가 hive_events 받음 + 미래 magnet/stream 채널은 dispatch table 추가만 (컨테이너 추가 X). N×N 폭증 회피 (N+M, M≤N). in-process async task (P2) 검토 후 책임분리·격리·blue/green swap 무관·crash 격리 우선으로 sidecar (P1) 선택. | 2026-05-22 | | D-hive-27 | **`employees.payroll_start_date` 단일 컬럼 분리** — `hire_date` 가 (i) 계약 발효일 (ii) 보수 개시일 (iii) 4대보험 가입일 3 의미 동시에 짊어지던 모순 해소. `payroll_start_date` Date nullable, NULL → hire_date fallback (정직원 90% 는 NULL → 자동). 임원계약 §2 (계약 시작) ≠ §6/§12 (보수·4대보험 시작) 케이스 대응. frame.executive_officer.appointed_date (= hive.hire_date) 정합 보존. 4대보험 가입일은 당분간 보수 개시일과 동일로 가정 — 다른 경우 발생 시 `social_insurance_enroll_date` 추가 검토. cycle.py 일할 base + employee_create/update MCP 옵셔널 입력 지원. alembic entity 0009. | 2026-05-29 | | D-hive-28 | **`compute_period` 시그니처에서 `employee_ids` 제거** — D-hive-20 원작자가 정정 경로를 분리 (full re-run = `compute_period`, 사후 정정 = `compensation_events.adjustment`, D-hive-17) 했음에도 후행 adjustment leg 미구현 + `employee_ids` 파라미터의 동작 ("period 전체 wipe + 일부만 INSERT") 이 운영자에게 "부분 정정" 직관을 줘 다른 직원 payslip 손실 위험. 단순 결함 아닌 의도된 분기 + 미완성 + 의미 모호의 누적. 시그니처 모호 파라미터 제거가 원작자 의도 정합. 골든 테스트 + MCP wrapper 사용처 0건이라 호환성 영향 없음. 후속: `compensation_events.adjustment` 정식 MCP 도구 구현은 별도 backlog. | 2026-05-30 | | D-hive-30 | **`compensation_plan_list` + `compensation_award_create` MCP 도구 신설 (M 트랙, D-hive-17 후행 leg)** — alembic 0004 의 plan/award/event 3-table schema 만 정의되고 등록 도구 미구현이라 운영자가 직접 SQL 박아야 했음. 본 결정에서: `compensation_plan_list(entity, plan_kind?)` (read) — plan_code 조회용. `compensation_award_create(entity, employee_no, plan_code, annual_amount_krw, effective_from, effective_until?, award_code?)` (admin) — employee/plan 존재 + active overlap 검증 후 INSERT (overlap 시 `COMPENSATION_AWARD_CONFLICT` raise, 옛 award `effective_until` 먼저 set 안내). `payroll_compute_period` 가 active salary award 없으면 skip — 본 도구가 페이슬립 산정의 선행. 사용 절차: plan_list → award_create → compute_period 재호출 → case A INSERT 자동 산정. 후속 (별도 backlog): award_update / award_close / plan_create / event_* 등. | 2026-06-05 | | D-hive-29 | **`compute_period` UPSERT-with-gates (ABCDE) + `compensation_events.adjustment` 정공법 도구** — DELETE+INSERT 가 (i) `payslip_send_log` FK NO ACTION 과 충돌해 발송된 페이슬립 재산정 차단, (ii) ID 변경으로 외부 참조 dangling, (iii) audit history 가 두 ID 로 쪼개짐. UPDATE 는 PG WAL/MVCC 동등 또는 우위. 직원별 case A(없음→INSERT) / B(pending+send_log=0→UPDATE) / C(pending+발송됨→skip) / D(paid→skip) / E(reversed→skip) MECE 룰. UPDATE 는 산정 fields 덮어쓰기 / 운영 fields + 운영 메타 보존. 동반 구현 = `compensation_event_adjustment_create` MCP 도구 (admin scope) — payslips uq 제약으로 갈래 1 line append + 합계 보정 채택, `compensation_events.adjustment` row 가 audit 본질. `delta_krw` 부호 = `line_items.amount_krw` 부호. 발송된 페이슬립 정정 시 직원 메일 ≠ DB → 운영자 재발송 책임. D-hive-28 의 "DELETE+INSERT" 표현을 본 결정이 갱신, employee_ids 제거 자체는 유지. **보강 (룰 H, 동일 날짜)**: adjustment 도구는 case C 만 허용 — A→`PAYSLIP_NOT_FOUND` / B→`PAYSLIP_NOT_DISPATCHED` (compute_period 안내) / D→`PAYSLIP_ALREADY_PAID` (운영자 수동·다음달 소급 자동화 의도적 미구현) / E→`PAYSLIP_REVERSED` raise. 자동화 leg (frame 정정 분개 consumer / 정정명세서 자동 재발송 / 운영자 Teams 알림) 별도 backlog. **보강 (룰 I, 2026-06-05)**: NPS 4.5% → **4.75%** (2026 한국 국민연금 보험료율 9%→9.5% 인상, 1998년 이후 첫 조정). 사용자 원본 산식 `MIN(base, IF(base<370K,16650,IF(base>5.9M,265500,ROUNDDOWN(ROUNDDOWN(base,-3)*4.75%,-1))))` 그대로 적용 — base 천원 절사 + 양끝 정액 cap (16,650 / 265,500) + 외부 MIN wrapper. **룰 K (2026-06-05 최종)** — **근로소득세/주민세 base = R(과세표준) / 4대보험(NPS·NHIS·LTC·EI) base = 신고보수월액(`reported_income_*_krw`, NULL fallback R)**. 보수월액 컬럼 값은 R 과 같게 set 정책 (운영자 책임, 회계·공단 신고 정합). 정액 케이스는 R 기준과 결과 동일, 일할 케이스 (신규 입사·중도 퇴사) 에서 R(일할 변동) vs 보수월액(고정) 차이 가능 — 별도 backlog `B-hive-payroll-base-policy-decision` 정책 결정. axec.AXEC-002/003 reported_income 4,186,222/4,022,567 → 3,966,667 정합 UPDATE (axev 는 이미 정합). 골든 axec 4월 net 강태훈=3,607,277 / 한진우=3,571,577 — 사용자 expected 정확 일치. **MCP 노출 leg 정정**: D-hive-21 payroll v2 컬럼 8개 (reported_income_pension/health·dependents·children·vehicle·is_executive·meal/vehicle_allowance) 를 `employee_get` select / `employee_update` allowed 에 일괄 추가 — 운영자가 신고보수월액 조회·갱신 가능. 골든 강태훈/한진우 axec 4월 pension 188,370→198,830, net 3,605,007→3,594,547 (강태훈) / 3,569,307→3,558,847 (한진우). 5월 ps#7 강태훈 (case C) = adjustment 정공법 정정 필요, ps#8 한진우 (case B) = 다음 compute_period 호출 시 UPDATE 자동. | 2026-06-01 / 2026-06-05 | | D-bp-mcp-calendar-2 | **Admin send-as 캘린더 쓰기** — `create_event` 등 6개 write tool 에 `as_user_email` optional 파라미터 추가 (find_free_time 제외 — 이미 attendeeEmails 로 다중 사용자 조회). 호출자 `role=admin` 이면 target user 의 캘린더에 직접 write 가능, event organizer = target user 표시. 구현: 신규 `getAppOnlyClient()` (graph.ts) = `msalApp.acquireTokenByClientCredential({ scopes: [".default"] })` 로 app-only token 발급 → `/users/{targetUpn}/events` Graph endpoint (UPN/email key — Blueprint `User.id` = Prisma cuid 이지 Entra oid 아님). Azure App **Application permission** `Calendars.ReadWrite` 필요 + tenant admin consent 1회. **권한은 Blueprint Next.js Azure App (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 에 부여** — MCP custom connector 의 별도 App (`482598f7-...`) 가 아님 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)). 명령: `az ad app permission admin-consent --id 2b222356-1c36-48e0-96a3-2c5e0ecbf937` (Global Admin 계정 필요 — AXE 테넌트는 soohun.kang 만). consent 후 blueprint-app 컨테이너 1회 재시작 (MSAL `acquireTokenByClientCredential` 내부 토큰 캐시 비우기). 비-admin caller 의 `as_user_email` 사용 시 403 `send_as_forbidden`. D-bp-mcp-calendar-1 의 delegated 경로는 그대로 (caller 본인 캘린더 = 기본 동작). 동기 = 관리자 (`ai@axellc.com`) 가 직원 계정 (`soohun.kang@axellc.com`) 의 캘린더에 직접 일정 등록하는 사용자 시나리오. | 2026-05-26 | | D-bp-mcp-mail-1 | **범용 이메일 발송 tool `send_mail`** — Blueprint MCP 의 첫 일반 outbound mail surface (기존 outbound 은 `create_event` .ics 초대뿐). caller 본인 mailbox 에서 임의 수신자(외부 도메인 포함)로 메일 발송. 구현: 신규 dispatcher route `POST /api/internal/mail` (calendar route 형제) → caller delegated token (`getClientForUser`; `Mail.Send` delegated 는 **이미** `src/lib/graph.ts` SCOPES 에 존재 → self 발송은 re-consent 불필요) → Graph `POST /me/sendMail`. params: `to`/`subject`/`body`(html\|text)/`cc`/`bcc`/`attachments`(Graph fileAttachment base64)/`save_to_sent_items`/`dry_run`. **Admin send-as** (`as_user_email`, calendar-2 와 동일 게이트): caller `role=admin` 이면 app-only token (`getAppOnlyClient`) + `/users/{targetUpn}/sendMail` 로 타 사용자 mailbox 발송. Azure App **Application permission** `Mail.Send` + admin consent 필요 — **Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 측**, MCP connector app (`482598f7-...`) 아님 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)). 비-admin 의 `as_user_email` → 403 `send_as_forbidden`. **Audit (injection-safety 정책 필수)**: 모든 발송 (`sent`/`dry_run`/`failed`) 이 append-only `MailSendLog` 1 row (caller · sentAs · to/cc/bcc count · subject · status · httpStatus · messageId) — Hive `payslip_send_log` + magnet decisions-ledger 패턴 정합. Graph `sendMail` 은 202 no-body → `message_id` null (Hive D-hive-23 동일; Sent 폴더 사본이 durable record). `dry_run=true` = 메시지 조립·검증·preview 만 반환, Graph 미호출. 동기 = 운영자 봇 (`ai@axellc.com`, role=admin) 이 외부 고객/임원에게 요약 메일 발송 필요. | 2026-05-29 | | D-bp-mcp-1 | Blueprint MCP standalone (Python+FastMCP, frame/hive 1:1 미러) + 5층 양파껍질 회고 → 체크리스트 항구화 | 2026-05-21 | | D-bp-mcp-2 | Blueprint MCP **blue/green pair** (frame `frame-mcp-blue/green` 패턴 미러, alias swap + Caddy reload, cloudflared 무중단) — single replica 가 build 시 5-10s 다운 유발하던 drift 해소 | 2026-05-21 | | D-bp-mcp-teams-1 | **`get_teams_message` MCP tool** — Blueprint MCP 가 `teams.microsoft.com/l/message/...` deep link 를 본문·발신자·타임스탬프로 resolve. 구현 = Python MCP → docker `default` 망 → Blueprint Next.js `POST /api/internal/teams/fetch-message` (Bearer `BLUEPRINT_INTERNAL_API_KEY` 공유 비밀, `/api/internal/entity-roles` 패턴 미러) → 기존 `getChatMessage()` (bot identity `ai@axellc.com`, app perm `ChatMessage.Read.All`) 재사용. 동기 = Claude (web/desktop) connector 사용자가 Teams 링크 던졌을 때 "fetch 불가" 응답이 나오던 capability gap — connector 자체는 enable 됐으나 tool 이 없어서 호출 불가. **Scope**: contextType=chat (1:1·그룹) 만. channel-context URL 은 501 (`/teams/{teamId}/channels/{channelId}/messages/{messageId}` Graph 경로 + groupId 파싱 follow-up). bot 미멤버 chat 은 403 그대로 surface. | 2026-05-26 | | D-bp-mcp-calendar-1 | **Blueprint MCP 의 첫 write surface = M365 caller 캘린더 CRUD** (7 tools: create/update/delete/get/list event, add_attendees, find_free_time). 사용자 요청 = Outlook calendar 자동 일정 등록 + 외부 도메인 (gmail/naver 등) attendee 초대. 구현: 단일 dispatcher route `POST /api/internal/calendar` (op selector — 7 ops in one file, calendar 가 1 logical CRUD domain). caller email → User.id → `getClientForUser(userId)` delegated Graph (`Calendars.ReadWrite`). 외부 attendee 도메인 = Graph 가 SMTP invite 자동 발송, 별도 provider 통합 불필요 (Google Calendar API 와 무관). NextAuth + graph.ts SCOPES 양쪽에 `Calendars.ReadWrite` 추가 — 기존 사용자 1회 re-consent (또는 admin tenant-wide consent). **D-bp-mcp-1 의 read-only 원칙과 무관**: D-bp-mcp-1 은 Blueprint 자체 도메인 (issue/session/workspace) 에 한정 — calendar 는 별개 도메인 (Microsoft Graph). 후속 [D-bp-mcp-calendar-2](#) 에서 admin send-as 추가. | 2026-05-26 | | D-bp-mcp-teams-3 | **`reference` 첨부 파일 본문 fetch** — D-bp-mcp-teams-2 의 v2 가 `attachments[i].contentUrl` 을 surface 했지만 OneDrive/SharePoint 파일의 **실제 본문**은 별도 Graph 호출이 필요해서 호출자가 링크만 받고 끝남. 사용자 첫 메시지에 `.md` 첨부가 있어 즉시 노출된 gap. v3 = 신규 tool `get_teams_attachment_file(content_url)` + 신규 endpoint `POST /api/internal/teams/fetch-attachment-file`. `downloadReferenceFile()` (graph-client.ts:1507) 의 in-memory 버전 = `encodeShareUrl` (private helper 미러: `u!` + base64url, no padding) + `/shares/{enc}/driveItem/content` ARRAYBUFFER fetch. 응답 = 확장자 기반 3 분기: text-like (md/txt/csv/json/yaml/yml/log/xml/html/ts/py/sh/sql/conf/toml/diff 등) → UTF-8 decode 후 `kind: "text"`; image (png/jpg/gif/webp/svg) → base64 + `kind: "image"`; 나머지 → base64 + `kind: "binary"`. 50 MB 기본 / 100 MB hard cap (호출 시 `maxBytes` override 가능). 동기 = file vision 으로 가는 게 아니라 텍스트 파일이 압도적이므로 text → string 직답이 가장 큰 가치. 이미지 자료는 `get_teams_hosted_content` (inline message images) 가 별도 경로. | 2026-05-26 | | D-bp-mcp-teams-2 | **첨부 + 인용 + 인라인이미지 지원 확장** — D-bp-mcp-teams-1 의 v1 응답이 `attachments[]` 를 drop 해 사용자 첫 smoke test 메시지에 첨부가 있어도 호출자가 알 수 없던 gap. v2 응답에 (a) `attachments[]` (`reference` OneDrive 파일 + `contentUrl` / `messageReference` Teams 인용 / Adaptive Card `content` JSON), (b) `quotes[]` (`messageReference` 자동 1-level resolve — `attachments.ts:resolveMessageReferences()` 패턴 미러, quote-of-quote 는 opaque), (c) `inlineImageIds[]` (`extractInlineImageRefs(bodyHtml)`) 추가. 신규 tool `get_teams_hosted_content(chat_id, message_id, hosted_content_id)` = Graph `/chats/{c}/messages/{m}/hostedContents/{h}/$value` ARRAYBUFFER fetch → 매직바이트 sniff (PNG/JPEG/GIF/WebP) → FastMCP `Image()` 반환 → Claude vision 직접 활용. 10 MB 상한. 신규 internal endpoint `POST /api/internal/teams/fetch-hosted-content`. | 2026-05-26 | | D-bp-entity-1 | Blueprint 에 **entity 개념 도입** (axec/axev) — Workspace 에 `entityId` FK + `paraLayer` enum (PROJECT/AREA/RESOURCE/ARCHIVE). PARA 4 layer 모두 entity scope. hive 의 schema-per-entity 와 달리 row-level FK (Blueprint 의 cross-entity workflow 와 부합). [PR #339](https://github.com/soohunkang/blueprint/pull/339) + hotfix [PR #341](https://github.com/soohunkang/blueprint/pull/341) 적용 완료. | 2026-05-21 | | D-bp-entity-2 | **PARA dispatch flow sub-consensus** — Project 종결 → Area/Resource 이관은 copy-with-provenance (`sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt`). Archive = 시점형 archaeology, Area/Resource = 현재형 living knowledge (검색 분리). 자동 분배 = LLM 제안 + 사용자 확인. schema 부분은 D-bp-entity-1 PR 에 통합, UI 는 PR 5 예정 (Path B Spike 선행 권고). | 2026-05-21 | | D-bp-para-1 | **PARA 범용 재구조화 — 조직론 모델 + dispatch "집(link)" + governance 분리 + 죽은 딜 본질** ([D-bp-entity-2](#d-bp-entity-2--para-dispatch-flow-sub-consensus) copy→link **revise**) — PARA=조직론(Area=영속기능/MCP=물질화 · Project=TFT · R/A는 Area 안). dispatch="집" 모델: artifact 몸통1·집N(**link 기본**), move=소모성→Archive→폐기, copy-curate=재사용본 저작→Resource (copy-with-provenance default 폐기). Blueprint=모든 Area 기본 substrate + 가로 결정로그, 서비스=졸업한 live 엔진. governance=결정로그(record, append-only) + 결재워크플로(process, layer), e-sign 통합=[B-bp-decision-pipeline-esign](/ops/backlog). **본질=죽은 딜 저장·harvest**. 신규코드=Rust. 상세=[/architecture/para-os](/architecture/para-os). | 2026-06-06 | | D-bp-rust-1 | **Blueprint 점진 Rust 전환 (strangler-fig) — substrate = 첫 organ** ([D-bp-para-1](/ops/decisions) 열린질문 a/c/d 확정) — 백엔드를 도메인별 Rust(axum) organ 으로 점진 추출, Next.js=얇아지는 frontend-of-record (**프론트는 당분간 TS**, UI 전면 재작성은 별개·먼 결정). 첫 organ = **PARA substrate**(artifact/dispatch/knowledge-query). 경계=**Artifact-scoped**: substrate 가 artifact/artifact_link(+annotation)/mcp_schema 소유, `workspace_id`·`entity_id`=opaque 외부 ref(FK·중복 0), **Workspace+paraLayer SoR=Prisma 잔류**. 공존=**Postgres schema-split**(Prisma `public` / sqlx `substrate`, 한 blueprint-postgres, 교차쓰기 0) → [D-bp-artifact-4](/ops/decisions) "monolith first·신규 인프라 0" **refine(모순 아님): 인프라 동일·언어만 Rust·프로세스 분리**. 배포=blueprint compose sidecar `blueprint-substrate` (cloudflared/공개 MCP/blue-green/ops 온보딩 **없음**, `axe ship blueprint` 한 경로). Next→Rust 인증=기존 `BLUEPRINT_INTERNAL_API_KEY` Bearer. 모델=[D-bp-para-1](/ops/decisions) home/link("집") 1급 + **2-level scope(personal/shared) 확정**(a) + citation 8종([D-index-6](/ops/decisions) `index.*` · [D-gate-2](/ops/decisions) `gate.decision` 포함). Project staffing=후속 defer(c). 상세=ADR `docs/adr/blueprint-rust-migration.md` + [/architecture/para-os](/architecture/para-os). | 2026-06-06 | | D-bp-entity-3 | **General bucket 본질 제거 + sys entity 도입** — `Workspace.entityId` nullable → **NOT NULL FK** (모든 workspace 가 정확히 1 entity 보유). ad-hoc / 시스템 row 는 seeded `sys` entity (kind=`system`, end-user dropdown 미노출). 사용자 결정 2026-05-22 ("General entity 는 필요하지 않습니다"). [PR #344](https://github.com/soohunkang/blueprint/pull/344) + hotfix [PR #345](https://github.com/soohunkang/blueprint/pull/345) (workspace.create 3곳 entityId 필수 — `_resolveEntityIdForCreate` helper: opts.entitySlug / drivePath guess / user.entityScopes[0]). 적용 후: axec 1 / axev 15 / sys 2 / null 0. | 2026-05-22 | | D-bp-entity-4 | **TopNav 전역 EntitySelector + hive name sync** — 페이지별 entity dropdown 폐기 → TopNav 우측 단일 selector (URL `?entity=` primary + localStorage `axe:lastEntity` fallback). session.user.entityScopes 노출. ProjectsClient + ARAClient 가 URL query 읽기로 fetch 구성. Blueprint Entity.name 을 hive `shared.entity.legal_name` 한국어 정식 명칭 (액스코퍼레이션 주식회사 / 액스벤처스 주식회사) 으로 sync — 두 시스템 entity universe SOT 정합. [PR #346](https://github.com/soohunkang/blueprint/pull/346) + [PR #347](https://github.com/soohunkang/blueprint/pull/347) (Next.js 16 useSearchParams Suspense — /axe layout dynamic) + [PR #348](https://github.com/soohunkang/blueprint/pull/348) + [PR #349](https://github.com/soohunkang/blueprint/pull/349). | 2026-05-22 | | D-bp-entity-5 | **Entity metadata 확장 + /api/entities + TopNav fetch** — Entity 에 `bizNo` / `fiscalYearStartMonth` / `countryCode` 추가 (hive shared.entity mirror, axec=366-86-03798 / axev=643-86-01377). `/api/entities` GET 엔드포인트 (서버 측 entityScopes 권한 검증). TopNav `ENTITY_LABELS` hardcode 제거 → /api/entities fetch — 새 entity 등록 시 client 재배포 0. seed drift detect 가 모든 mirror field 비교 (PR #350 의 early-out fix). [PR #350](https://github.com/soohunkang/blueprint/pull/350) + [PR #351](https://github.com/soohunkang/blueprint/pull/351). | 2026-05-22 | | D-bp-entity-6 | **Team entity scope + Member.entityId rename** — `Member.entityId` (polymorphic User/Agent FK disambiguator) → `underlyingId`. D-bp-entity-1 의 `Workspace.entityId` (accounting entity) 와 naming 충돌 영구 해소. team-service 응답에 `entityScopes: string[] \| null` 추가 (human = User.entityScopes / agent = null). TeamClient + OrgChartView 가 chip 표시 (axec/axev = lime / sys = muted) + URL `?entity=` filter (human 만 적용, agent 는 system actor pass-through). [PR #352](https://github.com/soohunkang/blueprint/pull/352). | 2026-05-22 | | D-bp-entity-7 | **User.defaultEntity — 사용자별 명시적 default entity** — 이전: TopNav 첫 로드 "all" / createWorkspace fallback "scopes[0]" 가 customers.yaml 순서 의존 (axec). 사용자별 default 명시 불가. 본 PR: `User.defaultEntity String?` 컬럼 + hydrateEntityScopesForUser 자동 set (null + non-empty scopes → scopes[0]; scope 외 default → scopes[0] fallback). session.user.defaultEntity 노출. TopNav fallback chain: URL → localStorage → defaultEntity → "all". createWorkspace fallback: opts → drivePath → defaultEntity → scopes[0] → throw. `/api/user/default-entity` PATCH 엔드포인트 (server-side gate). [PR #354](https://github.com/soohunkang/blueprint/pull/354). 운영 후속: Settings UI dropdown (endpoint 의 사용자 surface, 별도 PR). | 2026-05-22 | | D-bp-entity-8 | **entityScopes YAML 순서 보존 + scopes[0] fallback** — D-bp-entity-7 의 후속 함정: hydrate 가 entityScopes 를 alphabetic sort 후 저장 → customers.yaml `["axev","axec"]` operator 의도 SOT 가 DB `["axec","axev"]` 로 reset → scopes[0] = "axec" → axev default 불가능. fix: hydrate 가 raw `resolved` 순서로 저장 (sort 는 set-equality 비교만). TopNav fallback chain 에 `userScopes[0]` 추가 — URL → localStorage → defaultEntity → scopes[0] → "all". session.defaultEntity null (build cache miss 등) 이어도 yaml 순서 가 운영 default. customers.yaml 의 user_entity_map list 순서가 진정한 SOT. 사용자 결정 "모두 default 는 axev" 만족. [PR #355](https://github.com/soohunkang/blueprint/pull/355). | 2026-05-22 | | D-bp-entity-9 | **owned account switching (TopNav AccountSwitcher)** — Soohun 이 `soohun.kang@axellc.com` + `ai@axellc.com` + `cfo@axellc.com` 모두 운영. owner 가 owned AI agent identity 로 view-only switch. 본 PR: `User.ownedBy String?` self-FK (NULL 시 본인 계정, set 시 owner.id 참조). `POST /api/auth/switch-account { targetUserId }` 가 `target.ownedBy === caller.userId` 검증 후 httpOnly cookie `axe:active-user-id` set. `DELETE` 는 cookie clear (revert). session callback 이 cookie 발견 시 매 read 마다 ownership 재검증 → session.user.* 모두 target 로 override (email/name/role/entityScopes/defaultEntity). JWT 자체는 untouched — authenticated identity 의 audit trail 보존, view 만 변경. TopNav AccountSwitcher dropdown (owned ≥1 시만 노출, isImpersonating=true 시 lime border + tooltip). mid-session ownership revoke 시 자동 revert (defense-in-depth). admin role 도 bypass 불가 — owned-only. [PR #356](https://github.com/soohunkang/blueprint/pull/356) + hotfix [PR #357](https://github.com/soohunkang/blueprint/pull/357) (Suspense import). 적용 후 DB: ai@/cfo@ ownedBy = soohun.kang.id. | 2026-05-22 | | D-bp-entity-10 | **AccountSwitcher 항상 노출 + Add account** — 이전: owned ≥1 (`accounts.length > 1`) 조건으로 single-identity 사용자에게 dropdown 가려짐 → 다른 계정 추가 진입로 부재. 본 PR: gate 를 `!activeUserId` 로만 (signed-in 모두 노출). dropdown 안에 `+ 다른 계정으로 로그인` 옵션 추가 → 선택 시 `signIn("azure-ad", ..., { prompt: "select_account" })` 호출, Microsoft 계정 picker 강제. PR #358 첫 시도는 `return null on accounts.length === 0` 함정 (initial state 가 빈 배열 → null mount → useEffect setAccounts 무효) → hotfix [PR #359](https://github.com/soohunkang/blueprint/pull/359) 가 condition 을 `!activeUserId` 로 좁힘. [PR #358](https://github.com/soohunkang/blueprint/pull/358) + #359. | 2026-05-22 | | D-bp-entity-11 | **"전체 entity" 폐기 — 항상 specific scope** — 이전: TopNav 와 ProjectsClient/ARAClient/TeamClient 가 `entity === "all"` 분기로 전체 entity 합산 뷰 제공. 사용자 보고: "전체 entity 선택하는 것 잘 동작하지 않습니다" + 회계/거버넌스 측면에서도 cross-entity 합산은 의미 부재 (axec 와 axev 는 별 법인). 본 PR: ALL 상수 제거 + URL `entity` 가 userScopes 안 이 아니면 자동 specific 으로 rewrite (fallback: localStorage → defaultEntity → scopes[0]). 클라이언트 fetch 도 `entity` 항상 set. [PR #360](https://github.com/soohunkang/blueprint/pull/360) + follow-up [PR #361](https://github.com/soohunkang/blueprint/pull/361) (3 client 의 "all" 분기 제거 — ProjectsClient/ARAClient/TeamClient). | 2026-05-22 | | D-bp-entity-12 | **user name = AccountSwitcher trigger (M365/Gmail 패턴)** — 이전: 사용자 이름은 정적 ``, AccountSwitcher 는 별도 `` native 동작 (browser-provided dismiss / option 색) 잃지만 M365·Gmail UX 일관성 + click target = label 본질 우선. | 2026-05-22 | | D-bp-entity-13 | **EntitySelector → button + popover (UI 일관성)** — 사용자 요청: "entity dropdown 도 id dropdown 처럼 보여지면 좋겠습니다". TopNav 의 두 dropdown (EntitySelector + AccountSwitcher) 중 EntitySelector 만 native ` ``` 문제점: - `id` = Base UI 라이브러리의 무작위 패턴 (예: `base-ui-_r_dn_`) — 렌더마다 변경 가능. 안정 식별자가 아님. - `name` / `aria-label` / `autocomplete` = **모두 없음**. - 결과적으로 password manager 가 매칭할 안정 식별자가 0 — placeholder 만 시도 가능. Bitwarden 매칭 logic (실측): ```javascript // Bitwarden 의 form field 매칭 후보 정규화 const normalize = (s) => (s || '').toLowerCase().replace(/[^a-zA-Z0-9]+/g, ''); ``` - 한국어 placeholder `"이름"` → 정규화 결과 = `""` (빈 문자열) → 매칭 후보 사라짐. - 영문 placeholder `"OAuth Client ID"` → 정규화 결과 = `"oauthclientid"` → keyword 매칭 가능. 즉 한글 placeholder 만 가진 2 input (Name, 그리고 URL 도 일부 PM 에서 url scheme 매칭 실패) 은 구조적으로 자동입력 불가. ## 4. Requested fix 각 input 에 안정 식별자를 1개 이상 추가 (권장 = 3개 모두): ```html ``` 이유: - `name` = HTML 표준 form field identifier. Password manager 의 1순위 매칭 단서. - `aria-label` = 시각 장애인 접근성 (screen reader) + 영문 식별자 — password manager 의 보조 단서. - `autocomplete="off"` (또는 secret 의 경우 `"new-password"`) = browser 의 자체 저장 prompt 차단 (선택적). 추가 권장: 부모 `
` 에도 `name="add-custom-connector"` 같은 form-level 식별자를 부여 — Bitwarden 의 form context 매칭 보조. ## 5. Impact | 사용자 | 현재 | 권장 fix 후 | |---|---|---| | 한국어 UI + Bitwarden | 2/4 자동입력 (EN substring 만), 2/4 수동 | 4/4 자동입력 | | 한국어 UI + 다른 PM | 동일 (정규화 규칙 동일) | 4/4 자동입력 | | 영문 UI | 2/4 자동입력 (placeholder EN substring) | 4/4 자동입력 | | 다른 locale (일본어, 중국어 등) | 0/4 또는 2/4 (locale 별 placeholder 정규화에 의존) | 4/4 자동입력 | a11y 부수 효과: `aria-label` 추가로 screen reader 사용자의 form field 식별이 개선됨. ## 6. AXE Labs 자체 우회 (참고) 운영자 (AXE Labs 임직원) 가 한국어 UI 에서 자체 catalog 를 통해 우회: - Vaultwarden `MCP Connectors` org collection 의 6 Custom Field 등재: `Name`, `Server URL`, `MCP URL`, `Tenant ID`, `Scopes`, `Vault secret path`. - autofill 2/4 (Client ID + Secret, EN label substring 매칭) 가능. - 나머지 2/4 (Name + URL) = Bitwarden popup 의 ⎘ copy → paste 수동. 본 우회는 운영 가능하나 connector 등록 1 건당 운영자 click 약 6 회 발생. 본 upstream fix 는 한국어 UI 에서도 4/4 자동입력, 다른 locale 도 동시에 해소. ## 7. Attachments / references - Custom Connector modal HTML inspect 결과 (2026-05-27, Claude in Chrome MCP). - Bitwarden 매칭 정규화 규칙 (`/[^a-zA-Z0-9]+/g`) — Bitwarden browser extension source 참조 가능. - AXE Labs 자체 catalog 우회 패턴 (`B-axe-mcp-catalog-en-aliases` 결과물). - 관련 known-gaps: [claude.ai Custom Connector autofill 한국어 한계](/ops/known-gaps). ## 8. Submission channels - **Email**: `support@anthropic.com` (subject 예시: `Feature request: Custom Connector input identifiers for i18n password manager support`) - **GitHub**: https://github.com/anthropics/claude-code/issues — issue title 예시: `Custom Connector modal: add name/aria-label to 4 input fields for i18n password manager support` ## 9. Status | Field | Value | |---|---| | Report state | Draft | | Submission date | TBD (운영자 결정) | | Anthropic acknowledgement | TBD | | Anthropic fix release | TBD | 운영자가 검토 후 위 채널 중 하나로 제출. 회신 수령 시 본 페이지 §9 표를 갱신하고 [/ops/known-gaps](/ops/known-gaps) 의 해당 항목에 진행 상황 한 줄 추가. --- # Claude Desktop PTY/fd leak — bug report draft (2026-05) > macOS Claude.app 의 /dev/ptmx fd 누적 leak 으로 1-2 주 가동 후 forkpty ENXIO 차단. Anthropic support 또는 claude-code GitHub issue 제출용 draft. URL: https://docs.axelabs.ai/ops/reports/claudeapp-fd-leak-2026-05 # Claude Desktop PTY/fd leak — bug report draft (2026-05) > **상태**: Draft. 운영자 검토 후 `support@anthropic.com` 또는 [claude-code GitHub issues](https://github.com/anthropics/claude-code/issues) 에 그대로 전송 가능한 self-contained 리포트. > > **Backlog**: [B-claudeapp-fd-leak-anthropic-report](/ops/backlog) · **Tracking**: [B-claude-desktop-pty-leak-track](/ops/backlog) · **Mitigation**: [B-axe-pty-max-launchd](/ops/backlog) > > **관련 분석**: [Claude.app /dev/ptmx fd leak (known-gaps)](/ops/known-gaps) --- ## 1. Summary Claude Desktop on macOS leaks file descriptors to `/dev/ptmx`, accumulating fd usage over weeks of normal operation until the system-wide `kern.tty.ptmx_max` limit is reached. After that, new terminal/PTY spawns fail with `forkpty: Device not configured` (ENXIO), affecting all macOS users on the host (not just Claude.app itself). The only known fix is to quit and restart Claude Desktop, which reclaims all leaked fds. ## 2. Environment - **OS**: macOS (Darwin 25.3.0 at time of reproduction) - **App**: Claude Desktop — `/Applications/Claude.app/Contents/MacOS/Claude` - **Hardware**: Apple Silicon mac mini - **Uptime at failure**: 43+ days continuous operation (observed 2026-05-26) - **PTY limit**: `kern.tty.ptmx_max` = 511 (macOS default; sysctl raise to 2047 is rejected by kernel) ## 3. Symptom ### 3.1 Failure mode - New terminal spawns (Terminal.app, iTerm2, Claude Code subprocess `forkpty()`) are denied with ENXIO. - Error string: `forkpty: Device not configured` - **Scope**: system-wide — not limited to Claude.app. All users on the host lose the ability to allocate new PTYs until the leak is cleared. ### 3.2 Reproduction 1. Use Claude Desktop normally on a macOS host (open many chats, close them, leave the app running). 2. After 1–2 weeks of continuous uptime, monitor PTY fd consumption: ```bash sudo lsof /dev/ptmx | grep -i Claude | wc -l ``` Count grows steadily over time. 3. When the total `lsof /dev/ptmx` count reaches `kern.tty.ptmx_max` (default 511), `forkpty: Device not configured` starts to fire on every new PTY allocation attempt. ### 3.3 Diagnosis commands ```bash # Per-process PTY fd occupancy (sorted) sudo lsof /dev/ptmx | awk '{print $1}' | sort | uniq -c | sort -rn # Kernel PTY ceiling sysctl kern.tty.ptmx_max # Detailed fd list for Claude.app PID ps aux | grep -i Claude sudo lsof -p | grep /dev/ptmx | wc -l ``` ### 3.4 Observed data (2026-05-26, operator host) - Claude.app PID held fds 43, 44, 46, 88, 89, 92, 94, 98, 99, 102+ against `/dev/ptmx`. - Reaping userspace zsh zombies (82 accumulated, all Claude Code subprocesses) did **not** release the ptmx fds — confirming the leak is in Claude.app proper, not in spawned shells. ## 4. Workarounds (current) | Action | Effect | Notes | |---|---|---| | Quit + relaunch Claude Desktop | Immediate: all leaked fds reclaimed | Only known reliable fix | | `sudo sysctl -w kern.tty.ptmx_max=2047` | Ineffective | Kernel rejects values above 511 on current macOS | | launchd plist for boot-time sysctl ([B-axe-pty-max-launchd](/ops/backlog)) | Pending validation | Likely also rejected by kernel; tracked as long-term mitigation | ## 5. Suspected root cause - Claude.app spawns PTY-backed subprocesses (IPC helpers, terminal-like surfaces) without releasing the master fd when the subprocess exits or the chat is closed. - Reaping orphaned userspace `zsh` zombies (Claude Code subprocesses) does not free the `/dev/ptmx` fds — strongly suggests the leak is in the Claude.app process holding the master end open after the slave has gone away. - Pattern is consistent with missing `close()` on the master fd in a subprocess lifecycle path. ## 6. Impact - **Affected users**: macOS users running Claude Desktop with long uptime (1–2 weeks+) and any non-trivial chat volume. - **Blast radius**: system-wide PTY denial — all terminal applications on the host are blocked from spawning new sessions, not just Claude.app. - **Detectability**: silent until the limit is hit; the failure surfaces in unrelated apps (Terminal.app, iTerm2) and is hard to attribute to Claude.app without running `lsof /dev/ptmx`. ## 7. Requested fix 1. Audit Claude.app's PTY subprocess lifecycle — ensure master fds are `close()`d when the slave end exits or the owning chat/IPC channel is torn down. 2. Guarantee close-on-exit semantics for all spawned PTYs. 3. Add a self-check in the production build: if Claude.app's own `/dev/ptmx` fd count exceeds a threshold (e.g. 100), emit a telemetry event so this regression is caught automatically in future releases. ## 8. Attachments / references - Operator known-gaps entry: [Claude.app /dev/ptmx fd leak](/ops/known-gaps) - Related operator backlog items: - `B-claude-desktop-pty-leak-track` — tracking the recurrence - `B-axe-pty-max-launchd` — host-side mitigation attempt - `B-claudeapp-fd-leak-anthropic-report` — this report ## 9. Submission channels - **Email**: `support@anthropic.com` - **GitHub**: https://github.com/anthropics/claude-code/issues — cross-post recommended since the symptom also surfaces from Claude Code's subprocess `forkpty()` paths. ## 10. Status | Field | Value | |---|---| | Report state | Draft | | Submission date | TBD (운영자 결정) | | Anthropic acknowledgement | TBD | | Anthropic fix release | TBD | 운영자가 검토 후 위 채널 중 하나로 제출. 회신 수령 시 본 페이지 §10 표를 갱신하고 [/ops/known-gaps](/ops/known-gaps) 의 해당 항목에 진행 상황 한 줄 추가. --- # Timshel/vaultwarden upstream PR draft — bw 2026.4+ client compatibility (2026-05) > AXE Labs 가 Timshel fork 의 axe.2 release 에 적용한 2 patch 의 upstream 제출 draft. /accounts/prelogin/password alias + /identity/connect/token AccountKeys/MasterPasswordUnlock backport. 둘 다 dani-garcia/vaultwarden mainline backport 성격. URL: https://docs.axelabs.ai/ops/reports/timshel-vaultwarden-upstream-pr-2026-05 # Timshel/vaultwarden upstream PR draft — bw 2026.4+ client compatibility (2026-05) > **상태**: Draft. 운영자 검토 후 [Timshel/vaultwarden](https://github.com/Timshel/vaultwarden) 에 그대로 PR 본문으로 paste 가능한 self-contained 제출문. > > **Backlog**: [B-vault-axe.2-patches-upstream-pr](/ops/backlog) · **관련 분석**: [/ops/known-gaps](/ops/known-gaps) 의 Timshel fork drift 항목. > > **Scope**: 본 PR 은 axe.2 의 2 patch 만 다룸. axe.3 의 organizations Owner-skip 제거 + cipher.permissions backport 는 별 PR 권장 (§7 참조). --- ## 1. Summary Timshel/vaultwarden fork 가 dani-garcia/vaultwarden mainline 대비 2개 endpoint 가 outdated → bw CLI 2026.4+ / Bitwarden Chrome extension >= 2026.4 와 호환 깨짐. 본 PR 가 2 minimal patch 로 mainline 동등 shape 회복. ## 2. Motivation ### 2.1 Symptom - bw CLI `2025.7.0` 까지는 호환 정상. - bw CLI `2026.4.x` + Bitwarden Chrome ext `2026.4+` 에서 SSO user unlock 시도 시 다음 에러: - `Master password unlock data was not found` - `Cannot read properties of null (reading 'toWrappedAccountCryptographicState')` ### 2.2 Trigger 새 Bitwarden client (2026.4.x) 가 unlock flow 에서 server 측 응답 shape 의존성 강화 (mainline vaultwarden 의 새 spec). Timshel 은 옛 spec 유지 → 새 client 가 expect 하는 field 부재로 unlock 차단. ### 2.3 Affected - Timshel fork 사용자 + 2026.4.x bw client 조합. - SSO 인증 후 master password unlock 흐름. ### 2.4 Workaround (현재 운영) - 클라이언트 측: `npm install -g @bitwarden/cli@2025.7.0` 으로 pin (임시). - 서버 측 (영구): 본 PR 의 2 patch 적용 — AXE Labs 는 axe.2 release 로 production 운영 중. ## 3. Changes ### 3.1 Patch 0001 — `/accounts/prelogin/password` alias **File**: `src/api/identity.rs` (또는 동등 router). **Change** (개념): ```rust // 기존 .route("/accounts/prelogin", post(prelogin)) // 추가 .route("/accounts/prelogin/password", post(prelogin)) // mainline alias ``` 또는 동일 handler 가 두 path 모두 처리하도록 등록. **Rationale**: dani-garcia/vaultwarden mainline 의 `/accounts/prelogin/password` endpoint 와 동일 response shape. bw 2026.4 client 가 본 path 호출 → 200 OK + shape (kdf, kdfIterations 등) 받아 unlock 진행. ### 3.2 Patch 0002 — `/identity/connect/token` AccountKeys/MasterPasswordUnlock **File**: `src/api/identity.rs` (또는 connect_token handler). **Change** (개념): ```rust struct TokenResponse { // ... 기존 field AccountKeys: AccountKeysData, // 신규 MasterPasswordUnlock: MpUnlockData, // 신규 } struct AccountKeysData { publicKey: String, encryptedPrivateKey: String, // ... mainline 동등 } struct MpUnlockData { kdf: KdfType, kdfIterations: i32, kdfMemory: Option, kdfParallelism: Option, salt: String, masterKeyWrappedUserKey: String, emailKdfIterations: Option, // ... mainline 동등 } ``` **Rationale**: mainline vaultwarden 의 `/identity/connect/token` response 와 동일 field set. 2026.4+ client 가 본 field 로 unlock flow 진행. ## 4. Testing ### 4.1 Manual verification (AXE Labs production, 2026-05-26) Server 측 (axe.2 image deploy 후): ```bash curl -X POST https://axe.axelabs.ai/vault/api/accounts/prelogin/password \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com"}' # Expected: 200 OK + {kdf, kdfIterations, ...} ``` Client 측 (bw CLI 2026.4+): ```bash bw config server https://axe.axelabs.ai/vault bw login --sso # Expected: SSO + MP flow 통과, unlock 성공 ``` ### 4.2 Real-world validation - AXE Labs production 운영 (2026-05-26 부터): axe.2 image deploy 후 bw CLI 2026.4.x + Chrome ext 2026.4.x 모두 정상 unlock 검증. - Real users: 4 명 (운영자 + 3 임직원) — 회귀 0 보고. ## 5. Compatibility - **Backward compat**: 기존 `/accounts/prelogin` 그대로 유지 → 옛 client (2025.x 이하) 영향 0. - **Forward compat**: mainline vaultwarden 의 shape 와 동등 → 미래 client update 와도 정합. - **Database migration**: 없음 — 모두 response shape 변경만. ## 6. Code reference AXE Labs 의 fork repo: https://github.com/axelabs-ai/vault - axe.2 release commit: TBD (운영자가 commit hash 채울 것) - `patches/0001_prelogin_password_alias.patch` - `patches/0002_connect_token_account_keys_mp_unlock.patch` Mainline reference: - dani-garcia/vaultwarden 의 동등 코드 위치 (PR 제출 시 commit/file 링크 첨부 권장). ## 7. Related (별 PR 권장) axe.3 release 의 추가 2 patch — 본 PR scope 외: - **Patch 0003**: organizations Owner-skip 3 site removal — `post_organization_collections` + `post_bulk_access_collections` + `post_organization_collection_update` 의 `if member.access_all { continue; }` 제거 → Owner 도 명시 users_collections row 생성. - **Patch 0004**: `cipher.rs::to_json` User-sync branch 에 `permissions: {response: null, delete: , restore: }` 추가. 위 2 patch 는 organization permission model 의 mainline 정합 — 별 PR 로 분리 제출 권장 (review surface 분리 + 본 PR 의 compatibility-only 성격 유지). ## 8. Status | Field | Value | |---|---| | Report state | Draft | | 작성 일자 | 2026-05-28 | | 제출 일자 | TBD (운영자 결정) | | Timshel upstream 회신 | TBD | | 수락 시 효과 | AXE fork 의 axe.2 diff 자연 소실 (`B-vault-axe.2-sso-mp-incomplete` archive) | ## 9. Submission channels - **Repo**: https://github.com/Timshel/vaultwarden - **PR title**: `Backport prelogin/password alias + connect/token AccountKeys/MasterPasswordUnlock from dani-garcia mainline (bw 2026.4+ compatibility)` - **Label 권장**: `compatibility`, `mainline-backport` 운영자가 검토 후 위 채널로 PR 제출. 회신 수령 시 본 페이지 §8 표를 갱신하고 [/ops/backlog](/ops/backlog) 의 `B-vault-axe.2-patches-upstream-pr` 를 ✅ 로 이동. --- # 로드맵 (마일스톤) > AXE Labs 플랫폼의 분기/마일스톤 수준 큰 그림. 단기 실행 항목은 backlog 참조. URL: https://docs.axelabs.ai/ops/roadmap # 로드맵 (마일스톤) > 이 페이지는 **어디로 가는가** 의 큰 그림입니다. 다음 세션이 들어와서 "지금 무엇부터" 가 궁금하다면 [backlog](/ops/backlog) 가 entry point — 본 페이지는 그 백로그 항목들이 묶이는 상위 목표를 보여줍니다. 이미 ship 된 변경은 [updates](/ops/updates) 의 Ship Log + Highlights. > > **시간축 4 페이지** ([D-docs-updates-1](/ops/decisions)): [backlog](/ops/backlog) (현재) · **본 페이지** (미래) · [updates](/ops/updates) (과거) · [known-gaps](/ops/known-gaps) (사실). > > **상태**: 📐 설계 · 🔧 개발 · 🎯 검증 · ✅ 완료 · ⏸️ 보류 > > **신규 마일스톤 등재 절차**: 본 표에 한 줄 추가 → 관련 [decisions.mdx](/ops/decisions) D-마일스톤-N 등재 → 실행 항목은 [backlog](/ops/backlog) 에 마일스톤 ID 로 묶기. ## 마일스톤 표 | ID | 마일스톤 | 목표 | 상태 | Target | |---|---|---|---|---| | **M1** | [Stage 0 → 1 외부 출시](#m1--stage-0--1-외부-출시) | Multi-tenant closed beta 진입 — 격리·법무·관측·복원 전부 충족 | 🔧 개발 | 2026 Q3 | | **M2** | [frame 회계 도메인 확장](#m2--frame-회계-도메인-확장-axev-펀드-회계) | AXEV 펀드 (KIP/KVF) 회계 — 조합 회계·LP capital call·cross-entity mirror 분개 | 📐 설계 | 2026 Q3 | | **M3** | [Blueprint PARA dispatch](#m3--blueprint-para-dispatch-d-bp-entity-1-pr-5) | Project 종결 → Area/Resource 로 copy-with-provenance 분배 + LLM 제안 + dispatch UI | 📐 설계 | 2026 Q3 | | **M4** | [hive payroll v2 prod cutover](#m4--hive-payroll-v2-prod-cutover) | 한국 도메인 본질 7가지 (간이세액표·비과세·정산·일할 등) 검증 완료 후 실 사용 | 🎯 검증 | 2026 Q2 | | **M5** | [Backup/DR 강화](#m5--backupdr-강화) | hive-postgres Tier A 합산 (✅) + restore drill 정기화 + mysrt 처리 결정 | 🔧 개발 | 2026 Q3 | | **M6** | [Blueprint artifact + PARA 지식 레이어](#m6--blueprint-artifact--para-지식-레이어) | typed fact layer in Blueprint Postgres — citation-backed, PARA dispatch field-level 진화 | 📐 설계 | 2026 Q4 | | **M7** | [index 투자 도메인 backend](#m7--index-투자-도메인-backend) | 펀드 lifecycle (소싱·DD·IC·포트폴리오·엑싯·IR) typed fact SoT. financial_model 6-table + fund_investment N:M + 5 skill 진화 | 📐 설계 | 2026 Q4 | --- ## M1 — Stage 0 → 1 외부 출시 **목표**: 현재 단일 customer (axec/axev) 운영 → 외부 customer 1~3개의 closed beta 진입. 5분야 점검 (2026-05-21) 의 모든 차단 사항 해소. **근거 결정**: D-org-fanout (신규 예정), D-ops-15.5 (도메인 검증), D-ops-17~19 (secret 운영), D-config-15 (multi-customer 분기) **의존성**: - [backlog](/ops/backlog) 의 M1 태그 항목들 — tenancy FK 마이그레이션 · Azure AD 분기 · 법무 4페이지 · edge rate-limit · Sentry · `axe-health-monitor` · restore drill - 첫 외부 customer 확정 (영업) — 본 로드맵 외 의사결정 **완료 정의**: 5분야 점검 표 모든 항목 ✅, 첫 외부 customer 운영 1주 + 회고 1회. 상세 현황: [known-gaps#multi-tenant](/ops/known-gaps) Multi-tenant 섹션. ## M2 — frame 회계 도메인 확장 (AXEV 펀드 회계) **목표**: frame 이 corporate (axec/axev) 외에 **펀드 (KIP/KVF) 회계** 처리. AXEV 가 GP 인 일부 결성 조합 (수·타입 detail 대기) 의 정확한 출자금·미출자약정·운용보수·성과보수·평가손익·분배금 처리. **근거 결정**: D-frame-N1~N4 (예정, decisions.mdx 의 frame 측 미완 표 참조). **아키텍처 요지** (2026-05-22 drift 정정): - 기존 `shared` schema 확장 (`frame_meta` 신설 X — 명명 충돌 + cross-entity 메타가 이미 shared 에 존재) - `shared.entity` 에 `entity_kind` (corporate/kip/kvf) + `fund_meta` JSONB + `closed_at` - `shared.entity_relationship` 확장 (% → numerator/denominator/unit, kind ENUM 에 `gp_managed_fund`/`lp_invested_fund`) - `shared.cross_journal_link` 신설 (schema 격리 환경 mirror 분개 무결성) - fund schema 내부: `commitment_ledger` + `lp_master` + (optional) `fund_waterfall_state` - Blueprint workspace↔entity = **N:M** (`WorkspaceEntity` join) — D-bp-entity-1 plan 수정 필요 **의존성**: - [backlog](/ops/backlog) 의 M2 태그 항목들 — migration 7단계 - 결성 조합 detail (사용자 입력) — 1~4 단계는 detail 없이 가능 **완료 정의**: 결성 조합 1개 schema bootstrap + 과거 결산 적재 + AXEV↔조합 mirror 분개 1회 검증. 상세: [known-gaps#frame-측-미완](/ops/known-gaps) frame 섹션의 7단계 migration 순서. ## M3 — Blueprint PARA dispatch (D-bp-entity-1 PR 5) **목표**: Blueprint Project 종결 시 Area/Resource 로 artifact 이관 — copy-with-provenance + 검색 분리 + 정비 워크플로 + LLM 제안. **근거 결정**: [D-bp-entity-1](/ops/decisions) (entity 개념 도입) + 본 세션 합의 4개 (PARA dispatch flow, 2026-05-21). **합의 4개**: 1. **copy-with-provenance** — `sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt` 3 필드 신설. 원본 frozen + 사본 fork 2. **검색 분리** — Archive = "그때 어떻게 생각했는지", Area/Resource = "지금 이렇게 생각한다" 3. **끊임없는 정비** — 일회성 도구 X, living document 4. **LLM 제안 + 사용자 confirm** — 완전 자동 X **미결 3종** (PR 5 직전 결정): - 분배 단위 (파일 vs 의미) - Area 인스턴스 정의 권한 (org-admin vs free) - 정비 트리거 우선순위 (수동 / 주기 / LLM 제안) **의존성**: - D-bp-entity-1 PR 1 의 provenance 3 필드 (Workspace 스키마 변경) 선행 - M2 의 Blueprint WorkspaceEntity N:M 마이그레이션 (entity 결정과 무관하지 않음 — 분배 단위 결정 시 entity 경계 고려) **검토 권장 경로**: Path B Spike (DB 변경 없이 단일 workspace 로 흐름 검증 3-5일) → Path A 본 구현. ## M4 — hive payroll v2 prod cutover **목표**: 한국 도메인 본질 7가지 (간이세액표 lookup · 비과세 분리 · 보수월액별 · 부양가족 · 정산 · 일할 · 보험별 라운딩) 의 첨부 급여대장 셀별 정확 일치 검증 후 axec/axev 실 운영 cutover. **근거 결정**: [D-hive-21](/ops/decisions) ~ [D-hive-26](/ops/decisions) (payroll v2 + 발송 인프라 + 템플릿 + audit + event consumer). **현 상태**: 기능 구현 끝, Phase 3 (payroll v2 실데이터) 운영 중. backup Tier A 합산 완료 (D-hive-backup). **의존성**: - [backlog](/ops/backlog) 의 M4 태그 항목 — 첨부 급여대장 cell-by-cell 비교, DKIM/DMARC DNS prod 확정, audit log 무결성 검증 - M5 의 restore drill (페이롤 데이터 백업 검증) **완료 정의**: 2분기 (4월·5월) 전직원 급여 v2 로 산출 + 메일 발송 + 정산 일치. ## M5 — Backup/DR 강화 **목표**: 모든 platform postgres + critical state 의 Tier A 백업 (`axe-backup` 통합) + 정기 restore drill + DR cold storage rotation. **근거 결정**: [D-config-14](/ops/decisions) (DR cold storage), [D-hive-backup](/ops/decisions) (hive Tier A 합산). **현 상태**: - ✅ frame-postgres / blueprint-postgres / hive-postgres Tier A 합산 끝 - 🔧 mysrt-postgres — SRT 외부 SOT 가능성 잔존 → 별도 결정 보류 - 🔧 restore drill — 1회 실시 후 정기화 미정 **의존성**: [backlog](/ops/backlog) 의 M5 태그 항목들 — restore drill 첫 실시 · mysrt 결정 · cold storage rotation 정기 cron. **완료 정의**: 모든 postgres restore drill 1회 + 정기 drill cron + mysrt 처리 결정 등재. ## M6 — Blueprint artifact + PARA 지식 레이어 **목표**: Blueprint 의 markdown PKM (ctx) 위에 typed fact layer (`Artifact`). Per-field schema + citation + confidence + PARA scope. Cross-functional query / 결정론적 citation / time-travel / multi-agent concurrent read-write 가능. **Status escalation (2026-05-26)**: M6 가 Cortex 의 Day 1 표준으로 escalation. Cortex ([D-cortex-1](/ops/decisions)) 가 platform 의 첫 artifact-first reference impl — Q4 2026 Blueprint stage 2 (full integration) 보다 6 개월 앞서 실 사용. Cortex 의 artifact / citation / artifact_event / mcp_schema 5 테이블 + RLS + append-only trigger 가 reference 구현. Blueprint 측 PARA dispatch UI 도입 시 Cortex 의 owner-scoped artifact 도 mirror 대상. **근거 결정**: [D-bp-artifact-1~5](/ops/decisions) (본 마일스톤 등재 동반), [D-cortex-1](/ops/decisions) (Day 1 채택, Cortex reference impl), [D-bp-entity-1](/ops/decisions) (PARA + entity 골격), [D-bp-entity-2](/ops/decisions) (workspace-level provenance — 본 마일스톤이 field-level 로 진화), [D-config-16](/ops/decisions) (Blueprint Postgres). **아키텍처**: [/architecture/artifacts](/architecture/artifacts) 참조 — `Artifact` 테이블 (JSONB content + citations) + 6-kind citation resolver + PARA dispatch engine + Query API + ctx 진화. **3 Stage 구조** (활성화 조건 충족 시): | Stage | 산출 | 기간 | |---|---|---| | 0. PoC | filesystem-only 첫 schema (예: IC §재무 또는 `PortcoBoardKPIArtifact` — domain MCP 가 없는 영역에서 LLM 자율 extract) — DB 변화 0 | ~3일 | | 1. DB 진입 | `Artifact` + `Citation` + `artifact_link` Prisma migration, MCP schema discovery 인프라 (frame/hive `/schemas` endpoint + Blueprint fetch/cache/version), 4 paraLayer UI 페이지 신설 (`/axe/{projects,areas,resources,archive}`) | ~3-4주 | | 2. Full integration | Dispatch field-level engine (copy + link 2 mode), Knowledge Overview dashboard, MCP tool paraLayer 분리 (`list_projects` / `list_areas` / etc.), ctx 진화 (curation mode + markdown 점진 migration flow) | ~3-4주 | **의존성**: - [M3](#m3--blueprint-para-dispatch-d-bp-entity-1-pr-5) PARA Dispatch (workspace-level provenance UI) — 본 마일스톤이 그 field-level 진화 - [M1](#m1--stage-0--1-외부-출시) multi-tenant rollout — 평행 가능 (외부 surface 영향 0), 다만 critical path 충돌 회피 위해 M1 main 진행 시 M6 secondary - [backlog](/ops/backlog) 의 M6 태그 항목들 **완료 정의**: `Artifact` + `Citation` + `artifact_link` 테이블 production 적용 + MCP schema discovery active (frame/hive `/schemas` 등재) + 1개 deal 의 IC 가 raw markdown → artifact 경로로 실 사용 + ctx review 모드 가 typed fact 처리 + cross-PARA query 1개 use case 검증 + **AXEV 의 Area 4 (Finance / BOD / Legal / License) seed 완료 + 첫 Project close 가 적어도 1 Area 로 dispatch 검증**. **Strategic 의미**: AI-native fund global best-in-class infra ambition 의 knowledge tier 도달. 현재 dev-co level infra (per-customer isolation / blue-green / OAuth-RP / restic backup) 위에 PKM tool 수준 knowledge layer 가 있던 격차 해소. ## M7 — index 투자 도메인 backend **목표**: AXE Labs 7번째 vertical 서비스 — 펀드 운용 lifecycle (소싱 → 실사 → IC → 포트폴리오 → 엑싯 → LP IR) 의 typed fact SoT. frame (회계) / hive (HR) 와 같은 layer. Blueprint 의 5 skill (`ic` · `due-diligence` · `vc-deal-sourcing` · `portfolio-management` · `investor-relations`) 의 dataroom-bound run-time 산출물 (memo markdown / xlsx / yaml) 을 persistent typed artifact + cross-deal / cross-fund / time-travel query 가능한 형태로 승격. **핵심 차별성** (frame/hive 와 다른 본질): - **financial_model 6-table SoT** — xlsx 가 일급 entity 가 아니라 (driver / scenario / driver_value / output / exit_matrix_leaf) 의 typed model. DSL formula + topo evaluator 가 xlsx replay. ic skill 의 4 sanity check 가 DB CHECK 로 흡수 - **fund_investment(deal × fund × round) 3차원 N:M** — 한 deal 이 여러 펀드에서 동시 + 다른 시점 follow-on + 다른 instrument 로 들어갈 수 있음. 펀드별 IRR 독립 산출 - **Day 1 artifact-first** ([D-cortex-7](/ops/decisions) precedent 미러) — Rust + axum + sqlx + Postgres 16 RLS **근거 결정**: [D-index-1~10](/ops/decisions) (본 마일스톤 등재 동반, 2026-05-27). 핵심 결정은 D-index-4 (financial_model 6-table) + D-index-5 (fund_investment N:M). **아키텍처**: [/services/index](/services/index) 참조 — 서비스 main + 4 sub-page (financial-model / schema-catalog / skill-evolution). **3 Phase 구조**: | Phase | 산출 | 기간 | |---|---|---| | **0. Skeleton + 3 deal fixture** | `cp -R cortex index` byte-by-byte → Azure app + customers.yaml + docker-compose (postgres 4000 + mcp 4010/4011 + Caddy 4012) + cloudflared `^/index(/.*)?$` + MCP checklist 14 항목 + Phase 0 schema set (target_company · deal · fund_investment · financial_model 6-table + cortex artifact 4 테이블) + DSL parser + topo evaluator + 4 tools (`register_target_company` · `create_deal` · `propose_financial_model` · `compute_outputs`) + `/index/schemas` (14 schemas) + **Iippo/Sentry/Canopy seed yaml ingest → IRR 산출 ic skill 결과와 ±1pp 일치 검증** | ~6일 | | **1. DD + IC + KPI 코어** | dd_finding / ic_decision / portfolio_kpi schema + ~15 tools + ic skill `--push-to-index` mode + Blueprint workspace ↔ deal N:1 매핑 + Blueprint MCP schema discovery 에 `index` 등재 + Blueprint UI 의 deal-level artifact citation resolver | ~2-3주 | | **2. Cross-fund + LP + frame mirror + Exit + IR** | lp_comm / valuation / exit_signal / postmortem + frame cross_journal_link mirror (pg_notify `index_events` → frame LISTEN, capital call / distribution 자동 분개) + Blueprint cross-PARA query_knowledge (deal × kpi × time) + portfolio dashboard in Blueprint + IR 자동 보고 (hive payslip pattern) | ~3-4주 | | **3. 판단기억 + portfolio-care + self-calibration** (2026-06, post-v1) | (Phase 0–2 의 financial_model·IC·typed-fact 위 — 이번 사이클에 **artifact-first 판단층 live**: [D-index-45](/ops/decisions) judgment layer · 46 seed=SoT · 47 skill ownership P0–P3.) 다음: **pmc 운영 파이프라인**(8-agent 분기 KPI/risk/NAV — post-money care → `index.portfolio_kpi`·`risk_alert`·`valuation_snapshot` typed fact) · **post-mortem 루프**(ic `postmortem_stub` → pmc fill → `index.postmortem`; IC 예측 vs 실제) · **Epic 3 self-calibrating**(prediction freeze → outcome 포착 → deal-class bias → 다음 IC 자동보정 — post-mortem 이 outcome 공급) · **Epic 2 confidence load-bearing** · **evidence durability**(citation path→`drive_item_id`/`sha256`; OneDrive 소스 이동·삭제에도 provenance 생존 — 현재 107 path·0 sha256 = 끊김 위험) | ~3-5주 | **의존성**: - [M2](#m2--frame-회계-도메인-확장-axev-펀드-회계) frame 펀드 회계 확장 — Phase 2 의 cross_journal_link mirror 가 frame 측 commitment_ledger 필요. M2 미완 시 Phase 0/1 stand-alone 가동 가능 (event_outbox 보류) - [M6](#m6--blueprint-artifact--para-지식-레이어) Blueprint artifact + PARA — Phase 1 의 schema discovery 가 Blueprint M6 Stage 1 (DB 진입) 정합. 평행 가능 - [backlog](/ops/backlog) 의 M7 태그 항목들 **완료 정의**: - (a) Phase 0: 3 deal (Iippo / Sentry / Canopy) seed yaml ingest 성공 + ic skill 의 `irr_analysis xlsx` 결과와 index `compute_outputs(model_id, scenario_id)` 결과 ±1pp 일치 + Iippo 1호 / 2호 IRR 각각 독립 산출 가능 - (b) Phase 1: 새 deal 1건이 ic skill 끝 → `--push-to-index` 로 typed fact propose → ctx review confirm → Blueprint artifact citation 으로 resolve - (c) Phase 2: AXEV 펀드 1호 의 quarterly NAV report 가 100% index typed fact 위에서 산출 (markdown 자동 render — LP 보고 audit trail 보존) **Strategic 의미**: AXE 의 본업 (펀드 운용) 이 finally 1st-class platform service 화. frame/hive 가 운영 backbone, index 가 본업 SoT. ic skill 의 markdown/xlsx 산출이 휘발성 artifact 에서 cross-deal / time-travel query 가능한 typed fact 로 진화 — IC 결정의 audit trail 영구 + LP 의 모든 숫자가 DB cell trace. --- ## 마일스톤 라이프사이클 | 단계 | 표시 | 다음 단계 조건 | |---|---|---| | **📐 설계** | 아키텍처/결정 미확정 | decisions.mdx 에 D-마일스톤-N 등재 | | **🔧 개발** | backlog 항목 진행 중 | 모든 backlog M-태그 항목 ✅ | | **🎯 검증** | 기능 끝, 운영 검증 | 운영 회고 + 완료 정의 충족 | | **✅ 완료** | 운영 안착 | (1~2 분기 후 archive) | | **⏸️ 보류** | 의존성/외부 차단 | 차단 해소 시 재개 | ## 변경 이력 | 일자 | 변경 | |---|---| | 2026-05-22 | 페이지 신설 + M1~M5 초기 등재 ([D-docs-roadmap-1](/ops/decisions)) | | 2026-05-27 | M7 (index 투자 도메인 backend) 신설 ([D-index-1~10](/ops/decisions)) — 3 Phase + Iippo/Sentry/Canopy fixture | --- # Cloudflared 재기동 > cloudflared 가 죽었거나 config 변경 시 5초 다운타임 절차. URL: https://docs.axelabs.ai/ops/runbook/cloudflared # Cloudflared 재기동 ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/cloudflared 따라 axelabs-tunnel 재기동 진행해줘. 진행: 1. 현재 상태 진단 — `docker ps -a | grep axelabs-tunnel` + 외부 endpoint health (`curl -sI https://axe.axelabs.ai/frame/health`) 2. 페이지 시나리오 1 (죽음 감지) / 시나리오 2 (ingress config 변경) 중 분기 식별 3. 페이지의 각 명령 실행 + 검증, 매 step 결과 받고 다음. **절대 SIGHUP 금지** (cloudflared graceful reload 미지원) — docker restart 만 사용, 5초 다운타임 발생 사용자 사전 확인 4. 함정 — 본 runbook 은 axe-macmini 중앙 tunnel 만 다룸, customer 별 tunnel (`axelabs-{customer}`) 은 `axe onboard --apply` step 7 이 customer macmini 의 config 자동 push 5. 외부 endpoint 200 검증 + (선택) /ops/updates Ship Log ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. `axelabs-tunnel` (cloudflared) 가 죽거나 ingress config 변경이 필요할 때. > **Tunnel layer 구분** (2026-05-23 정정): AXE 플랫폼은 **2 종류 cloudflared tunnel** 운용. 본 runbook 은 axe-macmini 의 **중앙 tunnel** (`axelabs-tunnel` 컨테이너 — `axe.axelabs.ai` / `docs.axelabs.ai` / `admin.axelabs.ai` 등 운영자 자기 서비스) 만 다룸. **customer 별 tunnel** (`axelabs-realchoice` 등 — `{customer}.axelabs.ai` 서빙) 은 `axe onboard` 가 customer macmini 의 launchd `com.axelabs.{customer}.cloudflared` 로 부팅하며 본 runbook 적용 안 됨. customer tunnel ingress 추가 시 `axe onboard --apply` 가 step 7 에서 자동 push. ## 절대 SIGHUP 금지 ``` ❌ docker kill -s HUP axelabs-tunnel ``` **검증된 사실 (2026-05-15)**: cloudflared 는 SIGHUP graceful reload **미지원**. SIGHUP 받으면 프로세스 종료. → docker restart 만 사용. 5초 다운타임 발생. ## 시나리오 1 — 죽음 감지 `com.axe.health-check` 또는 운영자 콘솔에서 cloudflared 죽음 감지: ```bash # 상태 확인 docker ps -a | grep axelabs-tunnel # Exited (1) 또는 Dead 표시 # 재시작 docker start axelabs-tunnel # 5초 대기 후 검증 sleep 5 curl -sI https://axe.axelabs.ai/frame/health # 200 OK ``` ## 시나리오 2 — Ingress config 변경 신규 customer 추가 또는 path 추가 시: ```bash # 1. config 편집 vim /Users/axe/.axe/tunnels/axelabs/config.yml # 2. 검증 docker run --rm -v /Users/axe/.axe/tunnels/axelabs:/etc/cloudflared cloudflare/cloudflared:latest \ tunnel --config /etc/cloudflared/config.yml ingress validate # OK 면 # 3. 재시작 (다운타임 ~5s) docker restart axelabs-tunnel # 4. 새 ingress 동작 확인 sleep 5 curl -sI https://<new-host>/<path> ``` ## 시나리오 3 — credentials 만료 / 회전 cloudflared 의 tunnel credentials (`d8efecdd-2c3f-42de-9925-501433e21394.json`) 는 일반적으로 만료 없음. 단, 의심 노출 시: ```bash # 1. Cloudflare dashboard → Zero Trust → Networks → Tunnels → axelabs-tunnel → Configure → Rotate token # 2. 새 credentials 파일 받아서 macmini 에 push scp credentials.json axe-macmini:/Users/axe/.axe/tunnels/axelabs/ # 3. 재시작 docker restart axelabs-tunnel ``` ## Ingress 변경 시 함정 | 함정 | 결과 | 회피 | |---|---|---| | docker kill -s HUP | process 종료 | `docker restart` | | frame upstream 을 cloudflared 에 직접 (host:port) | blue/green swap 불가 | axe-frame-proxy 경유 | | path strip 기대 | 라우터에서 prefix 못 찾음 | 서비스에 `/frame` mount | | catch-all (404) 라인 제거 | unhandled 호스트 가 임의 origin 으로 | 마지막 `service: http_status:404` 유지 | ## 다운타임 분산 운영 cloudflared 재시작 = **5초** 다운타임 (모든 customer 의 모든 서비스). 따라서: - 가능한 한 **새벽 시간 (03:00 KST)** 에 수행 - 사전 알림 (customer admin 들에게 24h 전) - 한 번에 여러 변경 묶어서 (재시작 1회 분량) ## 자동화 모니터링 (TBD) 현재는 운영자 콘솔 (`https://admin.axelabs.ai`, `com.axe.console.refresh` 매시 rebuild) 에서 health probe 결과 확인. 매분 단위 자동 알림 (`com.axe.health-check` launchd + `axe-health-monitor` script) 는 **향후 작업 항목**. 대안 (즉시 구현 가능): ```bash # /Users/axe/.axe/bin/check-cloudflared.sh (TODO: 실제 launchd 등록) if ! curl -sf https://axe.axelabs.ai/frame/health > /dev/null; then osascript -e 'display notification "cloudflared 또는 frame 죽음" with title "AXE Labs"' fi ``` ## 다른 cloudflared 인스턴스 축전된 다른 tunnel 들: | Tunnel | 컨테이너 | ingress | |---|---|---| | axelabs-tunnel | `axelabs-tunnel` (Docker) | 플랫폼 (`*.axelabs.ai`) | | cortex-tunnel | `com.cortex.cloudflared` (launchd) | cortex.axellc.com + mysrt.axellc.com | | (기타 customer 측) | (각 customer macmini) | 각 customer 의 자체 | 각각 독립 운영. cortex-tunnel 죽음 ≠ axelabs-tunnel 죽음. --- # 신규 Customer Onboarding > 운영자 측 자동화 + 수동 touchpoints. D-day = vault 비번 + axe 2~3 줄. URL: https://docs.axelabs.ai/ops/runbook/customer-onboarding # 신규 Customer Onboarding ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/customer-onboarding 따라 신규 customer ([customer id]) D-day onboard 진행해줘. 진행: 1. D-day prerequisite 확인 — vault session unlock (bw + Keychain `axe.vault.session`, 8h cache) + customer IT 회신 `axelabs-bootstrap-{customer}.json` pack 수령 + customer macmini Tailscale ACTIVE direct 2. 페이지 Phase B 표 따라 5 step 중 현재 자동화 상태 분기 — `axe customers ingest` → `axe onboard --apply` → `axe deploy blueprint --apply` → `axe deploy hive --apply` (umbrella 미구현 시 순차) 3. 각 axe 명령은 dry-run (`--apply` 생략) 먼저 실행 + 변경 계획 사용자 확인 후 `--apply`. 매 step 결과 받고 다음. SSH 진입 / cloudflared ingress / vault collection 권한 점검 포함 4. 함정 발생 시 [/ops/known-gaps](/ops/known-gaps) 의 "D-day traps" 섹션 표 따라 우회 (Tailscale short alias / SSH non-login PATH / TXT+CNAME 공존 / keychain partition / bw cache stale / vault DOMAIN path / docker volume external 부재 / customers.yaml directory 잘못 생성 등 15+ 함정) 5. 5 step 완료 후 customer 측 health 검증 (frame/blueprint/hive 외부 endpoint 200) + Ship Log 한 줄 + customer IT 회신 ("배포 완료, 접속 안내") ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. 신규 customer (예: realchoice) 의 macmini 에 AXE Labs 스택 전체를 배포하는 절차. ## D-day TLDR — vault 먼저 풀어두면 1 명령 > **목표** ([B-onboard-1shot](/ops/backlog)): D-day 운영자 입력 = **axe 명령 1 줄**. > vault 의 `bw unlock` 은 **session prerequisite** (8 시간 cache) — D-day touchpoint 아님. ### Phase A — 세션 prerequisite (한 번, 이후 8 시간 자동) | 작업 | 명령 | |---|---| | vault session unlock + Keychain cache | `bw unlock --raw \| security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$(cat)" -U` | 이후 모든 `axe secret`/`axe onboard`/`axe deploy` 명령은 Keychain 의 BW_SESSION 자동 사용. 비번 재입력 없음. ### Phase B — Deploy touchpoint (umbrella 후 1 명령, 현재 5 명령) | # | 운영자가 손대는 부분 | 자동화 | backlog | |---|---|---|---| | 0 | customer IT 에게 docs link 4 개 송부 (D-7) | ✅ 자력 — bootstrap.sh 가 docs.axelabs.ai 의 raw 로 노출 (2026-05-23 해소) | [B-onboard-bootstrap-publish](/ops/backlog) ✅ | | 1 | `customers.yaml` 의 `{customer}` 블록 + services 슬롯 작성 | `axe customers add` = stub | [B-onboard-customers-add](/ops/backlog) | | 2 | customer IT pack.json → vault + customers.yaml 흡수 | ✅ `axe customers ingest {id} {pack}` (2026-05-23 구현) — dry-run/`--apply` + ruamel round-trip (comment 보존) + idempotent | [B-onboard-azure-pack](/ops/backlog) ✅ | | 3 | `axe onboard {customer} --apply` (cloudflared + frame, 18-step) | ✅ 자동 | — | | 4 | `axe deploy blueprint {customer} --apply` (Blueprint, 11-step) | ✅ 자동 (SSO 미설정 부팅) | [B-onboard-bp-sso](/ops/backlog) | | 5 | Hive stack 수동 (ssh + docker compose) | `axe deploy hive` 부재 | [B-onboard-hive-deploy](/ops/backlog) | → 위 5 개를 묶는 **umbrella 명령** [B-onboard-umbrella](/ops/backlog) 가 구현되면: ```bash # 세션 prerequisite (8 시간 1 회) bw unlock --raw | security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$(cat)" -U # D-day — 줄 1 개 axe deploy customer {customer} --from-pack ~/Downloads/axelabs-bootstrap-{customer}.json --apply ``` 운영자 D-day 입력 = **0 회** (vault 살아있으면) 또는 **1 회** (vault 만료 시 unlock 한 번). > 진정한 0 입력 D-day = (a) cron 이 `bw unlock` keep-alive 유지 + (b) umbrella 구현 + (c) pack.json 사전 수신. (c) 는 customer IT 측이 axelabs-bootstrap.sh 실행해서 안전채널로 보내주는 시점 의존 — 즉 운영자는 IT 회신만 기다리면 됨. ## D-day 실제 명령 (2026-05-26 시점, B-customer-deploy-generalization Phase 1 ✅) ```bash # 0. 운영자 vault unlock (master password 1 회) export BW_SESSION="$(bw unlock --raw)" security add-generic-password -s axe.vault.session -a ai@axellc.com -w "$BW_SESSION" -U # 1. customer IT 회신 받은 axelabs-bootstrap-{customer}.json 흡수 (one-shot) # sso.tenant_id + sso.apps.{}.client_id fill + services manifest 슬롯 추가 # + vault push × 3 (3 client_secret) 묶음 axe customers ingest {customer} ~/Downloads/axelabs-bootstrap-{customer}.json --apply # (dry-run 으로 먼저 확인하려면 --apply 빼고 실행 → 변경 계획만 출력) # 2. Cloudflare tunnel + DNS + cloudflared launchd 부트스트랩 (1-9 step) axe onboard {customer} --apply --skip-frame # --skip-frame: frame stack 은 별도 `axe deploy frame` 로. 이전 18-step 묶음 패턴 폐기. # 3. customer 측 bw vault session bootstrap (interactive 1회 — customer IT 가 실행) # operator 가 SSH 으로 helper 를 push 한 후 customer IT 에게 실행 요청: scp /Users/axe/.axe/bw-bootstrap.sh {customer}-macmini:~/bw-bootstrap.sh ssh -t {customer}-macmini "brew install bitwarden-cli && \ ~/bw-bootstrap.sh https:// " # → ~/.bw-session (mode 600) 생성. wrapper 가 SSH non-interactive 호출 가능해짐. # 4. service 3종 customer-측 deploy (각각 13-step, ~5-15 분 each) axe deploy frame {customer} --apply axe deploy blueprint {customer} --apply axe deploy hive {customer} --apply ``` > ⚠️ **위 `{customer}` 는 `services:` 매니페스트를 선언한 *신규* customer 자리** — sovereignty/self-deploy 로 졸업한 고객(realchoice/Truvia)은 **대상 아님**. 그 고객은 자기 macmini 에서 secret·compose·Caddy 를 **자체 배포**하고, `customers.yaml` 에 `services:` 매니페스트가 *의도적으로* 없어 `axe deploy {svc} {customer}` 가 `services … not declared` 로 `sys.exit` 한다. 운영자 역할 = **software supply (code/image) + 외부 노출 (DNS·cloudflared catch-all)** 뿐. 근거: [/ops/decisions](/ops/decisions) (D-ops-40 / B-customer-sovereignty-architecture). 각 `axe deploy {service} {customer}` 가 동일한 13-step 흐름 수행: preflight → clone → vault_check → secrets_bootstrap (auto-gen if missing) → env_local → wrapper push → network → (blueprint 만) frame_mcp_token → compose up → (frame 만) proxy push → health → (frame+hive) register_entities → ingress swap. **`axe onboard --skip-frame` 권장 이유**: 이전 18-step 통합 흐름은 frame stack 까지 묶였으나, 이제 customer-측 deploy 가 일반화되어 `axe deploy frame {customer}` 로 분리 호출이 idempotent + 재시도 안전. onboard 는 cloudflared layer 만 담당. ## D-14 ~ D-7 (사전 협의) | 작업 | 책임 | |---|---| | 1. 가격·SLA 협의 | 운영자 | | 2. customer ID 정함 (`x`, `newco` 등) | 운영자 | | 3. customer 측 IT 담당자 식별 | customer | | 4. macmini 구매 (M2/M3, 16GB+, 512GB+) | customer | | 5. Tailscale 가입 + key 교환 | 양측 | | 6. customer 측 Microsoft 365 tenant 확인 | customer | | 7. customer 측 도메인 검증 준비 (TXT record 추가 권한) | customer IT | | 8. axelabs.ai zone 의 `{customer}` A record 추가 | 운영자 (Cloudflare) | ## D-7 (Azure 사전 등록 — customer IT 자력) 운영자가 customer IT 에게 보내는 것 = **링크 4 개** (별도 첨부 / 스크립트 메시지 X — 2026-05-23 `B-onboard-bootstrap-publish` 해소): ``` https://docs.axelabs.ai/partner ← 4 단계 흐름 (entry) https://docs.axelabs.ai/partner/macmini-prep ← macmini 사전 준비 (Tailscale, SSH, 절전) https://docs.axelabs.ai/partner/registration ← Option A CLI 1 줄 (axelabs-bootstrap.sh) https://docs.axelabs.ai/partner/handoff ← JSON pack 회신 양식 ``` customer IT 가 30 분 (Option A) ~ 45 분 (Option B) 작업 → `axelabs-bootstrap-{customer}.json` 회신 (또는 텍스트 8 개 값). JSON pack 스키마: `axelabs-bootstrap/v1` — `tenant_id` + `apps.blueprint/vaultwarden/frame_mcp` 각각의 `client_id` + `client_secret` + (frame_mcp 만) `application_id_uri` + `scope` + `redirect_uris`. 운영자가 받으면 [B-onboard-azure-pack](/ops/backlog) 의 `axe customer ingest` 가 1 줄로 흡수 (구현 후) — 현재는 수동 paste + `axe secret push` × 3. 운영자가 받으면: 1. **client_secret 3개 → vault 로 push** (Keychain 직접 push 는 폐기 — 비밀의 SoT = Vaultwarden, [D-ops-17](/ops/decisions#d-ops-17--secret-deploy-time-pull)): ```bash axe secret push AZURE_{CUSTOMER}_BLUEPRINT_CLIENT_SECRET --service blueprint --customer {customer} axe secret push AZURE_{CUSTOMER}_VAULTWARDEN_CLIENT_SECRET --service vaultwarden --customer {customer} axe secret push AZURE_{CUSTOMER}_FRAME_MCP_CLIENT_SECRET --service frame --customer {customer} ``` (각 명령은 `Value for ...:` 프롬프트 → 안전채널로 받은 값 붙여넣기. 매니페스트에 customer service 슬롯이 사전 등재돼 있어야 함 — [B-onboard-azure-pack](/ops/backlog).) 2. `customers.yaml` 에 entry 추가: ```yaml {customer}: legal_name: "<...>" primary_domain: "<...>" public_domain: "{customer}.axelabs.ai" entities: ["<...>"] tailscale_host: "{customer}-macmini" sso: provider: "microsoft_entra_id" tenant_id: "<...>" apps: blueprint: client_id: "<...>" client_secret_env: "AZURE_{CUSTOMER}_BLUEPRINT_CLIENT_SECRET" redirect_uri: "https://{customer}.axelabs.ai/api/auth/callback/azure-ad" vaultwarden: client_id: "<...>" client_secret_env: "AZURE_{CUSTOMER}_VAULTWARDEN_CLIENT_SECRET" redirect_uri: "https://{customer}.axelabs.ai/vault/identity/connect/oidc-signin" frame_mcp: client_id: "<...>" client_secret_env: "AZURE_{CUSTOMER}_FRAME_MCP_CLIENT_SECRET" application_id_uri: "https://{customer}.axelabs.ai/frame/mcp" redirect_uris: - "https://claude.ai/api/mcp/auth_callback" - "https://claude.com/api/mcp/auth_callback" scopes: - "openid" - "profile" - "email" - "https://{customer}.axelabs.ai/frame/mcp/mcp.access" user_entity_map: {} default_entities_by_domain: "<primary_domain>": ["<entity>"] onboarded: "<date>" ``` 3. ~~cloudflared 중앙 tunnel ingress 편집~~ — **불필요** ([2026-05-23 drift 정정](/ops/known-gaps)). `axe onboard` 가 **customer 별 독립 tunnel** 을 customer macmini 에 생성/launchd 등록함 (`_render_cloudflared_config` in `/Users/axe/.axe/bin/axe`). 중앙 `/Users/axe/.axe/tunnels/axelabs/config.yml` 편집 의 D-7 의무는 사라짐. axe macmini 측 cloudflared 는 docs.axelabs.ai / admin.axelabs.ai 등 운영자 자기 서비스 전용. 4. ~~cloudflared 재시작~~ — 위와 동일. customer 측 tunnel 은 `axe onboard` step 8 의 launchd 가 자동 boot. 5. Cloudflare zone 의 axelabs.ai 에서 `{customer}` A record 추가 — **불필요**. `axe onboard` step 5 가 Cloudflare API 로 자동 CNAME 생성 (`.cfargotunnel.com`). ## D-day (~1 시간, 자동) 운영자 머신에서 — 상단 [D-day TLDR](#d-day-tldr--운영자-수동-touchpoints) 참조. 핵심 흐름: ```bash # Dry-run 으로 확인 (변경 0) axe onboard {customer} # 실제 적용 (cloudflared + frame) axe onboard {customer} --apply # Blueprint (별도 명령 — 통합은 B-onboard-1shot) axe deploy blueprint {customer} --apply ``` `axe onboard` 가 18-step 을 순차 실행 (Tailscale SSH 로 customer macmini 에 push): | Step | 작업 (실제 axe CLI 코드 기준) | |---|---| | 1 | Tailscale up / customer macmini reachable 확인 | | 2 | customer meta 검증 (customers.yaml entry, Tailscale FQDN) | | 3 | SSH probe (key 인증) | | 4 | cloudflared 터널 생성 (기존 있으면 reuse) | | 5 | Cloudflare DNS A record 생성 | | 6 | cloudflared credentials.json push → macmini | | 7 | cloudflared config.yml push (ingress 규칙 포함) | | 8 | cloudflared launchd plist 등록 + boot | | 9 | cloudflared health check (curl /cdn-cgi/trace) | | 10 | frame git clone + Dockerfile preflight | | 11 | frame git submodule + asset 동기화 | | 12 | macmini Keychain 에 secret push (vault, frame, blueprint, restic) | | 13 | macmini .env.local 생성 (env_file 용) | | 14 | docker compose up (postgres + frame-mcp-blue + frame-mcp-green + proxy) | | 15 | frame-mcp /health/ready poll (max 60s) | | 16 | `frame register-entity` × 각 entity + `frame migrate` | | 17 | axe-frame-proxy alias = blue 로 설정 | | 18 | cloudflared ingress alias swap (axe-frame-proxy 가 frame upstream) | 진행 중 어느 step 에서 실패하면 자동 rollback (기존 상태 유지). 운영자 화면에 명확한 오류 + 다음 단계 제시. ## D+1 (검증 + Vault 부트스트랩) 운영자 (`ai@axellc.com`) 측에서: ```bash # 외부 health axe health {customer} # customer macmini ssh ssh {customer}-macmini "docker ps" # Blueprint admin 부여 ssh {customer}-macmini "docker exec blueprint-app pnpm prisma db execute --stdin" <' --kind corporate --accounting-standard ksme" # 첫 hive entity 등록 (frame mirror) ssh {customer}-macmini "docker exec hive-mcp-blue python -m hive.cli register-entity --id <entity_id> --legal-name '<법인명>'" # PII passphrase 2개 vault push + frame/hive deploy (axe customer 의 deploy-axep.sh 패턴) # 일반화된 deploy-new-entity.sh 가 자동 처리 (D-ops-31 후속 일반화): # bash /Users/axe/.axe/vault/deploy-new-entity.sh <customer> <entity_id> "<법인명>" ``` ### Vault 부트스트랩 — `axe vault bootstrap {customer}` ([D-ops-33](/ops/decisions)) 운영자 mac 에서 1 명령: ```bash # Dry-run (default) — 현재 4 key 적용 상태 진단 axe vault bootstrap {customer} # 실 패치 + axe-vaultwarden force-recreate axe vault bootstrap {customer} --apply ``` 명령 자동 흐름: 1. SSH 로 customer macmini 의 `/Users/{ssh_user}/.axe/vault/docker-compose.yml` 읽음 (axe customer 는 local exec) 2. 4 key (D-ops-24/26/27) 적용 상태 진단 — 이미 OK 면 skip 3. 누락 key 만 patch (in-place, anchor = `SSO_SIGNUPS_MATCH_EMAIL` 다음 줄) 4. `docker compose up -d --force-recreate axe-vaultwarden` 5. Idempotent — 재실행 안전 적용되는 4 key: ```yaml SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true" # D-ops-24 SIGNUPS_ALLOWED: "true" # D-ops-26 SSO_ONLY: "false" # D-ops-27 — emergency MP fallback 보존 ORGANIZATION_INVITE_AUTO_ACCEPT: "true" # D-ops-27 — 직원 Accept 자동 ``` (`INVITATIONS_ALLOWED=false` 는 기존 docker-compose 의 default — JIT 통일.) 이 명령 빠지면 첫 employee 가 "Failed to retrieve the invitation" / "Your provider does not send email verification status" 등 차단 ([D-ops-24](/ops/decisions), [D-ops-26](/ops/decisions), [D-ops-27](/ops/decisions) 참조). ## D+2 (직원 onboarding + Vault 첫 멤버 invite) ### 직원 SSO 안내 운영자 → customer admin 으로 [신규 직원 온보딩 URL](/onboard) 전달. customer admin 이 자기 직원들에게 안내. 각자 [Frame connector 4-step setup](/onboard/claude-frame-setup) 따라 설정. Vault 접속 안내 ([troubleshooting](/onboard/troubleshooting) 의 Vault 섹션 참조): - 직링크: `https://{customer}.axelabs.ai/vault/#/sso` - Identifier: 그 customer 의 org 이름 (예: AXE, REALCHOICE) ### Vault Organization + 직원 invite customer admin (또는 운영자 SSH proxy) 가: 1. Web vault → Admin Console → org 생성 (`{CUSTOMER}` 대문자 권장, 예: AXE / REALCHOICE) 2. **dual-identity 권장 패턴** ([D-ops-29](/ops/decisions)): - `ai@{customer-domain}` = 자동화 / bot identity (Graph token 발행, proactive DM, agent run) - 실제 admin 직원 별도 계정 = 사람 작업 identity - 둘 다 vault Owner + access_all=1 3. 직원 invite — `/Users/axe/.axe/vault/invite-members.sh` 패턴 (customer 별 변수만 교체) 4. **Collection 구조 v1** ([D-ops-32](/ops/decisions)): - entity 1개 customer (예: realchoice): 4-collection 단순 구조 - `Platform — Service Secrets` (operator only) - `Platform — Infrastructure` (operator only) - `{Customer Entity}` (전직원 RW) - `Shared Tools` (전직원 RW) - entity 다수 customer (예: axe 의 axec/axev/axep): 6-collection (entity별 분리) ## D+7 (안정화 점검) - frame `audit_log` 활성? (실제 분개 1건 이상 발생?) - backup 정상? (`restic -r /Users/axe/.axe/backups/local snapshots --password-file ...`) - 직원 첫 시도 시 누락 없음? ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | customers.yaml entry 빠뜨림 | onboard 18-step 실패 | step 2 가 자동 검증 | | ~~Cloudflare A record 없음~~ | (해소 — `axe onboard` step 5 가 자동 생성) | — | | customer macmini sleep mode | onboard SSH 실패 | D-7 까지 절전 안 함 설정 | | Tailscale key 교환 안 됨 | step 1 실패 | D-7 까지 양방향 ping 검증 | | **customers.yaml `services:` 섹션에 customer 슬롯 없음** | `axe secret push` 가 매니페스트 lookup 실패 (`{env_name} not in manifest`) | `services.{svc}.{customer}.secrets[]` 슬롯 사전 등재. 현재 `axe` 만 등재됨 — realchoice 는 신규 작업 ([B-onboard-azure-pack](/ops/backlog)) | | **Cloudflare API token vault 미등재** | `axe onboard` step 4 의 `_cf_token()` 실패 | 첫 onboard 전 1 회 `axe secret push` 로 등재 ([B-onboard-cf-token-doc](/ops/backlog)) | | **Blueprint 가 SSO 미설정으로 부팅** | `axe deploy blueprint` 후 첫 로그인 실패 | 현재 `cmd_deploy_blueprint` docstring 명시 한계. customer Keychain 에 Azure secret 별도 push 필요 ([B-onboard-bp-sso](/ops/backlog)) | | **Hive stack 자동 배포 명령 부재** | `axe deploy hive` 가 없어서 매번 ssh + docker compose 수동 | [B-onboard-hive-deploy](/ops/backlog) | | Vault `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION` 미설정 | 첫 employee SSO "Your provider does not send email verification status" 차단 | D+1 의 Vault 부트스트랩 4-block ([D-ops-24](/ops/decisions)) | | Vault `SIGNUPS_ALLOWED=false` + `INVITATIONS_ALLOWED=false` | 첫 employee JIT 가입 "Failed to retrieve the invitation" 차단 | D+1 의 4-block ([D-ops-26](/ops/decisions)) | | Vault `SSO_ONLY=true` 설정 | Entra 장애 시 운영자 lockout (master password 진입 불가) | `SSO_ONLY: "false"` 유지 ([D-ops-27](/ops/decisions)) | | Vault UI 이메일-first 화면이 SSO Identifier 가림 | 직원이 가입 절차 못 찾음 | `/vault/#/sso` 직링크 안내 ([troubleshooting](/onboard/troubleshooting)) | | frame/hive entity 추가 시 PII passphrase 빠뜨림 | 그 entity 의 PII 작업 시점에 RuntimeError | `deploy-new-entity.sh` 가 PII passphrase 2개 vault push + ship 자동화 ([D-ops-31](/ops/decisions)) | | customer Vaultwarden 의 bw CLI 자동화 시 BW_SESSION 직접 export 만 함 | `axe secret push` 실패 (keychain 만 읽음) | `security add-generic-password -s axe.vault.session -a ai@... -w "$BW_SESSION" -U` 단계 통합 ([D-ops-31](/ops/decisions) 부수 발견) | | Vaultwarden Timshel 1.34.1-6 + bw CLI 2026.x | `TypeError: ... toWrappedAccountCryptographicState` crash | bw CLI 2025.7.0 pin (`npm install -g @bitwarden/cli@2025.7.0`). 항구 해소: [B-vault-upstream-migration](/ops/backlog) — dani-garcia/vaultwarden:1.36.0 native SSO ([D-ops-28](/ops/decisions)) | ## 다음 customer - [신규 직원 등록](/ops/runbook/employee-onboarding) - [Secret rotation](/ops/runbook/secret-rotation) --- # Frame DB 복구 > frame-postgres 손상/migration 사고/silent corruption 대응. URL: https://docs.axelabs.ai/ops/runbook/db-recovery # Frame DB 복구 ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/db-recovery 따라 frame DB 복구 진행해줘. 진행: 1. 현재 사고 진단 — 사용자 보고 증상으로 페이지 "시나리오 분류" 표의 5 시나리오 중 분기 식별 (migration / 사용자 실수 / macmini 손실 / silent corruption / postgres crash) 2. 즉시 대응 — frame container stop (사용자 추가 쓰기 차단) + restic 최신 snapshot 확인 3. 시나리오별 복구 절차 실행, 매 step 결과 받고 다음. destructive (drop database / restore from backup) 직전 사용자 확인 4. 함정 발생 시 페이지 본문 따라 우회 (Phase 5 stub `axe restore` 부재 → restic 직접 / silent corruption 의 부분 복원 범위 / migration rollback) 5. 정합성 검사 통과 (`integrity-check`) + frame 재기동 + (선택) /ops/known-gaps 사고 원인 한 줄 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. frame 의 PostgreSQL 데이터가 손상되었을 때 복구 절차. ## 시나리오 분류 | 사건 | 즉시 대응 | 복구 경로 | |---|---|---| | Migration 사고 (alembic 적용 후 정합성 깨짐) | 모든 frame container stop | local backup (Tier A) | | 사용자 실수 (잘못된 분개 대량 등록) | 해당 entity 만 freeze | `reverse_journal` × 다수 OR 백업 | | macmini 전체 손실 (도난·화재) | 새 macmini 셋업 | ring (Tier B) 또는 cold SSD (Tier C) | | Silent corruption (검증 실패) | 정합성 검사 결과 분석 | 부분 복원 | | Postgres 자체 crash | `docker restart frame-postgres` | 거의 자동 복구 | ## 시나리오 1 — Migration 사고 새 migration 적용 후 정합성 검사 실패: ```bash ssh axe-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec" # → FAIL: balance mismatch in journal X ``` ### 즉시 ```bash # 1. 모든 frame container stop (사용자 추가 쓰기 차단) ssh axe-macmini "cd /Users/axe/frame && docker compose stop frame-mcp-blue frame-mcp-green" # 2. Backup 최근 snapshot 확인 ssh axe-macmini " restic -r /Users/axe/.axe/backups/local snapshots \ --password-file <(security find-generic-password -w -s axe.backup.restic.local) " # 어제 03:00 KST snapshot 찾기 ``` ### 복구 ```bash # 3. 어제 backup 으로 frame-postgres 복원 # `axe restore` 는 Phase 5 stub 상태 → 현재는 restic 직접 호출: ssh axe-macmini " restic -r /Users/axe/.axe/backups/local \ --password-file <(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \ restore <snapshot_id> --target /tmp/restore --include 'frame-postgres/*' " # 그 후 docker exec frame-postgres pg_restore 또는 dump replay # 정식 `axe restore` 자동화는 Phase 5 (D) 작업 항목 # 4. frame container restart (이전 코드 버전으로!) ssh axe-macmini "cd /Users/axe/frame && git checkout <previous-good-commit>" ssh axe-macmini " cd /Users/axe/frame && set -a && source .env.local && set +a && docker compose build frame-mcp-blue && docker compose up -d --force-recreate frame-mcp-blue frame-mcp-green " # 5. 정합성 재확인 ssh axe-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec" # → OK ``` ### 손실 분석 복원 후 손실 = 백업 시점 (어제 03:00) ~ container stop 시점 사이의 모든 쓰기. ```bash # 손실된 audit_log 추출 (다른 백업 또는 in-memory) ssh axe-macmini "docker exec frame-postgres psql -U frame -d frame -c \" SELECT * FROM axec.audit_log WHERE ts > '2026-05-20 03:00:00+09:00' ORDER BY ts; \"" ``` 사용자에게 통지 + 수동 재입력 또는 잘못 적용된 migration 의 의도된 효과 분석. ## 시나리오 2 — 사용자 실수 회계사가 실수로 잘못된 분개 50건 등록: ### 우선 — `reverse_journal` 시도 ```python # Claude Code 에서 Frame:list_journals entity_id=axec, start_date=2026-05-20, status=posted # 잘못된 journal_id 목록 확인 후 for jid in [...]: Frame:reverse_journal entity_id=axec, journal_id=jid, memo="실수 역분" ``` `reverse_journal` 은 새 journal 생성 (append-only). audit_log 에 모두 기록. ### 대량 (50건+) — 백업 복원 50건 넘으면 reverse 절차도 부담. 백업 복원이 더 깔끔: ```bash # 1. 사용자의 다른 활동 멈추기 (claude.ai connector 비활성 요청) # 2. 가장 가까운 valid backup 복원 — table-level restore 자동화 TBD # 현재는 dump 전체 복원 후 손상 table 만 selective merge # (정식 도구화 = Phase 5 D 항목) ``` table-level 복원이 가능한지 확인 필요 — restic 자체는 file-level 만 → pg_dump 의 selective restore 필요. 향후 도구화 검토. ## 시나리오 3 — macmini 전체 손실 가장 심각. 새 macmini 에 처음부터 셋업. ### 절차 ```bash # 1. 새 macmini 준비 # - macOS 클린 install # - Tailscale 가입 + 운영자 key 등록 # - Docker Desktop install # - axe CLI install: bash <(curl -sSL https://docs.axelabs.ai/install/axe-cli.sh) # 2. customers.yaml 의 entry 그대로 사용 # (운영자 머신에서 push) # 3. 가장 신선한 백업 복원 (restic 직접 호출) # ring peer (realchoice macmini) 측에서 pull: ssh axe-macmini " restic -r sftp:realchoice@realchoice-macmini.tail090015.ts.net:/Users/realchoice/peer-backups/axe \ --password-file <(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \ restore latest --target /tmp/restore " # 또는 cold SSD 마운트 후 ssh axe-macmini " restic -r /Volumes/axe-cold-1/restic-repo \ --password-file <(종이메모) \ restore latest --target /tmp/restore " # 4. frame onboard (axe CLI 의 `--skip-azure` 플래그 는 향후 추가 예정; # 현재는 onboard 가 Azure 측 변경 자체를 하지 않으므로 일반 onboard 그대로) axe onboard axe --apply # 5. Microsoft 측 — redirect_uri 변경 불필요 (axe.axelabs.ai 도메인 그대로) # 6. 검증 axe health axe ssh axe-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axec" ssh axe-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity axev" # 7. 직원 통지 "frame 시스템 복구 완료. claude.ai 의 connector 는 그대로 작동합니다." ``` ## 시나리오 4 — Silent Corruption 매일 자동 `integrity-check` 가 잡아냄. 발견 즉시: 1. 어느 entity, 어느 검사가 실패했는지 확인 2. 가장 가까운 valid snapshot 찾기 (전날 또는 그 이전) 3. table-level 또는 entity-level 복원 4. 사용자에게 통지 (해당 기간 데이터 재입력 필요할 수도) ## Restore Drill (분기별) 매 분기 (Jan/Apr/Jul/Oct 15) 자동 drill 이 실행: 1. 임시 staging 컨테이너 (`frame-mcp-drill`) 에 어제 backup 복원 2. `integrity-check` 4개 모두 통과 여부 3. 결과 → 운영자 Slack drill 실패 = backup 자체 문제 (압축 손상, password 오류 등). 즉시 원인 분석. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | 손상 발견 후 frame 미정지 → 추가 쓰기 누적 | 손실 확대 | 즉시 `docker compose stop` | | ring peer 의 backup 도 손상되었을 가능성 | 양쪽 손상 | cold SSD 도 보유 | | 복원 후 정합성 검사 안 함 | 잠재 손상 미발견 | `integrity-check` 필수 | | pg_dump 의 schema-per-entity 옵션 누락 | 복원 시 entity schema 사라짐 | restic 의 raw file 복원 (PGDATA volume) | ## ⚠️ 현재 도구 상태 `axe restore --customer ... --tier ... --target ... --table ... --apply` 식의 통합 CLI 는 **Phase 5 (D) 작업 항목** — 현재 stub 상태. 본 runbook 의 실제 복구는 위에서처럼 **restic 직접 호출**. 자동화 완성 후 본 runbook 의 명령어 갱신 예정 — 그때까지 운영자는 restic CLI 직접 + docker exec 조합 사용. --- # Blue/Green Deploy > frame 무중단 배포 절차. URL: https://docs.axelabs.ai/ops/runbook/deploy # Blue/Green Deploy (frame) ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/deploy 따라 frame [customer] Blue/Green 배포해줘. 진행: 1. 현재 active color 확인 (`docker exec axe-frame-proxy` + network alias) + git log origin/main..HEAD 변경 미리보기 2. 자동 경로 (`axe deploy frame {customer} --apply`) 가능 판단, 안 되면 수동 9 step 분기 3. 페이지의 각 step 실행 + 매 step 결과 받고 다음. 특히 alias swap 직전 사용자 확인 (passive health 60s 통과 검증 후) + 기존 active stop 전 60s grace 대기 4. 함정 발생 시 페이지 본문 따라 우회 (health check 60s timeout / in-flight grace 미준수 / mcp-proxy upstream 미반영) 5. 새 active 첫 요청 검증 (`curl https://axe.axelabs.ai/frame/health` 200) + (선택) /ops/updates Ship Log ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. 다운타임 0 으로 frame 새 코드 적용. ## 자동 (권장) ```bash # Dry-run 으로 변경사항 미리 확인 axe deploy frame axe # 실제 적용 axe deploy frame axe --apply ``` `axe deploy frame {customer}` 가 자동으로 처리: | Step | 작업 | |---|---| | 1 | git pull origin main | | 2 | passive container 의 새 코드로 build (예: green 이 passive 면 green build) | | 3 | passive container start | | 4 | 헬스체크 통과 대기 (poll `/health/ready`, max 60s) | | 5 | docker network alias `frame-mcp` 이동 (active → passive) | | 6 | 새 active 의 첫 요청 정상 처리 검증 | | 7 | 기존 active (현재 passive) 의 in-flight 요청 60s grace | | 8 | 기존 active stop | | 9 | 운영자에게 deploy 완료 알림 | 총 5-7 분 (build 시간 포함). 사용자 측 다운타임: 0. ## 수동 (응급, axe CLI 사용 불가 시) ### 1. 현재 active color 확인 ```bash docker exec axe-frame-proxy cat /etc/caddy/Caddyfile # mount 위치 (frame-proxy/docker-compose.yml 의 :ro) | grep upstream # 또는 docker network inspect frame_default | grep -A 2 'frame-mcp"' # Aliases 에 `frame-mcp` 표시된 컨테이너가 active ``` active = blue 가정. ### 2. green 에 새 코드 build ```bash cd /Users/axe/frame git pull origin main set -a && source .env.local && set +a docker compose build frame-mcp-green docker compose up -d --force-recreate frame-mcp-green ``` ### 3. green health check ```bash sleep 10 curl -s http://localhost:3711/health/ready # → {"status":"ok"} ``` ### 4. alias 이동 (blue → green) ```bash # blue 에서 alias 제거 docker network disconnect frame_default frame-mcp-blue docker network connect frame_default frame-mcp-blue # alias 없이 다시 # green 에 alias 부여 docker network disconnect frame_default frame-mcp-green docker network connect --alias frame-mcp frame_default frame-mcp-green ``` > 위 절차는 axe-frame-proxy 의 upstream resolution 이 즉시 갱신. Caddy 가 alias 기반이라 DNS 캐시 없음. ### 5. axe-frame-proxy 갱신 확인 ```bash docker exec axe-frame-proxy cat /etc/caddy/Caddyfile # mount 위치 (frame-proxy/docker-compose.yml 의 :ro) | grep frame-mcp # upstream 이 frame-mcp:3710 그대로 (alias 가 green 으로 이동) ``` ### 6. blue 의 in-flight 요청 완료 대기 + stop ```bash sleep 60 # in-flight grace docker stop frame-mcp-blue # 다음 deploy 시 새 코드로 build 됨 (현재 passive) ``` ## 검증 ```bash # 외부 호출 확인 curl -s https://axe.axelabs.ai/frame/health # → 200 OK + service: frame-mcp # 새 코드 버전 확인 docker exec frame-mcp-green python -c "import frame; print(frame.__version__)" ``` ## Rollback (5초) 문제 발견 시 alias 만 되돌리면 끝: ```bash docker network disconnect frame_default frame-mcp-green docker network connect frame_default frame-mcp-green docker network connect --alias frame-mcp frame_default frame-mcp-blue ``` blue 가 다시 active. 새 코드는 green 컨테이너에 그대로 (다음 시도 시 fix 후 재배포). ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | `docker compose up --build` 단일 컨테이너로 (블루든 그린이든) | 30-60s 다운타임 | blue+green 동시 운영 필수 | | `--force-recreate` 없이 env 변경 적용 | 새 env 안 읽음 | 항상 `--force-recreate` | | customer macmini 에 docker daemon 죽음 | proxy 도 죽음 | `launchctl kickstart` 로 docker 재시작 | | build 실패 (Dockerfile 오류) | passive 컨테이너 stop | git revert + 재시도, blue 는 영향 없음 | ## 다른 서비스 - **blueprint** — 단일 컨테이너, 짧은 다운타임 수용 (`docker compose up -d --force-recreate blueprint`) - **vault** (Vaultwarden) — 단일 컨테이너, 다운타임 시 사용자 vault 로그인 불가 (rare op, 야간 작업) - **stream / magnet** — 각 customer 측 자체 운영 --- # 배포 SSOT 아키텍처 > origin/main 의 commit SHA = 배포의 진실원천. worktree 작업격리 + build-from-SHA + deploy lock + 스테이지 상태기계 + migration-validation 게이트로 다중 세션 배포 엉킴을 구조적으로 제거. URL: https://docs.axelabs.ai/ops/runbook/deploy-ssot # 배포 SSOT 아키텍처 — `origin/main` SHA = 배포 진실원천 > **원칙 (한 줄)**: `origin/main 의 commit SHA = 배포의 SSOT.` working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영이다. AXE 가 docs·backlog(matrix-postgres, [D-matrix-3](/ops/decisions))에 이미 적용 중인 SSOT 규율을 **배포**로 확장한 것. 결정 근거 = [D-ops-42](/ops/decisions). ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/deploy-ssot 따라 배포 SSOT 아키텍처를 rollout 해줘. 진행: 1. 현재 상태 진단 (어느 컴포넌트 A–F 가 이미 있고 어느 게 없는지 + axe CLI 버전관리 여부 + 지금 라이브 SHA vs origin/main drift) 2. additive 원칙 확인 — 각 컴포넌트는 기존 동작과 공존하게 추가 (기존 axe deploy/ship 경로 안 깨고) 3. 각 컴포넌트 추가마다 --dry-run + passive-color 카나리 (flip 안 함) 로 검증, flip 은 게이트 (사용자 확인) 4. migration 포함 릴리스면 ephemeral DB clone 검증 게이트 통과 후에만 flip 5. cutover (기본값 flip + pre-push 훅/branch protection 가드 활성화) 는 escape hatch 포함 + 사용자 확인 후 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## 동기 — 공유 tree + 공유 main 의 세 고장 다중 동시 Claude Code 세션이 repo 당 (frame / blueprint / hive / index / cortex / matrix) **1 working tree + 1 `main`** 을 공유한다. 여기서 세 고장이 동시에 난다: | 고장 | 메커니즘 | |---|---| | push 거부 | 타 세션이 먼저 push → 로컬 `main` 이 origin 과 diverge → non-fast-forward → push blocked | | WIP 혼재 | 서로 다른 feature 의 uncommitted 변경이 한 tree 에 누적 → 누구도 자기 slice 만 clean commit 불가 | | dirty 누출 | `axe deploy` 가 dirty working tree 에서 이미지 빌드 → uncommitted 코드 prod 누출 + push 전 배포 가능 | **실측 (2026-06-04)**: 이미 배포까지 끝난 blueprint fix (commit `b4067504`) 가 main diverge + 타 세션 WIP 로 수 시간 push 불가 → 운영자가 손으로 reconcile (stash → rebase → push). 같은 병의 가장 깊은 사례 = 운영자 `axe` CLI (364KB) 자체가 **버전 미관리 + in-place 편집** — 배포 도구인데 자기 자신은 SHA 추적 밖. ## 스테이지 파이프라인 blue/green 이 이미 canary/live/previous 3스테이지를 제공한다. 거기에 migration-validation 게이트 하나만 끼운다: ``` ┌─────────┐ ┌────────────────────┐ ┌──────────────────────┐ ┌────────────┐ ┌──────────────────────┐ │ built │ ───► │ canary │ ───► │ [migration-validation │ ───► │ live │ ───► │ previous │ │ : │ │ passive color │ │ 게이트] │ │ active │ │ 직전 active │ │ │ │ 트래픽 0 │ │ ephemeral DB clone 에 │ │ flip │ │ = 즉시 롤백 타깃 │ └─────────┘ │ health 검증 │ │ migration 적용·검증 │ │ (alias swap)│ └──────────────────────┘ ▲ └────────────────────┘ └──────────────────────┘ └────────────┘ │ │ │ fail │ rollback │ build-from-SHA (clean checkout) ▼ ▼ pushed SHA only flip 거부 alias 되돌림 (5초) ``` - **built** — pushed SHA 의 clean checkout 에서 이미지 빌드, 태그 `:`. push 안 된 SHA 는 여기 진입 거부. - **canary** — passive color 에 배포, 트래픽 0. health 검증만 (active 영향 0). - **migration-validation 게이트** — 스키마 변경 포함 시에만. prod DB 의 ephemeral clone 에 migration 적용·검증. fail = flip 거부. - **live** — active color flip (alias swap). 사용자 다운타임 0. - **previous** — 직전 active = 즉시 롤백 타깃 (alias 되돌림 5초, [Blue/green deploy](/ops/runbook/deploy) § Rollback). ## 컴포넌트 A–F — 각자 제거하는 고장 각 컴포넌트는 *특정 고장 하나*를 구조적으로 없앤다. ### A. 작업 격리 — `axe work ` 세션마다 origin/main 에서 분기한 git worktree (`~/.worktrees//`). 정규 repo 는 **fast-forward 전용 "main mirror"** 로 강등 (손편집 금지 — mirror 는 origin 의 읽기 투영일 뿐). **제거하는 고장**: 공유 tree 경합 · WIP 혼재 · stash 더미 = **발생 불가능**. 각 세션이 자기 tree 에서 자기 slice 만 commit. ### B. SHA 에서만 빌드 deploy 가 working tree 가 아니라 **pushed SHA 의 clean checkout** (`git worktree --detach ` 또는 `git archive`) 에서 이미지를 빌드한다. 이미지 태그 = `:`. push 안 된 SHA 는 배포 거부. **제거하는 고장**: deploy-before-push + dirty-tree 누출 = **구조적 불가능**. 빌드 입력이 origin 에 도달한 SHA 로 한정되므로 uncommitted 코드가 prod 에 갈 경로 자체가 없음. ### C. Deploy lock 서비스당 배포 직렬화. matrix-postgres `pg_advisory_lock` 우선 (이미 matrix DB 가 SSOT), 파일락 fallback (matrix down 시). **제거하는 고장**: 두 세션이 같은 서비스 blue/green 을 동시에 flip → alias 경합 / 어느 color 가 active 인지 불명 = 불가능. ### D. Provenance + drift 컨테이너에 `org.axe.git_sha` 라벨 부여. `axe health` / `axe host` 가 color 별 실행 SHA + origin/main 대비 drift 를 표시. **제거하는 고장**: "지금 라이브가 git 과 일치하나" 가 silent 하게 불명이던 상태. 항상 답이 나옴 (drift 가시화). ### E. 잘못된 경로 제거 `axe deploy` 가 더 이상 tree 입력을 받지 않는다 (B 가 SHA 입력으로 대체). `git push origin main` 직접 = **pre-push 훅 + GitHub branch protection** 으로 기계적 차단. `axe ship` 만 main 을 전진시킨다. **제거하는 고장**: 현재는 "main 직접 push 금지" 가 honor-system (사회적 규약뿐, [D-ops-16](/ops/decisions)). 훅 + branch protection 으로 기계적 강제 전환. ### F. 통합 ship 흐름 — `axe ship` worktree 에서 한 명령으로: ``` fetch → origin/main 에 rebase → SHA 에서 build + test → push (main fast-forward) → 그 SHA 를 deploy (C 의 lock 하에) → main mirror fast-forward → shiplog 기록 ``` **제거하는 고장**: 오늘 운영자가 손으로 한 reconcile (fetch / stash / rebase / push / deploy / 정리) 전 단계가 한 명령으로 압축. `b4067504` 같은 incident 가 명령 한 줄로 끝남. ## 안전한 rollout — additive → canary → cutover 전부 비파괴. 진행 중 WIP 보존. 1. **additive** — A–F 각 컴포넌트는 기존 동작과 **공존**하게 추가. 기존 `axe deploy` / `axe ship` 경로를 깨지 않음. 2. **canary 검증** — 각 컴포넌트를 `--dry-run` + passive-color 카나리 (flip 안 함) 로 검증. active 트래픽 영향 0. 3. **migration 게이트** — 스키마 변경 포함 릴리스는 ephemeral DB clone 검증 통과 후에만 flip. 4. **cutover** — 기본값 flip 전환 + 가드 활성화 (pre-push 훅 / branch protection). escape hatch 포함 (응급 시 우회 경로). 게이트됨 (운영자 확인). 5. **되돌림** — 문제 시 previous color alias 되돌림 (5초) + 가드 비활성화로 이전 운영 모델 복귀. ## 왜 별도 staging 환경을 안 쓰나 별도 staging *환경* (staging.axelabs.ai + 독립 DB + 독립 터널) 은 **채택 안 함**. - AXE 는 단일 Mac mini 호스트. staging 환경을 띄워도 prod 와 같은 호스트 → 새 실패유형을 **안 잡음** (호스트·네트워크·DB 엔진 동일). - 비용 (컨테이너 2배 + DB 2배) 과 parity 유지 부담 (staging 이 prod 와 drift 하면 검증 가치 0) 만 증가. - 실제로 막아야 할 위험 = (a) 깨진 코드가 트래픽 받기, (b) 깨지는 스키마 변경. (a) 는 **passive-color 카나리** (트래픽 0 에서 health 검증) 가, (b) 는 **migration-validation 게이트** 가 이미 커버. canary + migration-validation 이 staging 의 실효를 단일 호스트 비용 0 추가로 달성한다. ## 왜 migration-validation 이 본질인가 blue/green 의 **유일한 사각**이 스키마 마이그레이션이다. - blue 와 green 은 **DB 를 공유**한다 (코드만 2벌, 데이터는 1벌). - 따라서 green 용 마이그레이션을 적용하면 **blue 도 즉시 그 스키마를 본다**. 깨지는 변경 (컬럼 drop / 타입 변경 / NOT NULL 추가) 은 green flip 전에 이미 blue 를 오염시킨다. - 즉 스키마 변경은 blue/green 으로 **카나리가 원천적으로 불가능** — passive color 검증이 active 를 보호하지 못함. migration-validation 게이트가 이 사각을 덮는다: flip 전에 **prod DB 의 ephemeral clone** (`axe drill` / backup 스냅샷 기계 재사용) 에 마이그레이션을 적용·검증. clone 에서 깨지면 flip 거부 → 공유 DB 는 손대지 않음. 대상 예 = blueprint 대기 migration 2개 (`add_user_entra_oid`, `add_entity_legal_name`), hive alembic. ## 관련 - [D-ops-42](/ops/decisions) — 본 아키텍처 결정 근거 (컴포넌트 A–F 상세 + 실측 incident) - [Release flow (`axe ship`)](/ops/runbook/release-flow) — F 의 ship 흐름이 확장하는 기존 release-gate ([D-ops-16](/ops/decisions)) - [Blue/green deploy](/ops/runbook/deploy) — 스테이지 파이프라인의 alias swap + 5초 rollback 메커니즘 - [D-matrix-3](/ops/decisions) — matrix-postgres SSOT (C 의 advisory lock + backlog/roadmap/updates 작업 스테이지) --- # 직원 퇴사 > 퇴사한 직원의 모든 access 차단 절차. URL: https://docs.axelabs.ai/ops/runbook/employee-offboarding # 직원 퇴사 (운영자 측) ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/employee-offboarding 따라 퇴사 직원 access 차단해줘. 진행: 1. 퇴사 정보 확인 (email + 퇴사일 + 즉시 차단 여부) 2. 페이지 step 1 — customer IT 측 Microsoft 365 사용자 비활성 (Entra ID OAuth 차단, 토큰 60-90분 내 자동 만료) 3. 페이지 step 2 — `customers.yaml` 의 `user_entity_map` 에서 해당 email 제거 또는 주석, 사용자 확인 후 적용 4. 페이지 step 3 — customer macmini 측 frame 재기동, 매 step 결과 받고 다음. 함정 — 토큰 캐시 60-90분 미만료 / customers.yaml comment 처리 vs 삭제 5. 페이지 step 4 (선택) — 직원이 client_secret 알고 있던 정황 시 [secret-rotation](/ops/runbook/secret-rotation) playbook 으로 분기 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. customer admin 으로부터 퇴사 알림: ``` 퇴사: kim.minsoo@axellc.com 퇴사일: 2026-06-30 요청: access 즉시 차단 ``` ## 즉시 (퇴사일 ≤24h 전) ### 1. customer 측 Microsoft 365 사용자 비활성 customer IT 가 처리. 이거 하나만 해도 Microsoft Entra ID OAuth 가 차단 — 즉, frame 도 자동 차단. 직원 본인의 access_token 이 캐시되어 있더라도 60-90분 내 만료. ### 2. customers.yaml 에서 entity 매핑 제거 ```yaml axe: user_entity_map: "ai@axellc.com": ["axec", "axev"] "cfo@axellc.com": ["axec", "axev"] # "kim.minsoo@axellc.com": ["axec"] # ← 제거 (또는 주석 처리) ``` ### 3. frame 재기동 ```bash ssh axe-macmini " cd /Users/axe/frame && set -a && source .env.local && set +a && docker compose restart frame-mcp-blue frame-mcp-green " ``` 이후 그 직원의 토큰이 frame 에 도달해도 `EntityNotAuthorizedError` 로 거부. ### 4. claude.ai 측 secret 회전 (선택) 만약 그 직원이 회사 client_secret 값을 알고 있던 상황 (Vaultwarden 등) → 의심 시 회전: → [Secret rotation](/ops/runbook/secret-rotation) 절차 (응급). 악의 없는 정상 퇴사면 회전 안 해도 무방 (직원의 Microsoft 계정이 비활성화되어 secret 으로도 OAuth 시작 못 함). ## 24h 후 ### 5. Audit (직원 마지막 활동 기록) ```bash ssh axe-macmini "docker exec frame-postgres psql -U frame -d frame -c \" SELECT actor, op, table_name, ts FROM axec.audit_log WHERE actor = 'kim.minsoo@axellc.com' ORDER BY ts DESC LIMIT 30; \"" ``` - 마지막 활동 timestamp 확인 (정상 종료 vs 의심 패턴) - 이상 활동 있으면 customer admin 에게 통지 ### 6. 보관 audit_log 는 자동으로 영구 보관 (append-only). 별도 export 불필요. 향후 감사 필요 시 actor 로 조회. ## 1 주일 후 ### 7. (선택) Vaultwarden organization 에서 제거 만약 Vaultwarden 멤버였으면 organization 에서 제거: ```bash # Vaultwarden admin UI # Organizations → axe → Members → kim.minsoo@axellc.com → Remove ``` 또는 `bw` CLI 로: ```bash bw config server https://axe.axelabs.ai/vault bw login # (subcommand 로 organization member remove) ``` > **주의 — vault 의 trust boundary**: vault item delete 는 **그 secret 의 저장 위치에서만** 차단. 외부 service (Microsoft / GitHub / KB / 홈택스 등) 의 계정·2FA·OAuth client_secret 은 별 절차 필요. 상세: [/architecture/vault-policies#vault-scope--무엇을-보호하는가-trust-boundary](/architecture/vault-policies). 다음 step 8 표를 vault remove 와 **반드시 병행**. ### 8. 외부 service 권한 회수 (vault scope 외) vault item 을 지웠다고 그 secret 이 가리키는 외부 service 측 access 가 자동 차단되지 않음. service 마다 별도 admin 절차 필요. 회수 누락 시 그 service 의 access 잔존 + 데이터 export risk. | 외부 service | 회수 작업 | 책임자 | |---|---|---| | Microsoft 365 / Entra ID | 계정 비활성 (= step 1 그대로) | customer IT admin | | GitHub `axelabs-ai` org | org member 제거 + 본인 PAT 의 server 측 revoke 요청 | 운영자 | | KB / 홈택스 / 회계법인 portal | 해당 portal admin 의 계정 비활성/삭제 + TOTP secret regenerate (잔존 device 차단) | customer admin | | Cloudflare Access | user 를 Access policy 에서 remove | 운영자 | | Anthropic / OpenAI / 기타 API key | 해당 console 에서 API key revoke (vault item 삭제와 별개) | 운영자 | | OAuth client_secret (직원이 알던 정황) | Azure / Google 측 client_secret 재발급 → 옛값 폐기 | 운영자 (→ [secret-rotation](/ops/runbook/secret-rotation)) | → 각 row 를 vault item 삭제와 **쌍으로 처리**. 둘 다 끝나야 회수 완료. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | Microsoft 비활성만 하고 customers.yaml 안 건드림 | 그 직원의 토큰이 만료 후 OAuth 재시도하면 frame 가 entity 매핑 발견 후 통과 가능성 | 둘 다 처리 | | frame 재기동 잊음 | YAML 변경 안 반영 | 매번 restart | | 마지막 activity audit 안 함 | 의심 활동 놓침 | 24h 후 확인 | | vault 만 처리하고 외부 service 안 건드림 | 그 service 측 access 잔존 (KB/홈택스/GitHub/Anthropic 등) + 데이터 export risk | step 8 표 row 별 외부 service 회수 동반 — vault item delete 는 "저장소 차단" 일 뿐. trust boundary = [/architecture/vault-policies#vault-scope--무엇을-보호하는가-trust-boundary](/architecture/vault-policies) | ## 응급 — 의심 활동 발견 시 만약 audit_log 에서 의심 활동 (대량 export, write 폭주 등) 발견: 1. customer admin 에게 즉시 통지 2. 그 직원의 모든 활동 추출 (audit_log 전체) 3. 영향 받은 entity 의 daily integrity check 결과 재확인 4. 필요 시 백업으로 복원 검토 5. customer 요청 시 외부 감사인에게 audit_log 인도 audit_log 는 모든 write 의 old_data + new_data 를 JSONB 로 기록 → 정확한 변경 내용 복원 가능. --- # 신규 직원 등록 > customer admin 으로부터 신규 직원 추가 요청 받았을 때 운영자 절차. URL: https://docs.axelabs.ai/ops/runbook/employee-onboarding # 신규 직원 등록 (운영자 측) ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/employee-onboarding 따라 신규 직원 등록해줘. 진행: 1. customer admin 요청 정보 확인 (email + 업무 + 필요 entity 권한) 2. 페이지 step 1 — `customers.yaml` 의 `user_entity_map` 에 한 줄 추가 (`"": [...]`), 매 변경 사용자 확인 3. 페이지 step 2 — customer macmini 측 frame 재기동 (`docker compose restart frame-mcp-blue frame-mcp-green`, ~5초) 4. 함정 — entity 명 typo / scope 별도 분리 미구현 (default `read/write/close` 부여) / customer macmini SSH 진입 실패 5. 페이지 step 3 검증 (`axe health frame`) + 직원에게 onboarding 안내 송부 ([/onboard/ssh-access](/onboard/ssh-access) + [/onboard/vault-setup](/onboard/vault-setup) + [/onboard/claude-connectors](/onboard/claude-connectors) playbook 3종) ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. customer admin 으로부터: ``` 신규 직원: kim.minsoo@axellc.com 업무: 회계 (axec) 필요 권한: axec read+write ``` 같은 요청 받았을 때. ## 1. customers.yaml 수정 ```yaml axe: user_entity_map: "ai@axellc.com": ["axec", "axev"] "cfo@axellc.com": ["axec", "axev"] "kim.minsoo@axellc.com": ["axec"] # ← 추가 ``` → entity 만 매핑. scope 는 기본값 (`read`, `write`, `close`) 모두 부여 — 권한 별도 분리는 별도 작업 (D-ops-?, 향후). ## 2. frame 재기동 (customers.yaml reload) ```bash ssh axe-macmini " cd /Users/axe/frame && set -a && source .env.local && set +a && docker compose restart frame-mcp-blue frame-mcp-green " ``` 소요 ~5초. ## 3. 검증 (옵션) 직원에게 onboarding 안내 보내기 전, 본인이 그 직원 가짜로 시도: ```bash # Microsoft id_token (테스트용) 발급은 어려우므로, frame integrity 만 확인 axe health frame # positional target only; multi-customer 환경에선 ssh customer-macmini 측 health 확인 ``` `customers.yaml` 의 user_entity_map 변경 → frame 재기동 → 즉시 적용. 별도 검증 불필요. ## 4. 직원 안내 메일 ``` 제목: [AXE Labs] {회사명} Frame access 설정 안내 안녕하세요 김민수님, 회사 계정으로 회계 시스템 (Frame) 접근 권한이 부여되었습니다. 설정 방법 (5분): 1. claude.ai 에 회사 이메일 (kim.minsoo@axellc.com) 로 로그인 2. https://docs.axelabs.ai/onboard/claude-frame-setup 따라 진행 3. Client ID + Secret 는 별도 안전 채널 (Bitwarden Send) 로 전송 예정 권한: - entity axec (에이엑스이 코퍼레이션) - 권한: 조회 + 분개 등록 (read + write) 문제 발생 시: - 1차: 회사 IT 담당자 - 2차: ai@axellc.com (운영자) 감사합니다. ``` ## 5. Client Secret 전달 — `axe secret send` + Teams DM 자동 발사 Client ID 는 평문으로 같이 보내도 무방 (GUID 일 뿐). Client Secret 은 **반드시 Bitwarden Send 1회용 링크** — `axe secret send` 가 매니페스트 lookup + 1회용 링크 생성을 한 번에. 옵션 의미는 [/architecture/secrets § 사람에게 전달](/architecture/secrets#사람에게-전달--bitwarden-send). ### 5.1 URL 발급 (3개 secret) ```bash EMP=kim.minsoo # local-part (라벨용) EMAIL=kim.minsoo@axellc.com > /tmp/onboard-$EMP.txt for env in AZURE_BLUEPRINT_MCP_CLIENT_SECRET \ AZURE_HIVE_MCP_CLIENT_SECRET \ AZURE_FRAME_MCP_CLIENT_SECRET; do svc=$(echo $env | awk -F_ '{print tolower($2)}') url=$(axe secret send $env --service $svc --to $EMP 2>/dev/null) echo "$svc|$url" >> /tmp/onboard-$EMP.txt done ``` ### 5.2 Teams DM 자동 발사 ([broadcast-dm](/architecture/secrets#자동-발사--apiadminbroadcast-dm)) ```bash CRON=$(grep -E "^CRON_SECRET=" /Users/axe/blueprint/.env | cut -d= -f2-) BP=$(grep "^blueprint|" /tmp/onboard-$EMP.txt | cut -d'|' -f2) HV=$(grep "^hive|" /tmp/onboard-$EMP.txt | cut -d'|' -f2) FR=$(grep "^frame|" /tmp/onboard-$EMP.txt | cut -d'|' -f2) TEXT="$EMP 님, 회사 시스템 (Blueprint / Hive / Frame) Custom Connector 등록 부탁드립니다. 가이드: https://docs.axelabs.ai/onboard/claude-frame-setup claude.ai → Settings → Connectors → + Add Custom Connector × 3 Blueprint URL: https://axe.axelabs.ai/blueprint/mcp Client ID: 482598f7-540c-462c-9dfd-b957651eb804 Secret: $BP Hive URL: https://axe.axelabs.ai/hive/mcp Client ID: b7ead15d-2fea-4864-a5a8-b4b07d1629d4 Secret: $HV Frame URL: https://axe.axelabs.ai/frame/mcp Client ID: 137fc0ef-eb9f-4903-acbc-1a748add349c Secret: $FR Microsoft 회사 계정 ($EMAIL) 로그인 → Connected. 막히면 ai@axellc.com." curl -s -X POST http://localhost:3100/api/admin/broadcast-dm \ -H "Authorization: Bearer $CRON" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg e "$EMAIL" --arg t "$TEXT" \ '{emails:[$e], text:$t, contentType:"text"}')" # → {"summary":{"sent":1,...}} ``` 신규 직원이 Teams 에서 ai@axellc.com 으로부터 1:1 DM 수신 → 3 Send URL 클릭 → claude.ai Custom Connector Secret 칸 paste → `-a 1` 자동 무효. 운영자 손 = unlock 1번 + 위 2-block 실행만. ## 6. Bitwarden Vault 멤버 추가 (선택) 향후 frame-JWT 직접 발급 경로로 회귀할 경우 대비: 1. Vaultwarden organization → frame-jwt-axec collection → 멤버 추가: `kim.minsoo@axellc.com` 2. 권한: read-only (item 조회만) 현재 직접-Microsoft OAuth 경로에선 불필요. 향후 변경 가능성 대비. ## 7. 직원이 첫 호출 후 검증 직원 등록 후 며칠 내 본인이 frame audit_log 확인: ```bash ssh axe-macmini "docker exec frame-postgres psql -U frame -d frame -c \" SELECT actor, op, table_name, ts FROM axec.audit_log WHERE actor = 'kim.minsoo@axellc.com' ORDER BY ts DESC LIMIT 5; \"" ``` 활동 1건 이상 = 정상 작동 확인. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | `user_entity_map` 에 추가만 하고 컨테이너 재기동 X | 권한 적용 안 됨 | 항상 `docker compose restart` | | email 대소문자 불일치 | lookup 실패 | customers.yaml 는 lower-case 통일 (frame 코드가 자동 normalize) | | entity 이름 오타 (`axec` vs `axes`) | EntityNotAuthorizedError | YAML 저장 전 확인 | | 권한 부여 후 직원에게 알림 안 함 | 직원이 onboarding 절차 모름 | 메일 발송 절차 자동화 검토 | ## 향후 개선 - [ ] `axe employee add --customer axe --email <...> --entity axec` CLI 명령 추가 - [ ] customer admin 이 self-service 로 직원 추가하는 Blueprint UI - [ ] 권한 부여 audit (누가 누구에게 언제 부여) --- # macmini 손실 > customer macmini 도난/화재/완전 손실 시 1-day 복구. URL: https://docs.axelabs.ai/ops/runbook/macmini-loss # macmini 손실 (재해 복구) ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/macmini-loss 따라 customer macmini 손실 1-day 복구 진행해줘. 진행: 1. 사건 확인 — customer admin 통보 (도난/화재/disk 파손) + 손실 시점 파악 2. 즉시 access 차단 — customer IT 측 Microsoft Entra ID app 비활성 또는 secret 회전 (모든 직원 connector 401) 3. backup 상태 확인 (Ring peer / Cold SSD 가장 신선 snapshot) → 손실 시점 ~ 마지막 backup 간 데이터 손실 범위 추정 + 사용자 확인 4. 새 macmini 확보 + 단기 복구 (T+1h~1d) 페이지 절차, 매 step 결과 받고 다음. customer IT 와 양방향 통보 + 진행 상황 공유 5. 복구 완료 후 health 검증 + 정합성 검사 + customer 회신 + 사고 회고 /ops/known-gaps 기록 (재발 방지) ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. 가장 심각한 시나리오. customer macmini 가 도난/화재/disk 완전 파손으로 사용 불가. ## 즉시 (T+0 ~ T+1h) ### 1. 사건 확인 customer admin 으로부터 통보: - "사무실 화재로 macmini 손상" - "이전 후 부팅 안 됨" - "도난 신고" ### 2. 사용자 access 차단 악의적 사용 방지 — Microsoft Entra ID 측에서 app 비활성 (또는 secret 회전): ```bash # customer IT 에게 즉시 요청: "Azure portal → Microsoft Entra ID → App registrations → 'Frame MCP' → Authentication → 'Allow public client flows' 끄거나, Certificates & secrets 에서 secret 삭제" ``` → 모든 직원의 connector 가 401 받기 시작. ### 3. 현재 backup 상태 확인 ```bash # Ring peer 측에서 가장 신선한 backup 확인 ssh realchoice-macmini " restic -r /Users/realchoice/peer-backups/axe/restic-repo snapshots \ --password-file <(security find-generic-password -w -s axe.backup.restic.local) " # 가장 최근 snapshot 시점 확인 → 손실 시간 추정 # Cold SSD 도 확인 # (운영자가 SSD 꽂아서 확인) ``` 손실 시점 = (사건 직전 시점) ~ (마지막 backup 시점) 사이. ## 단기 복구 (T+1h ~ T+1d) ### 4. 새 macmini 확보 - customer 가 새 macmini 구매 (M2/M3, 16GB+, 512GB+) - 도착 즉시 운영자 측 배송 또는 customer IT 가 셋업 ### 5. macOS 기본 셋업 customer IT 또는 운영자가 원격 지원: ```bash # 1. macOS 사용자 생성 (예: axe) # 2. Tailscale install + 운영자 key 등록 # 3. SSH 접속 검증 # 4. Docker Desktop install # 5. axe CLI install curl -sSL https://docs.axelabs.ai/install/axe-cli.sh | bash ``` ### 6. backup 복원 ```bash # 운영자 머신에서 axe restore --customer <customer> --tier ring --from <peer> --as-of <latest> # 또는 cold SSD axe restore --customer <customer> --tier cold --as-of <latest> ``` 복원 대상: - `frame-postgres` 데이터 (Docker volume) - `.local/files/` (platform data, source files) - `~/.frame/` (PII salt) - 컨테이너 image 들 (재 build) ### 7. customers.yaml + cloudflared 그대로 운영자 머신의 `customers.yaml` 변경 불필요 — customer 의 도메인 (`{customer}.axelabs.ai`) 그대로 사용. cloudflared ingress 도 그대로 — origin 이 host.docker.internal:port 이고 customer macmini IP 변경 무관 (Tailscale FQDN 사용 안 함). ### 8. onboard 재실행 ```bash axe onboard <customer> --apply # Azure 측 설정은 손대지 않음 (onboard 가 Azure CRUD 안 함, 그대로 재실행 idempotent). # 부분 재실행 필요하면 `--skip-frame` (frame 만 건너뛰기) 사용 가능. ``` Azure Entra ID app 들은 그대로 (도메인 동일, redirect_uri 동일). frame / blueprint / vault 만 macmini 측 fresh setup. ### 9. 정합성 검사 ```bash ssh <customer>-macmini "docker exec frame-mcp-blue python -m frame.cli integrity-check --entity <each_entity>" ``` 모두 통과 → 복구 완료. ### 10. customer IT 측 Microsoft Entra ID 재활성 `Allow public client flows` 다시 Yes 또는 새 secret 발급 (3 절에서 차단했던 것 되돌리기). 새 secret 발급 시: 1. customer IT → 새 secret VALUE 2. 운영자 → macmini Keychain 에 push 3. 컨테이너 재기동 4. 직원들에게 새 secret 안내 (claude.ai connector 측 swap 필요) ## 손실 분석 복원 후 손실 분석: ```bash # 마지막 backup ~ 사건 사이의 audit_log ssh <customer>-macmini "docker exec frame-postgres psql -U frame -d frame -c \" SELECT actor, op, table_name, ts FROM <entity>.audit_log WHERE ts > '<last_backup_ts>' ORDER BY ts; \"" ``` 이 audit_log 자체도 backup 시점 이후 손실. 운영자가: 1. customer 에게 손실 시점 통지 + 그 사이 작업한 내역 재입력 요청 2. 매뉴얼 분개 재입력 (회계사가 자기 메모 보고) 3. 외부 증거 (은행 statement 등) 재수집 ## 보안 검토 (사건 직후) 도난/화재가 아니라 침해 의심이면: - Microsoft Entra ID audit log 검토 (의심 IP, 의심 시간대) - frame audit_log 검토 (의심 actor) - ring peer 의 backup 도 봐서 사건 직전 정상 활동 확인 - 의심 시 사건 정식 보고 + 외부 감사 ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | backup 만 신뢰, Microsoft 측 access 안 차단 | 도난자가 계속 access | 즉시 customer IT 호출 | | backup 신선도 명확 확인 안 함 | 손실 시간 misestimate | 매번 `restic snapshots` 의 `Time` 컬럼 명시 | | 복원 후 정합성 검사 안 함 | 잠재 손상 미발견 | `integrity-check` 필수 | | 자세한 손실 시점 추정 안 함 | customer 가 누락된 작업 모름 | 마지막 backup ts 명확히 통지 | ## 분기 Drill 의 의미 분기 자동 restore drill 은 정확히 이 시나리오 대비. drill 통과 = 복구 절차가 실제로 작동함을 검증. drill 실패 = 즉시 backup 절차 점검. 실패 누적 = 인프라 전반 재검토. --- # 운영자 broadcast — Teams DM 으로 임직원 1:N 공지 > Blueprint `/api/admin/broadcast-dm` REST 로 bot identity (ai@axellc.com) → AXE 임직원 1:N Teams DM. M365 Mail.Send 가 아닌 Teams 채널 — AXE 내부 broadcast 표준. vault 안내 / 운영 공지 / 시스템 변경 통보 등 use case. URL: https://docs.axelabs.ai/ops/runbook/operator-broadcast # 운영자 broadcast (Teams DM) ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/operator-broadcast 따라 임직원 [수신자 email list] 에게 다음 공지 broadcast 해줘: [공지 본문 paste] 진행: 1. 본 페이지의 Prereq 확인 (Blueprint LIVE + CRON_SECRET 접근 가능) 2. Step 1..4 순서대로 — 매 step 결과 받고 다음 3. 함정 발생 시 페이지 "함정 정리" 표 따라 우회 4. Step 4 결과 (3 messageId) 받으면 종료 + Ship Log 한 줄 (선택) ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. ## Prereq - 운영자 자격 (AXE org Owner — 본 broadcast 가 정당 operational 공지인지 본인 판단) - axe-macmini 에서 실행 (Blueprint LIVE 가 `http://blueprint.local:3100` 으로 접근 가능) - `/Users/axe/blueprint/.env` 의 `CRON_SECRET` 환경변수 존재 (Blueprint deploy 시 자동 set) - 수신자 email list (`@axellc.com` 도메인 — 운영자 자체 broadcast 가 cross-tenant 안 함) - 본문 = plain text 또는 escaped html (default text — 권장) ## Step 1: 사전 확인 (Blueprint LIVE + CRON_SECRET + bot identity) ```bash # (a) Blueprint health curl -sf -o /dev/null -w 'blueprint http=%{http_code}\n' http://blueprint.local:3100/api/health # (b) CRON_SECRET 길이 (값 출력 X) SECRET=$(grep '^CRON_SECRET=' /Users/axe/blueprint/.env | cut -d= -f2- | tr -d '"' | tr -d "'") echo "secret_len=${#SECRET}" # (c) bot identity = ai@ 인지 self-DM 거부 응답으로 검증 (실제 메시지 발송 X) curl -sf -X POST http://blueprint.local:3100/api/admin/broadcast-dm \ -H "Authorization: Bearer $SECRET" \ -H 'Content-Type: application/json' \ -d '{"emails":["ai@axellc.com"],"text":"self-test","contentType":"text"}' \ | jq '.results[0]' # 기대: {email:"ai@axellc.com", status:"skipped", reason:"target AAD id matches bot — refusing self-DM"} # → reason 안에 "matches bot" 보이면 bot = ai@ 정상 ``` 3개 다 통과해야 진행. 실패 시 함정 표 참조. ## Step 2: 본문 + payload JSON 작성 본문 = plain text (default contentType). 줄바꿈 `\n`, 특수문자 escape 불필요 (text mode 라 client 가 그대로 렌더). bullet 은 `-` 또는 `•` 등 plain. ```bash # 본문을 한 변수에 — heredoc 이 가독성 ↑ BODY=$(cat <<'EOF' [제목 1줄] [본문 내용] - bullet 1 - bullet 2 링크: https://... EOF ) # payload JSON 작성 (jq 가 \n + 특수문자 자동 escape) EMAILS_JSON='["soohun.kang@axellc.com","taehun.kang@axellc.com","jinwoo.han@axellc.com"]' jq -n --argjson emails "$EMAILS_JSON" --arg text "$BODY" \ '{emails:$emails, contentType:"text", text:$text}' > /tmp/bcast.json # 확인 (수신자 + 본문 길이만 — 본문 풀 출력 안 함) jq '{emails, text_len: (.text | length)}' /tmp/bcast.json ``` ## Step 3: POST 호출 ```bash SECRET=$(grep '^CRON_SECRET=' /Users/axe/blueprint/.env | cut -d= -f2- | tr -d '"' | tr -d "'") curl -s -X POST http://blueprint.local:3100/api/admin/broadcast-dm \ -H "Authorization: Bearer $SECRET" \ -H 'Content-Type: application/json' \ -d @/tmp/bcast.json \ -w '\nHTTP_%{http_code}\n' ``` 기대 응답: ```json { "summary": {"total":3, "sent":3, "skipped":0, "error":0}, "results": [ {"email":"soohun.kang@axellc.com", "status":"sent", "chatId":"19:...@unq.gbl.spaces", "messageId":"1779..."}, {"email":"taehun.kang@axellc.com", "status":"sent", "chatId":"19:...", "messageId":"1779..."}, {"email":"jinwoo.han@axellc.com", "status":"sent", "chatId":"19:...", "messageId":"1779..."} ] } ``` `sent=3` + HTTP 200 = 정상. cleanup: ```bash rm -f /tmp/bcast.json ``` ## Step 4: 결과 검증 + Ship Log (선택) 각 수신자가 Teams 앱 (phone + desktop) 에서 ai@axellc.com 로부터 1:1 DM push 알림 받았는지 본인이 확인 (수신자 회신 또는 운영자 본인 Teams 의 sent items 확인). 운영 회고용 Ship Log 한 줄 (broadcast 가 의미 있는 일이면 — vault 공지 / 신규 customer launch 등): ```markdown | YYYY-MM-DD HH:MM | broadcast (axe org) | (operator action) | **broadcast title** — 3 임직원 (soohun/taehun/jinwoo) Teams DM 발송. broadcast-dm REST + bot ai@. 본문 핵심: [한 줄 요약]. messageId: 1779..., 1779..., 1779... | ``` 추가하면 `/Users/axe/axelabs-docs/content/ops/updates.mdx` Ship Log 표 최상단에 한 줄. `axe ship docs` 로 배포. ## 함정 정리 | # | 증상 | 원인 | 우회 | |---|---|---|---| | 1 | 외부 7cb41f76 MCP 의 노출 tool list 에 `send_mail` / `send_email` / `graph_send_email` 없음 | 의도된 분리 — Blueprint 의 32+ graph_* tool 중 send 계열은 `blueprint-graph` 내부 MCP 에만 등록, 외부 connector 는 read-only 격리 | 본 페이지의 broadcast-dm REST 사용 (admin 채널 별도) | | 2 | `az rest --uri /me/sendMail` → `403 Forbidden ErrorAccessDenied` | az CLI 의 first-party app (04b07795) 이 Mail.Send scope 받을 권한 없음 (Microsoft 자사 앱 간 consent preauthorization 정책) | az CLI 경유 mail send 영구 불가 — broadcast-dm 또는 Outlook 수동 | | 3 | `az account get-access-token --scope https://graph.microsoft.com/Mail.Send` → AADSTS65002 | (#2 와 동일 root cause) | (#2 와 동일) | | 4 | `/api/admin/broadcast-dm` → `{status:"skipped", reason:"target AAD id matches bot — refusing self-DM"}` | bot identity = ai@axellc.com — 본인에게 DM 시도 거부 | 수신자 list 에서 ai@ 제거. 또는 self-test 용도 (Step 1 의 (c)) 일부러 의도적 | | 5 | `{status:"skipped", reason:"no AAD object id for user"}` | 수신자가 Blueprint 에 한 번도 로그인한 적 없음 — NextAuth callback 의 `User.aadObjectId` populate 안 됨 | 수신자가 https://blueprint.axelabs.ai 1회 SSO 로그인 → 자동 fill → 재시도 | | 6 | `401 Unauthorized` | `CRON_SECRET` 헤더 잘못 / .env 에 미설정 / Bearer prefix 빠짐 | `Authorization: Bearer $SECRET` 정확. .env 의 CRON_SECRET 값 직접 변수에 | | 7 | `Connection refused http://blueprint.local:3100` | Blueprint 미실행 (PC 부팅 직후 / launchd 미가동) | `ps aux \| grep blueprint` 확인, 필요 시 `launchctl kickstart -k gui/$(id -u)/com.axe.blueprint` | | 8 | 본문 줄바꿈 깨짐 (Teams 한 줄로 표시) | `contentType: "html"` 인데 plain text 넣음 — `
` 없으니 collapse | `contentType: "text"` 명시 (default 권장) | | 9 | 본문 안 `*` 강조 등이 안 보임 | Teams 는 markdown 미해석 (text contentType) | bullet 은 `-` plain, 강조는 대문자 or `===` 줄 구분 | | 10 | customer 직원 (`@truvia.co.kr` 등) 에게 보내려고 시도 → skip | broadcast-dm 의 bot = AXE tenant 의 ai@, cross-tenant chat 생성 불가 | customer 측은 본인 운영자 (Truvia 의 broadcast 채널) 자체 보냄 — sovereignty 원칙 | ## 관련 use case (참고) - **vault 운영 공지** — KDF rotation / setup 안내 (D-ops-40, 2026-05-26 첫 실 사용) - **신규 customer launch 안내** — 새 service 가 LIVE 됐을 때 - **시스템 변경 통보** — Blueprint major upgrade, frame schema migration 등 - **D-day 직전 사전 안내** — onboard step 차단 가능성 등 사전 경고 NOT for: 일반 잡담 (channel 사용), 1:1 코칭 (Teams 직접 chat), customer 측 공지 (customer 자체 채널). ## 매 작업 시 사전 작업 | 빈도 | 작업 | 자동화 | |---|---|---| | 매 broadcast | Step 1 사전 확인 (Blueprint LIVE + bot identity 검증) | ❌ (사람 판단) | | `CRON_SECRET` 회전 시 | Blueprint .env + axe-macmini launchd 재시작 | [B-blueprint-secret-rotation](/ops/backlog) 참조 | ## 참조 - [D-ops-40](/ops/decisions) — vault axe.3 release (본 broadcast 의 첫 use case, 2026-05-26) - [/architecture/vault-policies](/architecture/vault-policies) — vault 3 layer 정책 모델 (broadcast 본문 안 참조 patterns) - [/services/blueprint](/services/blueprint) — Blueprint 서비스 개요 - `src/app/api/admin/broadcast-dm/route.ts` — REST 구현 - `src/lib/teams/graph-client.ts` — bot Graph client (`getSharedClient()` 가 ai@ 정합) - [B-blueprint-broadcast-mail](/ops/backlog) — 실 SMTP email 발송 route 추가 (M5, 사용 빈도 보고 진행) - [/ops/known-gaps](/ops/known-gaps) — Microsoft 첫 당사자 app consent policy (az CLI Mail.Send 영구 차단) --- # Release flow (axe ship) > 코드 변경 → 운영 반영까지의 release-gate. `git push origin main` 대신 `axe ship` 한 명령으로 docs-check + push + deploy 를 묶음. URL: https://docs.axelabs.ai/ops/runbook/release-flow # Release flow — `axe ship` ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/release-flow 따라 본 repo 변경 release 해줘. 진행: 1. 현재 repo + branch + 변경 상태 진단 (git status + git log origin/main..HEAD + 동반 docs 갱신 commit 존재 여부) 2. 페이지 "Service 매핑" 표 확인 — 본 repo 의 deploy hook 자동/수동 분기 3. 페이지의 각 Step (docs-check → branch clean → push preview → confirm → push → deploy hook) 매 step 결과 받고 다음. 특히 Step 4 confirm 직전 사용자 확인 4. 함정 발생 시 페이지 "함정" 표 따라 우회 (docs commit 누락 / cross-repo drift / feature branch) 5. 배포 완료 후 production 검증 + (선택) updates.mdx Ship Log 한 줄 추가 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. > **Problem**: 코드 변경 후 docs (`axelabs-docs/content/*.mdx`) 갱신을 깜빡하면 production 사이트가 거짓말을 함. 매 commit 마다 hook 으로 강제하면 WIP commit 이 막혀서 `--no-verify` 우회 습관이 생김 → hook 사실상 무력화. > > **Solution**: release 시점에만 강제. `git push origin main` 의 자리에 `axe ship` 를 둬서, **production 으로 나가는 모든 push 가 docs-drift 검증을 통과해야** 함. WIP / feature branch push 는 raw git 으로 그대로 사용. ## 한 줄 요약 ```bash # 이전 습관 git push origin main && ssh axe-macmini "cd /Users/axe/ && docker compose ..." # 새 습관 axe ship # 현재 repo 자동 감지 → docs-check → push → deploy axe ship frame # 명시 axe ship docs # docs SSOT 자체 배포 ``` ## 무엇을 하는가 | Step | 동작 | 실패 시 | |---|---|---| | 1 | `axe docs-check --since origin/main` (현재 repo) | abort — 누락 mdx 페이지 출력 후 종료 | | 2 | branch == `main` && working tree clean | abort | | 3 | `git log origin/main..HEAD` 출력 (push 될 commit 미리보기) | nothing-to-push → abort | | 3b | `axelabs-docs` 에 unpushed commit 있나? 있으면 경고 | (계속 진행, 단순 hint) | | 4 | `yes/no` confirm 프롬프트 | abort | | 5 | `git push origin main` | git push 자체 실패면 abort | | 6 | service 별 deploy hook (아래 표) | (배포 실패는 알림, push 는 이미 완료) | ## Service 매핑 | `axe ship SERVICE` | repo | deploy hook | |---|---|---| | `frame` | `/Users/axe/frame` | `cmd_deploy` (blue/green swap, plan §3 D-config-13) | | `blueprint` | `/Users/axe/blueprint` | `cmd_blueprint_upgrade` (sequential blue/green) | | `docs` | `/Users/axe/axelabs-docs` | `docker compose up -d --build` (단일 컨테이너, ~30-60s 다운) | | `hive` | `/Users/axe/hive` | (자동화 X) manual hint 출력 | | `stream` | `/Users/axe/stream` | (자동화 X) manual hint 출력 | | `magnet` | `/Users/axe/magnet` | (자동화 X) manual hint 출력 | `hive/stream/magnet` 의 blue/green CLI 가 `axe deploy` 에 들어오면 매핑 갱신. ## 옵션 ``` axe ship [service] --dry-run # 실행 안 함, 어떤 step 이 무엇을 할지만 출력 --no-deploy # docs-check + push 까지만, 배포 hook skip (배포는 나중에 수동) ``` **escape hatch 없음**. `--skip-docs-check` 같은 플래그는 의도적으로 만들지 않음. drift 가 있다면: 1. `axelabs-docs/content/PAGE.mdx` 수정 2. `cd /Users/axe/axelabs-docs && git add -A && git commit -m 'docs: ...'` 3. `axe ship docs` (production 반영) 4. 원 repo 로 돌아와 `axe ship SERVICE` 재실행 ## docs SSOT 의 특수성 `axe ship docs` 는 docs-check 를 **skip** 함 (자기가 SSOT 인데 자기 자신과 drift 비교는 무의미). 대신: - branch + working tree clean 검사는 동일 - push 후 `docker compose up -d --build` 로 즉시 production 반영 - 배포 시간 ~30-60s, 단일 컨테이너 (blue/green X) ## 운영 순서 코드 변경이 docs 영향이 있는 경우 (대부분): ```bash # 1. 코드 작업 (frame 예시) cd /Users/axe/frame # ... 작업 ... git add -A && git commit -m "feat(frame): add foo MCP tool" # 2. docs 갱신 cd /Users/axe/axelabs-docs vim content/services/frame.mdx # MCP Tools 표에 foo 추가 git add -A && git commit -m "docs(frame): document foo MCP tool" # 3. docs 먼저 배포 (frame ship 의 docs-check 가 통과되도록 origin/main 에 docs commit 도달) cd /Users/axe/axelabs-docs axe ship # docs 자동 감지, push + rebuild # 4. frame 배포 cd /Users/axe/frame axe ship # frame 자동 감지, docs-check 통과, push + blue/green swap ``` ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | `axe ship` 대신 `git push origin main` 직접 | docs-check 미실행 → drift 누적 | release-worthy push 는 반드시 `axe ship` | | feature branch 에서 `axe ship` | "current branch is X, expected main" abort | merge to main 먼저 | | docs commit 안 한 채 `axe ship frame` | docs-check fail, push 거부 | docs 먼저 commit + ship | | `axe ship docs` 전에 `axe ship frame` | docs commit 은 frame repo origin/main 에서 보임 → check 통과, but production 사이트는 옛 mdx | docs 먼저 ship (cross-repo 경고가 출력됨) | | 다른 사람의 mdx 변경이 origin/main 에 있음 | docs-check 통과 (이미 mdx 있음) | 정상 — 다른 push 가 이미 docs 채움 | ## 왜 pre-commit hook 이 아닌가 자세한 의사결정 근거: [`D-ops-N` (release-gate-not-precommit)](/ops/decisions). 요약: - pre-commit hook 은 WIP commit 마다 발화 → 노이즈 → `--no-verify` 습관화 → hook 사실상 죽음 - release-gate (`axe ship`) 는 운영자가 의도해서 호출하는 단 한 곳에 검증을 집중 → 우회 유혹 적음 - 솔로 운영자 + Claude Code 주 개발자 환경에서 ROI 가 가장 높은 지점 ## 관련 - [`axe docs-check`](/ops) — drift 감지기 (ship 의 step 1 내부에서 호출) - [DECISIONS](/ops/decisions) — release-gate 결정 근거 - [Blue/green deploy](/ops/runbook/deploy) — frame/blueprint 배포의 내부 동작 --- # Secret Rotation > 모든 비밀 회전의 단일 명령 — axe secret rotate. Vault SoT + external provider (Azure/Meta/Naver/...) 동기화 + 무중단 재배포 자동화. URL: https://docs.axelabs.ai/ops/runbook/secret-rotation # Secret Rotation ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/secret-rotation 따라 [secret name] 회전 진행해줘. 진행: 1. 회전 대상 secret + provider 식별 (Azure / Meta / Naver / GitHub / Anthropic / 기타) 2. Azure 면 az cli 6 단계 분기, 그 외면 `axe secret rotate ` interactive 분기 3. 페이지의 각 step 명령 실행 + 검증, 매 step 결과 받고 다음. 특히 OLD secret revoke 직전 사용자 확인 (NEW secret 무중단 swap + production 검증 통과 후) 4. 함정 발생 시 페이지 본문 따라 우회 (App Owner 부재 / external portal 수동 / `unset NEW` shell history 정리 / merge-mode pull 누락) 5. 회전 완료 + production health 검증 + (선택) /ops/updates Ship Log 한 줄 ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. > **D-ops-17 + D-ops-19 (2026-05-21)** 이후 표준: Azure 의 client_secret 은 **az cli 6 단계** 로 portal UI 0회. 다른 provider (Meta/Naver/GitHub/Anthropic) 는 `axe secret rotate` 의 interactive 경로 + 외부 portal 수동. ## Azure 회전 — az cli 6 단계 (권장) ```bash APP_ID=137fc0ef-eb9f-4903-acbc-1a748add349c # frame_mcp (예시) OLD_KEY_ID=$(az ad app credential list --id $APP_ID \ --query "[?starts_with(hint,'')].keyId | [0]" -o tsv) # 1. Azure 새 secret 추가 — --append 가 OLD 유지 = overlap window NEW=$(az ad app credential reset --id $APP_ID \ --display-name "auto-rotated 2026-MM-DD" --years 2 --append \ --query password -o tsv) # 2. vault PUT axe secret push AZURE_FRAME_MCP_CLIENT_SECRET --service frame --value "$NEW" # 3. pull (merge-mode — config 보존) axe secret pull frame # 4. 무중단 swap axe deploy frame axe --apply # frame / hive axe blueprint upgrade axe --apply # blueprint # 5. 검증 docker exec $(active container) env | grep AZURE_FRAME_MCP_CLIENT_SECRET /usr/bin/curl -sI https://axe.axelabs.ai/frame/health # 200 # 6. OLD revoke (검증 후) az ad app credential delete --id $APP_ID --key-id $OLD_KEY_ID unset NEW # shell history 에서 제거 ``` ### 전제조건 — App Owner 확인 `az ad app credential reset` 은 app 의 owner 만 가능. portal-등록 app 은 default 로 owner 없음: ```bash az ad app owner list --id $APP_ID --query "[].userPrincipalName" -o tsv # 비어 있으면 → portal 에서 owner 추가 후 재시도: # open "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Owners/appId/$APP_ID" ``` 본 플랫폼의 모든 AXE app 은 `ai@axellc.com` owner 박혀 있음 (2026-05-21 audit). 신규 az cli 등록 app 은 호출자가 owner 자동 박힘. ## 다른 provider 회전 — `axe secret rotate` interactive Anthropic / GitHub / Meta / Naver / Slack 등 az 안 통하는 provider: ```bash axe secret rotate --service ``` 진행: 1. 매니페스트의 `rotation_external` 확인 → provider portal URL 출력 2. 운영자가 portal 에서 새 값 발급 → 복사 → 프롬프트에 붙여넣기 (`getpass`, 화면 안 보임) 3. vault PUT 4. `axe secret pull SVC` (merge-mode) 5. `axe ship SVC` 트리거 (working tree dirty 면 abort — `axe deploy ... --apply` 로 수동 보완) 6. 운영자가 provider 에서 OLD 수동 revoke 상세 데이터 흐름: [/architecture/secrets](/architecture/secrets). ## 회전 주기 — 정기 점검 Microsoft Entra ID app 의 client_secret 회전 주기 = 24개월 (Frame MCP), 180일 (Blueprint, Vaultwarden). 만료 30일 전 자동 알림. 미루지 말 것 — 만료 = OAuth 전체 마비. ## 사전 알림 **현재 상태**: `com.axe.secret-check` launchd 자동 알림 + `axe secret status --customer X` CLI 는 **향후 추가 예정** (TODO). 현재는 수동 확인: ```bash # 운영자 콘솔 (admin.axelabs.ai) 에 secret 만료일 표시 — `axe console rebuild` 가 매시 갱신 # 또는 Vault 에 보관된 메타에서 직접 확인: bw get item "Frame MCP — claude.ai connector secret" # 만료일 필드 확인 ``` 출력 예: ``` axe customer: blueprint: expires 2026-08-15 (89 days) vaultwarden: expires 2026-09-01 (106 days) frame_mcp: expires 2028-05-19 (730 days) ``` 90일 이내면 운영자 콘솔 dashboard 에 빨간 배너. ## 회전 절차 (180일 secret, ~30분) 예: customer `axe` 의 `frame_mcp` secret 회전 (24개월 만료). ### 1. 새 secret 발급 (customer IT 측) customer IT 에게 메일: ``` 제목: [AXE Labs] Frame MCP client_secret 갱신 요청 (만료 30일 전) 안녕하세요, 귀사 Microsoft Entra ID 의 'Frame MCP' app 의 client_secret 이 30일 후 만료됩니다 (2028-05-19). 다음 절차로 새 secret 발급 + 안전 채널로 전달 부탁드립니다: 1. Azure portal → Microsoft Entra ID → App registrations → 'Frame MCP' 2. Certificates & secrets → + New client secret 3. Description: frame-mcp-axe-2028-05 4. Expires: 24 months 5. Add → VALUE 즉시 복사 6. Bitwarden Send (view-once, `-a 1 -d 1`) 또는 안전 채널로 운영자에게 전달 — 절차: [/architecture/secrets § 사람에게 전달](/architecture/secrets#사람에게-전달--bitwarden-send) 운영자 측 swap 완료 후 기존 secret 은 안전하게 폐기됩니다. 감사합니다. 액스코퍼레이션 주식회사 (운영 주체, ai@axellc.com) ``` ### 2. 새 secret 받으면 ```bash # Keychain push (replace 기존) security add-generic-password -a axe -s "axe.axe.frame.client_secret" -w '<new_value>' -U # Verify security find-generic-password -s "axe.axe.frame.client_secret" -w # → <new_value> 출력되면 OK ``` ### 3. 컨테이너 재기동 (env reload) ```bash cd /Users/axe/frame set -a && source .env.local && set +a docker compose up -d --force-recreate frame-mcp-blue frame-mcp-green sleep 5 # Verify env 적용 docker exec frame-mcp-blue env | grep AZURE_FRAME_MCP_CLIENT_SECRET | head -c 30 # → 새 값의 prefix 보이면 OK ``` > ⚠️ **다운타임 ~3초** — `--force-recreate` 가 blue+green 동시 재기동. 향후 blue→green→blue 순차 재기동으로 0초 만들 예정 (D-ops-16 후보). ### 4. claude.ai connector 측 영향 `frame_mcp` secret 의 경우 **직원 측 영향 있음**: - claude.ai 의 Custom Connector 에 입력한 secret 도 갱신 필요 - 운영자 → 직원들에게 새 secret 전달 (안전 채널) - 직원 각자 claude.ai 의 connector 편집 → Advanced 의 OAuth Client Secret 교체 → Save > ⚠️ 이게 secret 회전의 가장 큰 운영 부담. 직원이 많을수록 painful. 향후 OAuth proxy 패턴 (D-ops-15) 으로 secret 분배 제거 검토. ### 5. 기존 secret 폐기 (24시간 후) 새 secret 활성 확인 + 직원 전원 swap 완료 후: ``` # customer IT 에게: "기존 client_secret (<처음 4글자만 hint, 예: XX.X~ 처럼 식별 가능한 prefix>) Azure portal 에서 삭제 부탁드립니다. 새 secret 으로 swap 완료 + 24시간 검증 완료했습니다." ``` > ⚠️ **secret VALUE 평문을 docs/메일/채팅 어디에도 적지 마세요**. 식별 hint 는 Azure portal 의 secret 표에서 보이는 hint prefix (보통 처음 3-4자 + "...") 만 사용. 운영자 본인이 회전 시점에는 Keychain `find-generic-password` 로 prefix 확인. customer IT 측에서 Azure portal → Certificates & secrets → 기존 secret 옆 휴지통 클릭. ### 6. 검증 ```bash # customers.yaml 메타 업데이트 (선택) # sso.apps.frame_mcp.client_secret_env 의 만료일 주석 갱신 # Audit (TODO: `axe secret status` 추가 후) # 현재는 Vault item 의 만료 메타 갱신 + 운영자 콘솔 dashboard 확인 ``` ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | 만료일 잊고 24시간 후 마비 | OAuth 전체 다운 | 매일 자동 알림 + 30일 전 작업 | | 새 secret swap 전 기존 삭제 | OAuth 즉시 다운 | 24시간 grace period | | Keychain replace 안 하고 새로 add | duplicate, 어떤 게 active 인지 모름 | `-U` 플래그로 update | | 컨테이너 restart 만 (--force-recreate X) | env file 재로드 안 됨 | `up -d --force-recreate` | | 직원에게 secret 일괄 push 잊음 | 직원들 connector 실패 | swap 전에 communication | ## 응급: secret 노출 의심 평문이 어딘가 노출된 의심이 있을 경우 (메일 전송 사고, 노트북 분실 등): 1. customer IT 에게 즉시 새 secret 발급 요청 (정상 절차 X, 비상) 2. 받자마자 Keychain push + 컨테이너 재기동 3. 기존 secret 즉시 삭제 (24시간 grace X) 4. audit_log 검사 — 의심 시간대에 비정상 access 있는지 5. 직원 전원에게 알림 + claude.ai connector 즉시 update 요청 상세: [Runbook · Secret 노출 대응](/ops/runbook/secret-incident) (예정). --- # Vaultwarden 복구 > self-host vault 복구, OIDC 깨짐 대응, sso_nonce 수동 패치. URL: https://docs.axelabs.ai/ops/runbook/vault-recovery # Vaultwarden 복구 ## AI 요청 프롬프트 ``` https://docs.axelabs.ai/ops/runbook/vault-recovery 따라 axe-vaultwarden 복구 진행해줘. 진행: 1. 현재 증상 진단 — docker ps + curl /identity/.well-known/openid-configuration + 사용자 보고로 시나리오 1~6 중 어느 분기인지 식별 2. 시나리오 식별 후 추가 분기 확인 (PostgreSQL vs SQLite backend / Timshel fork vs mainline / customer 측 axe.axelabs.ai vs realchoice 측) 3. 페이지의 각 명령 실행 + 검증, 매 step 결과 받고 다음. backup 또는 force-recreate 등 destructive 명령 직전 사용자 확인 4. 함정 발생 시 페이지 "함정" 표 따라 우회 (column 명 verifier vs code_verifier / digest pin / 시나리오 6 = 영구 손실 — 복구 불가) 5. 복구 완료 검증 (SSO 로그인 + vault item 조회 + admin endpoint) + 사고 원인 /ops/known-gaps 한 줄 (재발 방지) ``` 본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타. 페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감. `axe-vaultwarden` 서비스가 죽거나 OIDC 가 깨졌을 때. ## 시나리오 1 — 컨테이너 죽음 ```bash docker ps -a | grep vault # axe-vaultwarden Exited (1) # 단순 재시작 cd /Users/axe/.axe/vault docker compose up -d --force-recreate sleep 5 curl -sk https://localhost:8222/identity/.well-known/openid-configuration | head -5 ``` 설정 정상이면 정상 가동. ## 시나리오 2 — OIDC 깨짐 (sso_nonce 누락) D-ops-12 에서 다룬 알려진 이슈. Timshel fork 가 sso_nonce 테이블을 알아서 생성 안 함. **2026-05-25 정정** (Truvia realchoice 검증) — Timshel fork 의 실제 column 명 = `verifier` (NOT `code_verifier`). PostgreSQL / SQLite backend 분기 명령 별도. 증상: ``` ERROR: relation "sso_nonce" does not exist (PostgreSQL) 또는 sqlite> .schema sso_nonce → empty (SQLite) ``` ### PostgreSQL backend (axec axe-vaultwarden 패턴) ```bash docker exec axe-vaultwarden-postgres psql -U vaultwarden -d vaultwarden -c " CREATE TABLE IF NOT EXISTS sso_nonce ( state TEXT PRIMARY KEY, nonce TEXT NOT NULL, verifier TEXT NOT NULL, redirect_uri TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); " docker compose restart axe-vaultwarden ``` ### SQLite backend (`soohunkang/vault` repo + Vaultwarden 공식 image, 예: realchoice 의 ~/vault) ```bash # vault-app 컨테이너 안의 sqlite3 (또는 host 측에서 docker exec) docker exec vault-app sqlite3 /data/db.sqlite3 <<'EOF' CREATE TABLE IF NOT EXISTS sso_nonce ( state TEXT PRIMARY KEY, nonce TEXT NOT NULL, verifier TEXT NOT NULL, redirect_uri TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); EOF # 만약 위 schema 작성 후 column 명 mismatch 로 fail 보이면 (이전에 잘못된 column 이름으로 만든 경우): docker exec vault-app sqlite3 /data/db.sqlite3 "ALTER TABLE sso_nonce RENAME COLUMN code_verifier TO verifier;" docker compose restart vault-app ``` (db.sqlite3 경로는 `DATA_FOLDER` env 또는 `/data` volume mount 기준. compose.yaml 확인 후 적용.) ## 시나리오 3 — Microsoft SSO 갑자기 안 됨 원인 후보: - Vaultwarden app 의 client_secret 만료 - redirect_uri 등록 잘못됨 - AZURE_TENANT_ID 또는 AZURE_VAULTWARDEN_APP_ID 잘못 설정 확인: ```bash # .env 검증 cat /Users/axe/.axe/vault/.env | grep -E 'AZURE_(TENANT_ID|VAULTWARDEN)' # 컨테이너 env 검증 docker exec axe-vaultwarden env | grep -E 'SSO_(AUTHORITY|CLIENT)' ``` 해결: 1. customer IT 측 Azure 에서 새 client_secret 발급 2. `.env` 의 `AZURE_VAULTWARDEN_CLIENT_SECRET=` 새 값으로 3. `docker compose up -d --force-recreate` ## 시나리오 4 — 컨테이너 손상 / 데이터 손실 ```bash # 1. backup 확인 restic -r /Users/axe/.axe/backups/local snapshots \ --password-file <(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \ --tag vault # 2. 가장 신선한 vault snapshot 복원 # `axe restore` 는 Phase 5 stub — 현재는 restic 직접 + docker volume restore: restic -r /Users/axe/.axe/backups/local \ --password-file <(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \ restore <snapshot_id> --target /tmp/vault-restore --include '/Users/axe/.axe/vault/*' # 그 후 docker volume 또는 .yml volume mount 위치에 복사 # 3. 컨테이너 재시작 cd /Users/axe/.axe/vault docker compose up -d --force-recreate # 4. Microsoft SSO 시도 open https://axe.axelabs.ai/vault ``` ## 시나리오 5 — Timshel fork 업데이트 Timshel fork 의 새 release 가 나왔을 때. ⚠️ **신중하게**. Timshel 은 mainline Vaultwarden 보다 작은 PR queue → 가끔 breaking change. ```bash # 1. 현재 digest pin 확인 grep ghcr.io /Users/axe/.axe/vault/docker-compose.yml # 2. 새 digest 확인 (Timshel 의 GitHub Releases) # https://github.com/Timshel/vaultwarden/releases # 3. 비프로덕션 환경에서 테스트 (있다면) # 4. backup 전체 (vault + DB) # 수동 backup (현재 `axe backup` subcommand 없음 → restic 직접): restic -r /Users/axe/.axe/backups/local \ --password-file <(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \ backup /Users/axe/.axe/vault --tag vault-pre-upgrade # 5. .yml 의 image: line 갱신 vim /Users/axe/.axe/vault/docker-compose.yml # image: ghcr.io/timshel/vaultwarden@sha256:<new> # 6. 재시작 docker compose up -d --force-recreate # 7. 검증 # - admin 로그인 # - Microsoft SSO 로그인 # - 기존 collection 의 item 조회 # - 새 item 생성/조회 # 8. 실패 시 rollback docker compose down # .yml 의 image: 옛 digest 로 되돌림 docker compose up -d ``` ## bw CLI data.json cache stale recovery (운영자 측) 서버 측 복구가 아닌 **운영자 본인 머신의 bw CLI** 회복 절차. server-side vault patch deploy 후 local `data.json` cache 가 stale 상태로 잔존하면서 발생. **증상**: ``` [Encrypt service] MAC comparison failed ``` `bw unlock` 또는 `bw get ` 시 위 에러 → bw 명령 전체 실패. **언제 발생**: server-side vault patch deploy 후 (axe.2 → axe.3 등). bw CLI 의 local `data.json` 에 `cryptoSymmetricKey` cache 가 옛 schema 의 wrapped key 보유 → 새 server 가 내려준 패치 후 shape 와 mismatch → decrypt 시 MAC mismatch. `bw unlock` 자체는 cache 무효화 안 함. **빈도**: server patch 마다 1회 — 2026-05-22, 2026-05-26 두 차례 재발 확정. patch shape 변경 시 매번. **표준 회복 절차** (운영자 본인 머신, 약 30초): ```bash bw logout bw config server https://axe.axelabs.ai/vault bw login # email + MP + Entra MFA (interactive) bw unlock # MP — session token stdout export BW_SESSION="...token..." bw get password # 검증 ``` 이후 Keychain 의 `axe.vault.session` entry 갱신: ```bash bw unlock --raw | security add-generic-password -s 'axe.vault.session' -a "$(whoami)" -w "$(cat)" -U ``` **자동화 후보** (영구 fix, [B-bw-cache-stale-autoheal](/ops/backlog)): - `axe vault reset` 신설 — 위 4단계를 한 명령으로 - `_bw_get_password` 헬퍼가 `MAC comparison failed` 패턴 감지 시 자동 reset + retry - `axe ship vault` post-deploy hook 이 patch shape 변경 시 osascript 알림 — 운영자 본인에게 "logout/login 필요" pre-warn **함정**: | 함정 | 결과 | 회피 | |---|---|---| | ai@ personal vault 가 약 37 items → fresh login 시 sync 약 30초 대기 | `bw login` 직후 `bw get` 호출 시 일부 item 미반영 | login 직후 `bw sync` 명시 + 약 30초 대기 | | `bw login` 시 `BW_CLIENTID` / `BW_CLIENTSECRET` env 잔존 시 API key 모드로 진입 | SSO 흐름과 다른 인증 경로 — 일반 사용 OK 단 본 회복 절차는 interactive 가 가장 안전 | 회복 시 `unset BW_CLIENTID BW_CLIENTSECRET` 후 `bw login` | | `~/.config/Bitwarden CLI/` (또는 `$BITWARDENCLI_APPDATA_DIR`) 의 `data.json` 직접 삭제 시도 | clean state 안 됨 + session token 잔존 | `bw logout` 권장 (data.json + session 모두 정리) | | Keychain 의 `axe.vault.session` 옛 token 잔존 | `axe secret *` 호출이 옛 invalid session 으로 NO_SESSION_IN_KEYCHAIN | login 후 위 `security add-generic-password ... -U` 로 갱신 필수 | ## 시나리오 6 — 운영자 master password 분실 ⚠️ **이건 영구 손실**. Vaultwarden 의 master password 는 vault 의 encryption key. 모르면 모든 item 복호화 불가. 회피: 1. master password 는 종이 메모 + 신뢰할 수 있는 가족 (또는 다른 안전한 곳) 2. 매월 1회 master password 로 로그인 시도 (잊지 않게) 3. Bitwarden export 정기 + 그 export 파일 자체도 vault 의 master password 로 암호화 분실 시 — vault 폐기, 모든 item 재발급. 회사 운영 1주일 이상 마비 가능. 그래서 종이 백업 필수. ## 함정 | 함정 | 결과 | 회피 | |---|---|---| | Timshel digest pin 안 함 (`:latest`) | 무작위 업데이트 | 항상 `@sha256:<digest>` | | backup 안 함 | sso_nonce 깨졌을 때 복구 불가 | 매일 자동 backup | | .env 평문 commit | secret 노출 | .gitignore + Keychain | | master password 단일 보관 | 분실 시 영구 손실 | 종이 + 가족 이중화 | | 분기 drill 안 함 | restore 절차 실제 검증 안 됨 | `com.axe.restore-drill` 자동화 | --- # 운영자 문제 해결 > 직원이 보고하는 문제 분류 + 운영자 측 진단 단계. URL: https://docs.axelabs.ai/ops/troubleshooting # 운영자 문제 해결 직원이 "frame 안 됩니다" 라고 호출 왔을 때 진단 순서. ## Step 1 — 외부 health ```bash curl -sI https://axe.axelabs.ai/frame/health # 200 OK 인지 확인 ``` 200 OK 안 나오면: - → cloudflared 죽음? `docker ps | grep axelabs-tunnel` - → frame 자체 죽음? `docker ps | grep frame-mcp` - → Cloudflare 측 장애? Cloudflare status page 확인 ## Step 2 — protected-resource 메타데이터 ```bash curl -sS https://axe.axelabs.ai/frame/.well-known/oauth-protected-resource | python3 -m json.tool ``` 기대 응답: ```json { "resource": "https://axe.axelabs.ai/frame/mcp", "authorization_servers": [ "https://login.microsoftonline.com/122fb574-7efa-476a-95b6-bee81bce2cce/v2.0" ], "scopes_supported": [...], "bearer_methods_supported": ["header"] } ``` `resource` 가 다르면 → customers.yaml 의 public_domain 확인. `authorization_servers` 의 tenant_id 가 customers.yaml 와 다르면 → 환경변수 갱신. ## Step 3 — frame 로그 ```bash docker logs --since 10m frame-mcp-blue 2>&1 | grep -v 'health\|/ready' | tail -30 ``` 패턴: - `POST /frame/mcp 401 Unauthorized` 만 보임 → Bearer 안 들어옴, claude.ai 측 또는 Microsoft 측 문제 - `POST /frame/mcp 200 OK` + tool 호출 → 정상 - `INVALID_TOKEN` / `OIDC_UNAVAILABLE` / `EntityNotAuthorizedError` → 각각 다른 진단 ## Step 4 — 직원 측 정보 수집 직원에게 요청: 1. claude.ai/customize/connectors 페이지의 Frame connector 상태 (Connected / Disconnected / Authorization failed) 2. 오류 메시지 전문 (스크린샷) 3. 브라우저 주소창의 전체 URL (특히 `error_code=`, `entra_aadsts_code=`, `entra_trace_id=`) 4. 본인 회사 이메일 ## Step 5 — Microsoft sign-in log Azure portal → Microsoft Entra ID → Monitoring → Sign-in logs: - 직원의 trace_id 또는 시간 + 이메일로 필터 - Sign-in error code (AADSTS XXX) 확인 - Failure reason ## Step 6 — 분류 | 증상 | 원인 후보 | 해결 | |---|---|---| | AADSTS700016 | client_id 오타 또는 misconfig | customers.yaml 확인 | | AADSTS9010010 | resource ≠ scope prefix | Application ID URI ↔ scope 일치 확인 | | AADSTS7000218 | secret 없음 + public client flow off | secret 발급 + claude.ai 입력 | | AADSTS65001 | consent 안 함 | 직원이 consent 화면에서 Accept | | mcp_client_invalid | aud 검증 실패 (v2 토큰) | manifest accessTokenAcceptedVersion = null | | Couldn't reach MCP server | URL 오타 또는 frame 죽음 | Step 1 | | EntityNotAuthorizedError | customers.yaml 매핑 누락 | user_entity_map 에 추가 + frame 재기동 | | 권한 부족 | 직원의 scope 부족 | 권한 상승 결정 + customers.yaml | ## 자주 발생하는 운영자 측 실수 | 실수 | 증상 | 대응 | |---|---|---| | customers.yaml 수정 후 frame restart 안 함 | YAML 변경 안 반영 | `docker compose restart frame-mcp-blue frame-mcp-green` | | 환경변수 변경 후 `--force-recreate` 안 함 | env 안 읽음 | `docker compose up -d --force-recreate` | | Application ID URI cleanup (삭제) | AADSTS500011 | 복원 후 propagation 대기 | | Bitwarden secret 평문 평이메일 전송 | 노출 | secret rotation | | Cloudflare DNS A record 누락 | DNS 미해석 | record 추가 + propagation 대기 | | 운영자 콘솔 stale | dashboard 옛 정보 | `axe console rebuild` | ## 진단 cheat sheet ```bash # 인프라 health axe health <customer> # Cloudflare tunnel docker ps | grep tunnel docker logs --tail 50 axelabs-tunnel # Frame docker ps | grep frame docker logs --since 5m frame-mcp-blue | grep -v health docker exec frame-mcp-blue python -m frame.cli integrity-check --entity # customers.yaml cat /Users/axe/.axe/customers.yaml | grep -A 5 user_entity_map # Microsoft Entra # (Azure portal 또는 Microsoft Graph CLI) ``` ## Escalation 위로도 안 풀릴 때: 1. 운영자 본인 → 진단 + Slack `#alerts` 노트 2. customer admin 에게 통지 (영향 범위 안내) 3. 직원에게 임시 우회 (workaround 가능하면 제공) 4. 본격 디버깅 시간 잡기 (보통 1시간 내 해결, 안 풀리면 휴식 후 fresh 접근) ## 학습 — 이슈 사후 기록 해결 후 [DECISIONS](/ops/decisions) 또는 함정 모음에 추가: - 새로운 함정 발견했으면 표에 add - 기존 함정 변형이면 기존 항목 update - 향후 동일 사건 재발 시 참조 가능 --- # 업데이트 (Updates) > docs.axelabs.ai + 플랫폼의 ship 별 changelog + Highlights + API feed. 시간축 = 과거 (이미 한 일). URL: https://docs.axelabs.ai/ops/updates # 업데이트 (Updates) > **시간축 4-페이지 분리** ([D-docs-updates-1](/ops/decisions), 2026-05-22): > > | 페이지 | 시간 | 용도 | > |---|---|---| > | [roadmap](/ops/roadmap) | **미래** | 분기 마일스톤 (M1~M5) | > | [backlog](/ops/backlog) | **현재** | 1주 안 행동 큐 | > | **본 페이지** | **과거** | ship 별 changelog + 큐레이션 highlights | > | [known-gaps](/ops/known-gaps) | (사실) | 함정·미해결 | 본 페이지는 **3 섹션**: 1. **Ship Log** — 매 `axe ship SERVICE` 마다 한 줄 자동 기록 (Phase 2: [`B-axe-ship-update-hook`](/ops/backlog)). Phase 1 (현재) = 수동 append. 정직 채널, 운영 회고용. 2. **Highlights** — 사람 큐레이션. 의미 있는 변경만 narrative (직원·외부 IT 용). 3. **API feed** — [`/api/changes`](/api/changes) JSON endpoint. LLM agent 가 "지난 N일 변경" fetch. --- ## Ship Log > 매 ship 의 한 줄. `2026-MM-DD HH:MM — SERVICE ship — COMMIT-SUBJECT` (commit hash + service stack). > > Phase 1 (수동 append) — 운영자가 `axe ship SERVICE` 후 한 줄 추가. > Phase 2 (완전 자동) — backlog 의 [`B-axe-ship-update-hook`](/ops/backlog) 항목. | 일시 (KST) | Service | Commit | 한 줄 | |---|---|---|---| | 2026-06-05 | vault (axe org) | (운영자 action + docs) | **vault axe.3 — 4명 KDF rotation 전부 완료 → D-ops-40 전체 종결** ([D-ops-40](/ops/decisions) Progress xiii). ai (5/27) → soohun → taehun (5/29) → jinwoo (6/4) 순서로 SSO→MP unlock 정상화. **본질 발견 = KDF dropdown 변경 불필요** — Master password change + ☑ "Also rotate my account's encryption key" 만으로 user.akey 재wrap 충분 (kdf_type 안 바뀌어도 fix 동작, ai 만 Argon2id 까지). jinwoo last_login 5/23→6/4 점프 = 완료 신호. taehun 실사용 피드백 6건 → [/onboard/vault-setup](/onboard/vault-setup) playbook 반영 (Windows section + Phase 강제도 + TOTP UI 동작 + SSO 사용자 검증 분기 + valid 변형) + [B-vault-revoke-scope-doc](/ops/backlog) (vault trust boundary). [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) ✅. **D-ops-40 전 작업 (build pipeline + 4 patches + GHCR + deploy + 검증 3건 + 정책 layer + 4명 rotation) 종결** — self-host vault 의 Bitwarden client compat + organization 권한 + SSO→MP 영구 fix 완성. | | 2026-06-04 | docs | `22b1334` | docs(frame): D-frame-2 evidence blob-storage + D-frame-3 CF classification rule _(auto-logged by `axe ship` — curate/expand, then Highlights)_ | | 2026-06-04 | docs | `ed65f9b` | docs(ops): netheal install complete + Claude.app pty-leak osascript workaround _(auto-logged by `axe ship` — curate/expand, then Highlights)_ | | 2026-06-04 | hive | `48f38a1` | feat(auth): trust Blueprint platform tokens (D-axe-idp-1 Phase 2) _(auto-logged by `axe ship` — curate/expand, then Highlights)_ | | 2026-06-03 | frame | `f1b124d` | feat(auth): trust Blueprint platform tokens (D-axe-idp-1 Phase 1) _(auto-logged by `axe ship` — curate/expand, then Highlights)_ | | 2026-06-04 | index | `b273979` | **evidence-durability v1 — citation sha256 anchor (B-index-evidence-durability)** — 운영자 "workspace unique id" 제안 + 폴더 이동/삭제 분석에서 도출. citation 을 fragile path → stable anchor: `sha256` content-anchor(범용·무의존) + Blueprint-id(`workspace_id`/`driveItemId`/`marker`) 필드 readiness. `CitationAnchor`+`is_durable` · ingest sidecar sha256 + **propose-time fallback hashing**(skill 무변경에도 durable) · `verify_citations` MCP tool · 현황 페이지 durable% 라인. **additive**(기존 357 artifact/545 citation byte-identical) · 80 test · validate-seeds Δ0 · 공개 페이지 누출 0. blue/green deployed. deferred(v2): 545 backfill · Blueprint resolve API · driveItemId 채우기. | | 2026-06-03 | blueprint | `57ff2ac7` | feat(auth): Blueprint as platform OIDC Provider (D-axe-idp-1 Phase 1) _(auto-logged by `axe ship` — curate/expand, then Highlights)_ | | 2026-06-03 | blueprint | `#381`+`#382` | **전략 P3 — investment skill vertical-gate (contamination purge, D-index-47)** — 사용자 merge 승인. 8 투자 skill(ic·ingest·due-diligence·vc-deal-sourcing·investor-relations·portfolio-management·legal-compliance + pmc)에 `owner:index` 태그 + blueprint boot(`start.sh`)/webhook(`route.ts`) 양 경로 gate(customer `services.index` → 비-index `rm -rf`+skip purge, index keep). **axe 3중 안전**(services.index + `:-axe` 기본 + fail-safe keep + 운영자 ungated → simul KEEP 16/16) · realchoice/Truvia PURGE 7/KEEP 9 · `customers.ts customerHasService` + test 21/21 · tsc 0. pmc(index-only)도 base 로 mirror(gated→axe 만). PR #381(gate)+#382(pmc) squash-merged origin/main. 운영자 잔여: Truvia env 확인+재기동 purge + R7 DB residue. rollback=revert(axe 무영향). | | 2026-06-03 | index | `6edf0ff` | **전략 P2 — index→blueprint attest sentinel + mirror mechanism (D-index-47)** — 사용자 "P2 진행". ⭐ 조사 결과 "런타임 fix" 는 **이미 달성**(P0+기존 sync): ic/ingest 가 index SoT↔blueprint origin/main↔런타임 byte-identical(attest IN-SYNC). 그래서 P2 = 위험한 content push 아닌 **메커니즘**: `index-skill-sync.py`→`--attest`(deterministic dir-sha256 drift sentinel) 재범위 + `index-skill-mirror.sh`(미래 미러, sanctioned PR flow, pmc fail-closed) 빌드. **blueprint 무쓰기·무push·무PR**, pmc 보류(P3 contamination gate 전). note-3 worry 해소 — 런타임이 stale 이 아니라 index 가 stale 이었음. | | 2026-06-03 | index | `11aca12` | **전략 P1 — provenance stamp + schema-drift gate (D-index-47)** — 사용자 "P1만 진행"(P2/P3 명시-go 보존). 모든 propose 경로의 `artifact_event(op=propose).payload_after` 에 `_provenance` 스탬프(frozen_enums_hash 포함) + schema-drift hard gate(불일치→`SCHEMA_CONTRACT_DRIFT`, insert 전 fail-closed). frozen_enums_hash = 전용 SoT fn(D-index-15 enum + SCRUB_KINDS + ARTIFACT_REF_RELATIONS) pinned tripwire + /index/schemas 가 publish(외부 ic-push discover+pin). additive(기존 357 artifact/event byte-identical, append-only 라 retro-stamp 없음) · 66 test(+17) · sqlx clean · validate-seeds Δ0. blue/green redeploy + live 드리프트 probe 검증(무오염). | | 2026-06-03 | index | `8ec2c10` | **seed = SoT for judgment facts — hardcoded backfill 폐기 (D-index-46, B-index-judgment-in-seeds ✅)** — 사용자 "위생 본질 전략 순" 의 본질 leg. [D-index-45](/ops/decisions) 의 1회성 hardcode backfill(`backfill.rs`+`corpus.rs` 2824 LoC)을 제거 → seed.yaml 이 judgment 단일 SoT. per-seed `judgment:`(assumptions+calibrations) + `seeds/_corpus.yaml` + seed-ingest `--emit-judgment`(natural-name anchor→corpus id 해소, idempotent·confirm-on-create). 23 seed 마이그레이션 후 두 모듈 삭제. **HARD GATE**: 전 23 seed 재ingest +0 new · committed wipe(318→0, relational 무손상)→seed-ingest 단독 +318 byte-identical 재현(42 stale dup citation pruned) · append-only trigger intact · validate-seeds 23 Δ=+0.00 · 51 test. **신규 deal 자동 artifact화** — hardcode transcription 0. | | 2026-06-03 | index | `1c707e1` | **artifact-first judgment layer — 증거층↔판단층 결합 (D-index-45, Epic 1, 2-wave supervised)** — 사용자 "Epic 1 같이 검토하고, 실제 구현은 서브에이전트로 · 본질에 집중 quick-fix 금지". 휴면 artifact store(39 fact)→relational 23 deal 결합: 모든 투자판단 fact 를 일급 typed+cited+audited artifact 로 승격. migration 0(generic store 재사용), 4 kind(assumption·calibration·comp·base_rate), 링크=일급 citation(index_field+artifact_ref relation, side-bridge table 아님). **23/23 deal → 357 fact**, 0 orphan judgment value. additive-only 3중 증명(event propose/confirm 만·relational updated_at 미전진·validate-seeds Δ=+0.00), 48/48 test, sqlx --check clean. ⭐ adversarial gate 가 wave1 61% 미완 적발 → wave2 5딜 author + 6 rival backfill module → 1 `backfill.rs` 통합. 방향: structured-seed 파생[B-index-judgment-in-seeds]·Epic 2 confidence load-bearing·Epic 3 self-calibrating loop. | | 2026-06-03 | docs | `3eade41`+`19ffd2c` | **backlog C1–C5 인터랙티브 정비 + axe-cli ship-hook/drill ✅ 반영** — 운영자와 C1(entity legal영문+display한글, frame/hive/blueprint 마이그레이션 준비·미적용)·C2(axe-cli git-track → 비공개 `axelabs-ai/axe-cli`)·C3(`axe drill` freshness+robust-canary · `axe ship` post-deploy Ship Log hook)·C4(설계 DRAFT 5건 보존)·C5(미커밋 코드 전부 보존) 순차 해결. ⭐ **premise 정정**: B-backup-restore-drill = drill 메커니즘 강화일 뿐 postgres 실제 restore drill 은 미완(open 유지). 동반 게시: D-hive-29 payroll UPSERT/adjustment docs · @axe/ui resync. ⭐ 본 row = ship-update-hook 첫 실가동 산출(자동 stub → curate). | | 2026-05-30 | index+blueprint | `931284b9`+`c8b3746` | **ingest 데이터품질 metric_kind 세분 + lifecycle 회귀 스위트 (D-index-44, 5축 평가 2 unit 병렬)** — 사용자 "지금 가능한 작업, 본질·퀄리티·결과·안전·혁신". **(X)** blueprint PR #380: convert_xlsx metric_kind 세분 — coarse valuation(절대 EV+EV/EBITDA 배수+음수 혼재, reconcile 가 노출한 결함) → `valuation`/`multiple`/`moic` 분리 + curation(음수/absurd helper drop). 52/52 test, irr·moic·cagr 0 regression(XIRR function-path 무영향). webhook sync live. **(Y)** index `c8b3746`: artifact lifecycle 회귀 8 test (idempotency·status 파생·confirm/reject·append-only(grant+trigger 이중)·RLS 격리·reconcile·seed-draft non-ingestability serde-reject), 35 pass 0 residue, offline-safe. ⭐ 발견: append-only trigger 가 ON DELETE CASCADE 차단 → event 보유 artifact 삭제불가 = audit 영속성. **통합**: 정제 classifier 재ingest → store refresh → valuation 12 + multiple 4 split 반영(39 facts). self-correcting 루프(reconcile 가 노출 → source fix). | | 2026-05-30 | index | `3eece93`+`118568a` (deploy) | **artifact-first lifecycle 완성 — Query API+ctx review+reconcile+seed-draft (D-index-43)** — 사용자 "서브에이전트로 종결 후 보고". D-index-42(propose) 잔여 4개를 2 supervised subagent 순차 구현 + 1 blue/green. **(1) Query API**: `query_artifacts`/`get_artifact` (status=최신 event.op 파생). **(2) ctx review**: `confirm_artifact`/`reject_artifact` (op=confirm/reject, append-only 감사). **(3) reconcile**: `reconcile_artifacts` — metric_kind 별 value spread flag(불일치 전량 surface, silently 택1 금지; Apposter irr 86% flag). **(4) L2 본령 seed-draft**: `draft_seed_from_artifacts` — deal artifacts → 보수적 seed.yaml SCAFFOLD(`index draft-seed`). ⭐ 날조금지 구조강제: surface→audit주석 only, calibration·intake·baseline 전부 `~ TODO`, valuation 후보다수 자동선택 거부, draft non-ingestable(validate-seeds fail). coverage auto_filled 0/needs_human 18/surface 29. RLS 정식경로, build online+offline clean, 27 test, 6 MCP tool live(ext healthz 200). artifact-first 전 lifecycle 가동(propose→query→review→reconcile→draft). 잔여: ctx review UI(Blueprint)·Query API /ic 통합·extractor metric_kind 세분·전 deal artifact화. | — 사용자 "ingest 때 artifact화 한다던 것 구현됐나?" (정직 확인). **gap 발견**: index artifact/citation/artifact_event store([D-index-2](/ops/decisions) cortex mirror)가 Day-1 이후 **0행 미사용** — 23-deal 은 relational 테이블에만, artifact-first 층 dormant. **구현**(subagent + orchestrator QC): `propose-from-ingest` CLI + `propose_artifacts_from_ingest` MCP tool — D-index-41 sidecars(`cells.json`/`figures.json`) → 각 key_output/figure = `artifact`(extracted_metric|extracted_figure) + `citation`(cell/page 증거) + `artifact_event`(op=propose). RLS 정식경로(index_app+index.actor GUC, `db::set_current_user` 첫 실사용자), idempotent. **before/after (Apposter)**: store **0 → 35** (31 metric+4 figure, 1:1:1 artifact:citation:event). Exit IRR 57.2% = typed artifact(`metric_kind=irr`)+citation `Return!Y62`+propose event. append-only+RLS live 검증. build clean(online+offline, sqlx guard), blue/green 재배포 — `propose_artifacts_from_ingest` MCP tool live(both healthy, ext healthz 200). [D-bp-artifact-5](/ops/decisions) "proposed fact" 전반부 실현. 잔여: ctx review + Query API + L2본령(seed-draft) + L3 reconciliation. | | 2026-05-30 | blueprint | `PR #379` | **ingest 고도화 L1 — 재무모델·IR structured extraction (D-index-41)** — 사용자 "ingest 역량 끝까지 고도화, 서브에이전트로". impl agent(worktree, skill-sot-guard 준수) + orchestrator QC. ingest 가 dataroom xlsx/pdf 를 md photocopy 하던 lossy 문제(수식 40개 truncate → `` 매몰, 기계추출 불가) 해소. **convert_xlsx v2**: `{stem}_xlsx.cells.json` (full grid value+formula+number_format+label, named_ranges, **key_outputs[]** XIRR/IRR/NPV/MoM/CAGR/valuation 자동탐지) + md Model Summary/Key Outputs/formula↔value 표. **convert_pdf v2**: `figures.json` 라벨인접 재무 figure sweep. 숫자=mechanical only([D-index-15](/ops/decisions)), citation(cell/page) 자동. **before/after (Apposter 20-sheet model)**: Exit IRR `=XIRR(…)` 가 truncated 주석 매몰 → **31 key outputs 자동 surfacing** (Exit IRR 57.2%/50.4%·MoM 8.60x·Entry EV 270억·9x exit, 값+수식+라벨). test 40/40 PASS·기존 포맷 무변경·의존성 0·datetime fix. QC: broad-recall false-positive(date·mislabel CAGR 25M) numeric+rate 게이트로 34→31 정정(true output 보존). PR #379 merged→main(`3a9966bd`)→launchd webhook sync→`~/.claude/skills/ingest` v2 live. [B-index-ingest-structured-extraction](/ops/backlog) L1 ✅(L2 typed seed-draft / L3 reconciliation / L4 versioned-diff 잔여). | | 2026-05-30 | index | `de47a2c` | **Render/리얼초이스(23rd) 적재 + par-entry cheap-option = E[CF] 극단 (D-index-40) — 데이터룸 소진, DB 23** — 사용자 "더 추가할 프로젝트?". 데이터룸 전체 스캔 → 적재가능 신규 = Render 1건. **Render = 리얼초이스/트루비아**(데일리퓨어크레아틴, 크레아틴 건기식). best-effort SCREEN, status=passed (미투자 — AXE 플랫폼 고객이나 고객≠투자집행). ⭐ **par-entry**(5,000만@100원=액면=순자산, 트루비아 벤처확인 enabler 신주) → 어떤 성공도 거대 multiple → **E[CF] 35.13%**(랭킹 4위 수준)이나 **E[MoM] 4.53x·P(loss) 68%·median 손실**. exit multiple 은 0.8-1.4x commodity scrub 완료([D-index-26](/ops/decisions) fantasy 아님) — 높은 E[CF]=par 진입 구조적 산물. ⭐ **[D-index-25](/ops/decisions) poster child**: E[CF] 단독 랭킹은 par-entry 딜에서 오해 — Render 35% = top deal 아닌 **68% 손실확률 cheap call option**. SCREEN=PASS, 단 5,000만=트루비아 벤처확인 enabler+고객관계 cheap option 으로 재무 IC 와 분리. deal_id eb83b883. **데이터룸 소진**: 잔여 Whale=위시켓(자료 확보 시)·DHP 3(빈 폴더)·Artemis(AXE 자체 제품)·델리후레쉬(RFP)·thin lead 5 = 비-딜/자료대기. **23-deal 랭킹**: Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > **Render 35.13(⚠par cheap-option)** > Canopy 31.8 > Interstellar 30.5 > … > Open Research −25.85. validate-seeds Δ0 Pass. | | 2026-05-30 | index | `7d6ccca` | **Archive batch 6-deal 적재 (17~22th, D-index-39) — 전부 correctly-avoided — DB 22** — 사용자 "1_Project (Archive) 6건 모두 best effort 검토". 6 병행 agent + orchestrator 양방향 QC. 전부 archived=미투자 → [D-index-24](/ops/decisions) calibrated canonical. **Medistaff**(의료진, screen) **−0.59%** (founder 형사 jeopardy+두나무 lead) · **Open Research/oo.ai** **−25.85%**(P loss 90%) — ⭐ "deal structure ≠ company quality"(회사 2,000억 라운드 중이나 창업자 개인 distressed 담보 secondary bridge, 도박/사채/팀 9-of-9 이탈) · **JS E&L**(2차전지 부품) **−1.97%**(EV 캐즘·자본잠식 임박, PER 16x→2-8x scrub) · **Catalyst/클래스카드**(교육 PE buyout) **16.99%** — index 2nd buyout + 첫 PEF/GP구조(AXE=GP, committed=consortium 170억, 10x flat exit→5-9x scrub+60% LTV wipeout, first-time-GP 0 commit, "높은 IRR≠Go") · **데이톤/DATON**(DC AI Ops, screen) **16.57%**(흑자 100억 매출·DC tailwind 진성이나 terms-void) · **Apposter/b.ring**(스마트링) **8.34%** — ⭐ "field DD kills thesis"(현장실사 Bic Camera 방문→commodity 입증→Case Drop, Oura 10x PSR→2-3x scrub). ⭐ 교훈: (1) negative-E[CF] 기록 = PASS 결정의 정량 검증, (2) 양방향 QC(instrument floor·fat-tail scrub·wipeout)가 agent 에 briefing 만으로 전파. **22-deal 랭킹**: …EGA 23.3 > (Superman 20.8) > Catalyst 16.99 > 데이톤 16.57 > Infinity 16.5 > Eduon 15.0 > 디벨로퍼그룹 14.76 > 수성별 14.0 > Sendy 11.7 > Nanora 10.1 > Novachips 9.2 > Apposter 8.34 > 유비랩 4.06 > Medistaff −0.59 > JS E&L −1.97 > Open Research −25.85. validate-seeds Δ0 6/6 Pass. | | 2026-05-30 | index | `c0bf6d5` | **디벨로퍼그룹(15th)+유비랩(16th) 적재 — buyout-class + 양방향 QC (D-index-38) — DB 16** — 사용자 "디벨로퍼그룹·유비랩 검토". 입력 richness 양극. **디벨로퍼그룹 스터디카페 사업부 100% 영업양도** = index **최초 buyout-class**(PE형 캐시카우 M&A, AXE 12억/EBITDA 3.87x, 무차입, 보통주/secondary). full IC v5 surface 공식 34.09% → **메모 자신의 Devils-reweighted 16.5% anchor**([D-index-24](/ops/decisions) 미집행→calibrated + [D-index-26](/ops/decisions) Exit 5.0x→4.0x·조정EBITDA·kakao 9,251M fantasy scrub + [D-index-21](/ops/decisions) genuine wipeout) → **E[CF] 14.76% / E[MoM] 2.02x / P(loss) 15%**. ⭐ buyout 신 insight: cashflow buyout 의 FCF 배당이 실질 하방 floor → downside ~1.1x(원금손실 거의 없음), **P(loss) VC 대비 구조적 저**. deal_id b88e2dcb. **유비랩**(그래핀/그라파이트 방열 시트 deep-tech, OLED foldable+AI 반도체 TIM) SCREEN(병행 agent + orchestrator QC): elite founder(유봉현 삼성D 33년·NeoGraf 美VP)+삼성D/애플 샘플 진성 traction 이나 component multiple cap(직접경쟁사 신화인터텍 EV/Rev 0.2x)+pre-rev hockey-stick(92억@3y 2,500x)+Samsung 단일고객 → **"좋은 회사 ≠ 좋은 딜"**(Interstellar/Novachips 가족). ⭐ **양방향 QC** (D-index-37 보완): agent 가 base 를 as-converted(0.76x)로 계산해 E[CF] 2.9%/P(loss) **80%** 산출 → 본인 가정 RCPS 1x 우선권을 base(EV 143억≫20억)에 미적용한 **내부 불일치 artifact** 교정(우선권 floor 일관적용 base 1.0x) → **E[CF] 4.06% / P(loss) 43%**(peer 정합). QC=올바른 수치 착지(낮은 수치 아님) — fat-tail 도 instrument-floor artifact 도 교정. deal_id c98ba89e, stage=Screening. **16-deal 랭킹**: Sentry 43.5 > … > Eduon 15.0(screen) > **디벨로퍼그룹 14.76(buyout)** > 수성별 14.0 > Sendy 11.7 > Nanora 10.1 > Novachips 9.2 > **유비랩 4.06(최하, screen)** (+ Superman 20.8 별도). validate-seeds Δ0 둘 다 Pass. | | 2026-05-30 | blueprint | `247b41f8` | **send_mail MCP 도구 — 범용 outbound email (D-bp-mcp-mail-1)** — `create_event` 형제. caller 본인 mailbox 발송(delegated `Mail.Send` 이미 SCOPES → self re-consent 불요) + admin send-as(`as_user_email`, app-only `Mail.Send`+consent 선행). 신규 route `POST /api/internal/mail` + Python tool `mail.py` + append-only `MailSendLog` audit(`sent`/`dry_run`/`failed`) + migration `20260529120000_add_mail_send_log`. params to/subject/body(html\|text)/cc/bcc/attachments(base64)/save_to_sent_items/dry_run. 에러 `send_as_forbidden`/`recipient_invalid`/`mail_send_not_consented`/`graph_error`. Graph `sendMail` 202 no-body → `message_id` null. ⭐ **동시 세션 격리 배포**: 내 커밋만 origin/main 위로 cherry-pick(247b41f8), 다른 세션의 unpushed `579a0fa1`(index-skill-sync) + 미커밋 WIP 는 `wip/index-skill-sync-579a0fa1` + stash 로 보존하고 배포 제외. | | 2026-05-30 | index | `fc56004`+`7b4a50d` | **EGA·수성별·Nanora 3-deal batch 적재 (12~14th deal, D-index-37) — DB 14** — 사용자 "EGA·Nanora·수성별 모두 진행" + "Nanora full ic". 3 병행 background agent(deal당 1) + orchestrator QC. ⭐ **핵심 교훈 — full-IC 메모를 blind transcribe 하면 안 됨** (메모 rigor 가 deal 마다 극과 극): **EGA**(디파이넘버, NMN 뉴트리코스메틱) 메모는 surface-optimistic → agent 가 그대로 옮긴 E[CF] **127%**(매출 16억→550억 34x + EV/Rev 7× + 18x MoM@2y, wipeout 無)는 [D-index-26](/ops/decisions) fat-tail 환상(Iippo 74% 재현). status='passed'(미투자)라 [D-index-24](/ops/decisions)상 calibrated 가 canonical → **scrub**(ramp 16→300/120억 + EV/Rev 5×/3.5× K-beauty M&A anchor + **genuine wipeout 0.2x**: 창업자 이력 부재·매출 16억 미검증 red flag) → **calibrated E[CF] 23.3%** (E[MoM] 2.33x, P(loss) 40%); surface 는 헤더 audit trail 보존. 반면 **수성별**(국군복지단 DOOH 광고, 우선협상대상자) 메모는 이미 보수적 — Σp −23.8% → E[CF] **+14.0%**(sign-flip, Interstellar 패턴). 단 **deal 조건 전부 dataroom 부재**(10억/30억/25% 가정) + 팀공백 + CAPEX gap = info-void Conditional(stage Passed, instrument TBD). **Nanora**(US precision-wellness, TCM→구독 supplement+smart ring+AI, SAFE @ $9M post cap, pre-launch)는 사용자 지시로 **SCREEN→FULL IC 승격**: 18-section + multi-scenario + adversarial(proponent/premortem-critic 분리). E[CF] **10.05%**(E[MoM] 1.76x, P(loss) 50%) — Care/of($225M→Bayer→2024 폐업) category-graveyard + Pre-Seed→A 85% fail base-rate 로 회사 plan 하향, 판정 **Conditional**(결정변수 = post-launch 90-day cohort retention, T+9M revisit). **QC 결론**: agent batch 는 효율적이나 transcribe 산출은 반드시 orchestrator 가 fat-tail/wipeout/base-rate QC (EGA 127%를 적재 전 차단). **14-deal canonical E[CF] 랭킹**: Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Interstellar 30.5 > Hancom 23.9 > **EGA 23.3** > Infinity 16.5 > Eduon 15.0(screen) > **수성별 14.0** > Sendy 11.7 > **Nanora 10.1** > Novachips 9.2. 엔진 재확인: `validate-seeds seeds/*.yaml` 전 seed status Pass — **EGA E[CF] 23.30% · 수성별 14.02% · Nanora 10.05%** (docs 값 정합), baseline Δ0 regression 은 committed-baseline 3-deal(Iippo/Sentry/Canopy)에만 적용·나머지는 live-computed. | | 2026-05-30 | index | `eduon seed` | **SCREENING 단계 정규화 (D-index-36) + 에듀온 11th deal (첫 Screening)** — 사용자 "screen 단계 추가 + 정규 프로세스화 + 적재". 1-pager inbound 에 19-agent `/ic` 바로 돌리는 GIGO 방지용 **Screening 게이트** 신설 (deal.stage 'Screening' enum 기존재 — migration 불요). 프로토콜: 외부 리서치 5종 + base-rate calibrated 재무모델 + engine IRR(E[CF]) + premortem 필수, dataroom 불요, "DD 자원 투입 가치" 판단(Go/No-Go 아님). **첫 사례 에듀온**(초등 영어 Writing 프랜차이즈 Seed 보통주 5억@Pre 20억): WebSearch 5 + franchise rollout 재무모델 → **E[CF] 15.0% / E[MoM] 2.0x / P(loss) 45%**. ⭐ 교훈: 회사 IRR 58% / Claude ad-hoc 26.5%(upside 10x) → **calibrated 15%** — 가맹 "3년 1,000"(3030영어 21년→1,400) + exit "3x rev/300억"(크레버스 0.59x·웅진 0.20x) 두 낙관이 base-rate·comp 에 깨짐 → upside 6x cap. **Screen=PASS(DD 미진행)** + 재screen 조건(RCPS+Pre 10-12억+가맹 traction). 산출물 `ic/{research,finance,memo}/` + index 적재(deal_id 2bb5d585). 향후 ic skill screen-mode = [B-index-ic-screen-mode](/ops/backlog). DB 11 deal. | | 2026-05-30 | index | `7269286` | **Interstellar (인티그레이션) 적재 — 10th deal (D-index-35)** — 사용자 "추가 적재 프로젝트?" → 데이터룸 스캔, IC 분석 완료·미적재 = Interstellar 1건 (Render IC 미완 / Whale admin docs / 델리후레쉬 RFP / Pipeline 스크리닝 — 미대상). 인티그레이션(한의·치과 5-카테고리, 한의사 가입률 83%) Series D RCPS, AXE follow 검토 20억 **PASS** (알토스+네이버 lead 종결). D-index-24 PASS→calibrated canonical (status='passed'). **E[CF] IRR 30.5% / E[MoM] 3.23x / P(loss) 36.0%** (IC surface Σp 25.3%). ⭐ PASS 사유=거버넌스(R6 자기거래 IPO reject≥30%)+1.6% minority, **IRR 아님** — 9-deal 중 5위(Canopy 31.8 > Interstellar 30.5 > Hancom 23.9)인데도 PASS = "높은 IRR ≠ Go". 부수: seed.rs SERIES_OK 에 Series D/E 추가(누락 보완). validate-seeds 12 Pass / 23 test / DB 10 deal. (live MCP 는 DB 데이터 즉시 반영 — SERIES_OK 는 ingest-only 라 재배포 불요.) | | 2026-05-29 (eve8) | index | `537c04e` (deploy) | **live index MCP blue/green 재배포 — D-index-25~34 코드 라이브** — 그간 commit/검증만 됐던 index 코드(option/passed status·intake 게이트·Σp 폐기·instrument·E[CF] canonical)가 live MCP 에 미반영(blue/green 컨테이너가 35h+ 구 바이너리 서빙 = 새 데이터에 구 코드 = option/passed 오집계)이던 것 배포. `axe ship index` = `manual_hint`(자동화 미구현, [B-index-azure-app](/ops/backlog) 후 frame_blue_green 승격 예정)라 **수동 compose 재배포**: `docker compose build` (SQLX_OFFLINE + `.sqlx` 최신) → green 재생성·healthy·new-binary 검증(sentry option 수락 + E[CF] 43.5% + 폐기코드 가드) → blue 재생성(alias 유지, Caddy `lb_try_duration 5s` 가 restart blip 흡수)·healthy → 검증. **DB 데이터는 이미 current** (option 1·passed 4 status + intake 필드 11/12, baseline 은 live 계산이라 financial_output 0행 = 정상) → 재적재 불요, 코드만 stale 이었음. 검증: 내부 healthz + 외부 `https://axe.axelabs.ai/index/healthz` 200, 4 컨테이너 healthy, 두 이미지 fresh(14:48). | | 2026-05-29 (eve7) | index | `537c04e` | **canonical E[CF] 랭킹 확정 (D-index-34) + seed 주석·description E[CF] 재기재** — "정확도 심화" 마무리. **(1) D-index-34**: D-index-19/21/23/24 의 인라인 랭킹이 Σp/surface 기준이던 것 → engine `validate-seeds` 로 9 deal E[CF] 재산출, canonical 랭킹 확정: Sentry 43.5 ≈ Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Hancom 23.9 > Infinity 16.5 > **Sendy 11.7 > Novachips 9.2** — 구 Σp 대비 **tail flip** (Sendy↑Novachips) + Sentry-Iippo 거의 동률. Infinity executed 특례: v8 모델 불변(D-index-24) + 집계만 E[CF] 16.5%(D-index-32), IC headline 13.8% provenance. D-index-23/24 인라인에 "⚠ pre-E[CF] → D-index-34" 마커. **(2) seed 주석 정합** (index `537c04e`): starnex 36.4→38.1·hancom 22.4→23.9·sendy 6.5→11.7·infinity 13.8→16.5 주석 E[CF] 재기재 (value 불변). **(3) description 정정** (index `21e533d`, 병행 에이전트): 재척도 후 stale "3억 ×"(starnex→20억)·"× 10% = 222.5억"(sendy→2%/44.5억). **(4) #82 driver refactor 이미 완료 확인** (Iippo/Canopy 단일 revenue_growth). proceeds-bridge-retrofit(#81)은 날조위험+D-index-24 충돌로 보류. 11 seed validate Pass, E[CF] 전부 불변. | | 2026-05-29 (eve6) | index | `74e1eb4` | **irr_instrument_adjusted 폐기 (D-index-33) + Σp output_code 가드** — 사용자 "수익성 정확도 심화" 선택 후 조사. **발견**: exit_matrix leaf recovery 가 instrument 구조를 이미 전역 반영 (RCPS preference floor: Sentry 0.05~0.14·Starnex 회생 1.10x / 보통주 후순위 wipeout: Hancom 0.30·Sendy 0.15·Novachips 0.20·Infinity 0.12) → IRR(E[CF])가 이미 instrument-aware. Novachips 1건의 `irr_instrument_adjusted`(-4.55% = E[CF] +9.2% −12pp)는 leaf wipeout(0.2x) **이중계상** (Σp·IRR 동형 함정). **조치**: (a) novachips baseline 제거 + 헤더 재프레이밍, (b) `seed.rs` DEPRECATED_OUTPUT_CODES load-time hard reject — `irr_instrument_adjusted`(D-index-33) + `irr_loss_included`/`irr_success_only`(Σp, D-index-32 완결), (c) overlay 잔존 Σp 정합 — infinity_recalibrated/superman_fresh 의 `irr_loss_included` → `irr_expected_cashflow` (engine E[CF]: Infinity -4.3% [구 Σp -8.3%] / Superman fresh 20.8% [구 Σp 10.5%]). [B-index-instrument-adjusted-irr](/ops/backlog) won't-do close, [B-index-docs-irr-ecf-restate](/ops/backlog) 신규(docs IRR 다수 pre-E[CF]). 11 seed validate + 23 test + 음성 테스트 Pass. | | 2026-05-29 (eve5) | index | `3b2f543` | **투자성 intake 게이트 + option/passed status + Σp·IRR 폐기** ([D-index-29](/ops/decisions)/[30](/ops/decisions)/[31](/ops/decisions)/[31b](/ops/decisions)/[32](/ops/decisions)). 사용자 thread: "Series A Option 표현? / pass 도 IRR 관리하되 fund·project 제외 / 최소 intake (entry·valuation EV vs EqV·라운드·규모·Series) / 신주 구주 혼합 / p IRR 폐기 + 남은 것 모두". **D-index-29** `status='option'` (pro-rata 권리 미행사) — committed_total·baseline·fund·per-position IRR 전면 제외, IRR=null (Sentry 후속 5억 option 재분류, commit `9523638`). **D-index-30** `status='passed'` (검토 미투자) — what-if IRR 보존하되 fund/project baseline 제외 + all-passed fallback (`incl()` closure 공통화); 5/29 배치 정정 동반 (Canopy BW = Canopy 내 passed position 4.2% warrant, per-position BW IRR 28.0% < RCPS 31.8% 정합 → 별도 deal CanopyBW 삭제; Sentry 1호·Iippo 1·2호·Starnex 20억/8월말·Hancom 5억/7월말·Sendy 10억 Pre-C·Novachips 6/10; proceeds 비례 재척도로 IRR 보존 Starnex 38.1%/Hancom 23.9%/Sendy 11.7%, commit `f650bec`). **D-index-31** 투자성 분석 최소 intake 게이트 5 필수 필드(series·round_size_krw·committed_krw·entry_date·pre_money_krw) + EV/EqV 영구 차단(entry=항상 EqV·exit=proceeds_basis) + `_TEMPLATE.yaml` (intake_enforced 2-tier: 신규 hard·기존 warn); 9건 backfill (commit `c4c12f4`+`9e86a07`). **D-index-31b** `share_type` 신주(primary→post 반영)/구주(secondary→post 불변) + 혼합 `round_primary_krw` + post≈pre+신주 ±2% invariant (Infinity pre 499.5→400억 정정으로 검증; Hancom·Novachips secondary, commit `98b88c2`). **D-index-32** Σp·IRR(rate 평균, 오집계) 폐기 → IRR(E[CF]) 단일 canonical: query_irr/html/xlsx 에서 `weighted_irr_*` 제거 + `irr.rs` 함수 `#[deprecated]` + 9 baseline·validate output_code 전환; E[MoM]은 linear 집계라 유지 (23 unit test + validate 3 PASS, commit `3b2f543`). 부수: superman_fresh entry_date 보강 → 실 seed `entry_basis=assumed` 0건, [B-index-entry-date-backfill](/ops/backlog) + [B-index-deprecate-sump-irr](/ops/backlog) ✅. | | 2026-05-29 (eve4) | index | `85c8481` | **IRR 방법론 stack — 기대현금흐름 IRR canonical + per-fund 수익성 + fat-tail scrub + 미집행 timing** ([D-index-25](/ops/decisions)/[26](/ops/decisions)/[27](/ops/decisions)/[28](/ops/decisions)). 사용자 thread: "확률 IRR 폐기 보류 + 추가투자 별도 + 1·2·3호 펀드 수익성 + Iippo 2호 분석 + cashflow IRR 정확한 표현 + 2호 IRR 동일 의문". **D-index-25** 기대현금흐름 IRR = IRR(E[CF]) (확률을 CF 에 먼저 적용 → IRR) 를 canonical 로, Σp·IRR(rate 평균, 오집계)은 병기(폐기 보류) — 반드시 E[MoM]+P(loss MoM<1) 동반 (`irr.rs::expected_cashflow_irr`, commit `2d2066f`). **D-index-27** per-position(`position_ecf_irr`, ratio-split + deal_paid_offset) + per-fund(`fund_performance`, axe_ia_001/002/003 횡단 pool) IRR; Iippo 2호 = 가공 follow-on 제거 → 실제 **AXE Private Fund II 개인투자조합** Pre-A 공동투자 1억 RCPS, 별도 IC Memo (commit `7e313b5`). **D-index-26** fat-tail leg comp-scrub 를 **canonical 에 직접 적용**(이전 overlay-only 라 74% 환상 잔존): Iippo mgmt 1~2.5조→1,000~2,500억(강남언니 8x ceiling) E[CF] 74%→43.3%, Sentry upside 47x→14.7x(마스턴 AUM), Canopy 12x→6x EBITDA(Bird) — IC 원본 주석 보존 (commit `8c5e60e`). **D-index-28** 2호 IRR 이 1호와 동일했던 건 paid_date=null→offset 0 silent fallback (계산식은 정확; 격리 테스트 +1개월 44.24%/+4개월 47.40%, exit=calendar 고정이라 늦은 entry=짧은 hold=높은 IRR); 수정 = 2호 entry 2026-06-15 가안(planned)·paid null 유지 + `entry_basis {paid/planned/assumed}` never-silent flag + `fund_ecf_irr` 의 `.max(0.0)` 클램프 버그(deal_ref<fund_epoch 음수 offset 정상) 제거 → 단일 position 펀드 IRR 가 standalone 과 일치 (commit `85c8481`). 부수: flag 가 Hancom·Novachips·Sendy·Starnex·Superman entry date 누락(`assumed`) 노출 → [B-index-entry-date-backfill](/ops/backlog) 진행. validate-seeds 9-deal Pass, DB 9 deal 정합. | | 2026-05-29 (PM12) | cortex | (interactions 참석자 표시 + 대시보드 폴리시) | **interactions 참석자 표시 + 대시보드 포맷 폴리시** (운영자 피드백 연속). **(1) 참석자**: /cortex/interactions 에 참석자 정보가 없다는 지적 — 데이터는 `attended` relationship 으로 이미 연결돼 있었으나 화면 미표시(display gap). list 2번째 컬럼을 "참석자 / 일시", detail 에 "참석자 (N)" 섹션 추가 (person 링크). **방향 불일치 흡수**: log_interaction 은 from=interaction→to=person, xlsx_hpe 백필은 from=person→to=interaction 으로 만들어 양방향 모두 resolve (`fetch_attended_participants`, `$iid IN (from,to)` 후 반대쪽 endpoint=person). 36 interaction 전부 참석자 연결 확인. 데이터 정규화 migration 은 미실시 (display 흡수, [known-gaps](/ops/known-gaps) 기록). **(2) 대시보드**: stat 카드 값에 단위(명/건) + 천단위 콤마(fmt_n); "전체 인원"→"전체" + total_orgs("N개 회사") sub 추가. **(3) 404**: 운영자가 본 /cortex/interactions 404 는 직전 blue/green --force-recreate 재시작 윈도우(수초)의 일시적 현상 — 라우트 정상, 재현 안 됨. blue/green --no-cache rebuild (binary 4465506b). | | 2026-05-29 (PM11) | cortex | (D-cortex-customer + dashboard 사람중심 재편) | **고객 태깅(다대다) + home 대시보드 사람중심 2-row 재편** ([D-cortex-customer](/ops/decisions), [D-cortex-dashboard-redesign](/ops/decisions)). **(1) 고객**: 운영자 "1명이 여러 건(프로젝트 a/b/c)으로 고객 가능, 우선 수동 태깅". 고객 = subject 에 붙은 ≥1 customer_case attribute (다대다). tag_customer(subject_id, case, note?) — (subject,case) 멱등, person 은 network 자동 승급; untag_customer — 해당 case archive (tier 강등 X). MCP 23→25 tools. **(2) 대시보드 재편**: 운영자 "relationship 은 사람에게 안 중요". kind 카드 그리드 제거 → 사람중심 2-row: 구글연동 → Push대기 → 전사 row → 안심주석 → 나의 row. 각 row = 전체 인원 · 네트워크 N명/N개 회사 · 고객 N명/N개 회사 · 1개월 활동 N건/총 N건. NetworkStats 한 구조+쿼리를 per-user(RLS)·전사(superuser) 재사용. 안심 주석 "네트워크 아닌 인원은 조직 내 비공유". 검증 나의 row: 총 3600/네트워크 624/회사 413/고객 0/활동 1·1. blue/green --no-cache rebuild. | | 2026-05-29 (PM10) | cortex | (D-cortex-org-stats + import-xlsx=network 정정) | **전사(cross-user) 집계 대시보드 + import-xlsx tier=network 정정** ([D-cortex-org-stats](/ops/decisions), [D-cortex-person-tier](/ops/decisions)). **(1) 전사 집계**: 운영자 "대시보드에 조직 전체 통계도". 범위 = 전사 cross-user + 전 직원 visible + 합계만(직원 익명). `fetch_org_wide_counts` 가 set_current_user 미호출 → cortex superuser 연결(bypassrls)로 모든 owner artifact 를 kind별 COUNT + distinct owner 수만 집계 (개별 레코드·owner 분해 X — D-cortex-2 격리 유지, 익명 숫자만 공유). home "조직 전체" 섹션 + 익명 callout + non-link org_metric. 집계 person 3600/relationship 3071/org 1500/deal 17/interaction 1/users 1. **(2) import-xlsx=network 정정**: 운영자 지적 — Network_CRM.xlsx 는 큐레이션 인맥이라 import 가 만지는 person 은 pool 아니라 network. create_person_from_xlsx_row payload tier='network', matched(Google pool) person 도 system:xlsx-migration 이 promote (멱등). ImportStats.tier_promoted. **(3) 등록자 추적 확인**: created_by(artifact) + actor(artifact_event append-only) 가 shared 포함 누가 등록·수정했는지 전부 기록 (현재 per-user 라 actor 는 `본인 또는 system:*` 뿐; cross-employee attribution 은 D-cortex-9 Phase 2). blue/green --no-cache rebuild. | | 2026-05-29 (PM9) | cortex | (D-cortex-person-tier — pool/network 운영 자동화) | **person tier (pool\|network) 도입 — 자동 sync 연락처 vs 운영자 인맥 분리 + auto-promote** ([D-cortex-person-tier](/ops/decisions)). 운영자 관찰: Google Contacts 3,601 중 대부분이 raw 연락처라 "내 인맥" 뷰 오염. backfill = network 625 / pool 2,976. **기본값**: Google sync 신규 person = pool (sync.rs INSERT; 기존 tier 는 enrichment 키라 overlay 가 안 건드림), register_person = network (운영자 명시 등록 = 인맥; tier='pool' 명시 허용). **auto-promote (pool→network, 멱등)**: classify_person / register_relationship(person endpoint) / log_interaction(participant) 어느 액션이든 대상 person 자동 승급 (tier_source='auto_promoted_from_<action>', audit actor='system:auto-promote'). 한 helper 가 3 handler 의 tx 안에서 호출. **network→pool 강등 없음** — 유일 역방향 = 명시 demote_to_pool(person_id, reason) MCP tool. **노출**: list_persons + web /cortex/people 기본 tier='network', ?tier=pool\|all 확장; home People 카드 = 내 인맥 값 + 풀/전체 sublabel. **D-cortex-3 정합**: tier=enrichment 키 → sync 보존. 회귀: overlay_never_touches_tier_enrichment_key (sync.rs 6 tests). MCP 22→23 tools. blue/green --no-cache rebuild (PM8 leader election + Dockerfile cargo test gate 동반). | | 2026-05-29 (PM8) | cortex | (D-cortex-leader-election + Dockerfile ship hook) | **D-cortex-3 PM7 의 본질 해결 — leader election (pg_try_advisory_xact_lock) 도입 + Dockerfile 안 cargo test 자동 회귀 차단** ([D-cortex-leader-election](/ops/decisions)). PM7 의 fix 는 "둘 다 --no-cache 재빌드 + binary sha 일치 검증" 운영 invariant 였는데 본질 = blue/green 둘 다 run_loop 를 돈다는 것 자체. binary 동일성은 race 가 일어나도 같은 결정을 내리는 우회 — race 자체 차단이 본질. 5축 평가 (본질 5 · 결과 5 · 혁신 5 · 퀄리티 4 · 안전 4 = 23) 로 가장 강한 옵션 선택. **구현**: `src/google/sync.rs::run_cycle` 시작 시 transaction 열고 `pg_try_advisory_xact_lock(2026052907)` 시도. 성공 시 leader → 본문 실행 → commit (lock auto-release). 실패 시 INFO 로그 후 skip. **Transaction-scoped lock** = COMMIT/ROLLBACK/disconnect 시 auto-release → leader panic·crash 해도 무인 페일오버. **부수효과**: Google API quota 절반 (2× → 1×), DB 부하 절반. **Ship hook (직교 추가, 점수 18)**: Dockerfile RUN 안 `cargo build --release && cargo test --release --bin cortex google::sync::tests` 한 줄 — test 실패 시 image 생성 자체 불가 → 옛 destructive 코드가 production 으로 흘러갈 수 없음. **검증**: (1) psql 두 세션 동시 `pg_try_advisory_xact_lock(2026052907)` → 첫 세션 t, 두번째 f. 첫 세션 commit 후 두번째 t (auto-release). (2) Docker --no-cache 양쪽 rebuild + recreate → 같은 binary sha `0658a1c7...`. cargo test 5/5 통과 (자동 회귀 차단). (3) 첫 sync cycle 후 blue/green 중 한 쪽만 "owner sync ok" + 다른 쪽 "leader lock held, skipping" 관찰 예정. **CLAUDE.md 운영 함정 갱신**: blue/green 동일 binary 가 강제→권장 으로 격하 (leader election 이 본질 차단). 새 함정 = skip 로그가 한 쪽에서 항상 안 보이면 그 쪽이 leader 가 못 됨. | | 2026-05-29 (PM7) | cortex | (D-cortex-3 PM7 — blue/green 동일 binary invariant + 회귀 테스트) | **D-cortex-3 enrichment wipe 재발 → blue/green Docker layer cache race 발견 + fix + 회귀 테스트 5개 추가** ([D-cortex-3 갱신](/ops/decisions)). PM4·PM6 의 overlay-preserving 코드가 이미 ship 됐는데 동일 증상 재발 — 김승우 (코오롱·한섬·스위티) 3명의 memo/cohort/former_org/former_title 이 또 system:google-sync 의 edit event 로 wipe. **진짜 root cause**: PM6 의 `docker compose build --no-cache` 가 `cortex-mcp-blue` 한 서비스에만 적용되고 `cortex-mcp-green` 은 stale 캐시 이미지 그대로 유지. blue/green 둘 다 run_loop 를 동시에 돌기 때문 (leader election 부재) sync 한 번 돌 때마다 race — 새 코드가 enrichment 보존해도 옛 코드가 같은 초에 destructive UPDATE 로 덮음. 운영자가 본 .236 preserve 와 .337 wipe 가 같은 초에 짝지어 일어난 흔적 = 두 컨테이너의 race 결과. **즉시 안전**: `UPDATE google_oauth_token SET status='paused'` → sync 정지. **fix**: 두 서비스 모두 명시적 `docker compose build --no-cache cortex-mcp-{blue,green}` + `up -d --force-recreate cortex-mcp-{blue,green}` → 안의 `/usr/local/bin/cortex` sha256 일치 검증 (50faa90d...). **복구**: `cortex restore-from-audit --owner soohun.kang@axellc.com --scan --apply` → 183 person enrichment 복원. sync 재개 (status='active') 후 강제 sync_google_now 3,385 → 3 victim 그대로 + missing_memo/cohort/former_org/former_title/HPE 전 owner 합쳐 0. **회귀 차단**: `src/google/sync.rs` 에 `#[cfg(test)] mod tests` 5개 unit test — (1) overlay 가 enrichment 보존 + (2) Google canonical 전부 null 일 때도 enrichment 보존 (edge case) + (3) wholesale replace 차단 + (4) 멱등 + (5) is_meaningful 가드. `cargo test --bin cortex google::sync::tests` 5/5 통과. **운영 invariant 명문화** (CLAUDE.md 운영 함정 섹션): sync.rs 같은 critical path 수정 시 blue·green 둘 다 명시적 `--no-cache` + `docker exec sha256sum /usr/local/bin/cortex` 일치 검증 필수. image-level hash 는 빌드 timestamp 비결정성으로 달라도 OK, binary 자체만 같으면 안전. | | 2026-05-29 (PM6) | cortex | (Google contact displayName backfill 134/134) | **`cortex backfill-google-names` CLI 일회성 실행 — 134/134 success** ([D-cortex-google-names-backfill](/ops/decisions) + [D-cortex-3 갱신](/ops/decisions)). 옛 register_person 으로 push 된 Google contact 134건의 빈 displayName 정정 (unstructured_name fix 이전 createContact 호출분). per-person flow 는 People API updateContact 호출 + citation.ref 갱신 + dispatch event. rate 1 req/sec, idempotency gate (citation.ref 의 unstructured_name_backfill 마킹 v1). dry-run candidate=134 (Bucket A=2 김승우/정성화 + Bucket B=132). canary 5/5 → full 129/129 (총 134, 약 2.5분). 사후 candidate=0, dispatch event 134, 김승우/정성화 citation 마킹 v1. **선행** restore-from-audit 가 Cortex 측 display_name 을 채워둠. **함정 발견** — Docker layer cache 로 코드 fix 후에도 old binary 가 동작 (docker compose build 만 했을 때 캐시 재사용). docker compose build --no-cache 강제 빌드 + 새 binary 동작 직접 검증 (memo/former_org 보존) 후 134 backfill 실행. **D-cortex-3 본문 갱신**: 단방향 createContact 원칙 안에서 식별 키 정정 한정의 updateContact 도 허용 (enrichment 키는 절대 push 안 함). | | 2026-05-29 (PM5) | cortex | (MCP tools 17 → 22 + register_deal 재설계) | **Cortex MCP write tools 확장 — 17 → 22 tools, register_deal 재설계 + 5 read tools** ([D-cortex-mcp-write-tools](/ops/decisions)). 운영자 follow-up — claude.ai 안에서 일상 운영 (deal stage 전환, org 조회, person ↔ deal 연결 등) 가능하게. **재설계**: `register_deal` = `title` → `name` 표기 통일 (16/19 기존 deal 이 이미 'name' 쓰는 backfill 패턴 + 3개 'title' archived 됨), stage 자유 string (권장 enum origination/screening/dd/ic/active/closed/killed description 만 — 실제 분포 active/closed/exploring/review 호환), **visibility default 'private'** (deals 민감), `contact_person_ids[]` → 각각 `deal_contact` relationship 자동 (idempotent, deal visibility 상속), `target_org_id` 있으면 `targets` relationship 자동, `source_attribute_id` (HPE → deal 추출 provenance). `register_organization` = case-insensitive idempotency (`lower(payload->>'name')` 매칭 — 'Aruda'/'ARUDA'/'aruda' 모두 동일 id) + `aliases` 명시 필드. `archive_artifact` = `reason` 필드 (payload.archived_reason) + `restore=true` alias (archive=false 와 동등). **신규 5 read tools**: `list_organizations` (name+aliases ILIKE search), `get_organization`, `list_deals` (stage/target_org_id 필터), `get_deal` (recent 20 events 포함), `list_relationships` (person_id 가 from 또는 to + edge_kind 필터). 모두 archived 자동 제외 (get_* 단건만 archived 도 노출). **검증** (7/7 smoke): tools/list=22 / ARUDA case-insensitive 3 variant 동일 id / register_deal default private / contact_person_ids+target_org_id → 자동 2 relationship (deal_contact + targets) / archive_artifact reason+restore alias / register_relationship 멱등 / list_* read OK / archived org list 에서 안 보이고 get 으로는 보임. blue/green Docker recreate. | | 2026-05-29 (PM4b) | cortex | (D-cortex-3 enrichment 보존 fix + 데이터 복구) | **D-cortex-3 sync overwrite 버그 발견 + 즉시 패치 + 데이터 복구** ([D-cortex-3](/ops/decisions)). 운영자 ("방금 2명 신규 저장했는데 잘 적재된건가") 가 확인 요청 → audit log 점검 결과 김승우 (코오롱인더스트리 전무) + 정성화 (한섬비즈온 대표) 두 person 의 display_name + memo + cohort + former_org/title 이 **모두 sync 백그라운드에 의해 destroy** 됨. **Root cause**: `src/google/sync.rs::upsert_from_person` 의 `UPDATE artifact SET payload = $1` (Google payload 전체 덮기). register_person → Google createContact → 4분 후 sync 실행 → Google 의 빈 응답 (createContact 가 displayName 못 저장한 buggy 상태) 으로 Cortex enrichment 까지 통째로 날아감. **즉시 패치 3종**: (1) `sync.rs` 에 `GOOGLE_CANONICAL_KEYS` 화이트리스트 (display_name·given/family_name·org·title·department·emails·phones·memberships·biographies·userDefined) + `overlay_google_into_cortex(existing, google)` 가 meaningful (non-null + non-empty) 값만 selective overlay → enrichment 키는 자동 보존. (2) `google/people.rs::NameCreate` 에 `unstructured_name` 필드 추가 — People API 의 `Name.displayName` 은 server-computed 이므로 클라이언트가 보내도 무시, `unstructuredName` 으로 보내야 Google 측 displayName 정상 생성. (3) `mcp.rs::try_google_create_contact` + `web/views.rs::push_one` 의 NameCreate 인스턴스화에 unstructured_name=display_name 추가. **데이터 복구**: append-only audit log 보존 덕분에 두 person 의 propose (operator) + operator edit 의 enrichment 합성 → SQL UPDATE artifact + INSERT artifact_event op='restore' (actor='system:enrichment-recovery-D-cortex-3'). 정성화: display_name·org·title·memo·cohort 복원. 김승우: + former_org/title 복원. **검증**: 강제 sync_google_now (3,385 contacts) 후 두 person enrichment 그대로 + sync 가 만든 새 edit 의 payload_after 가 `memberships=["myContacts"]` 만 추가 (Google meaningful 값) — display_name·memo·cohort 보존 확인. blue+green Docker recreate. **D-cortex-3 본문 갱신** — Sync 보존 계약 명문화. **회피 가능했던 운영 함정** (참고): 이번 버그는 register_person → createContact name bug + sync 의 destructive overwrite 두 결함이 결합해서 만들어짐. createContact 만 고쳐도 destructive overwrite 는 잠재 위협으로 남았을 것. overlay 화이트리스트 도입이 더 본질적인 안전망. | | 2026-05-29 (PM4) | docs | (ssh-access trap fix) | **ssh-access runbook 함정 fix — `install` 은 replace, append 는 `tee -a`** ([/onboard/ssh-access](/onboard/ssh-access)). 운영자 ai 가 taehun.kang SSH key 등록 (직원 onboarding) 중 발견 — Step 3 (a) `sudo /usr/bin/install -m 600 -o axe -g staff /Users/axe/.ssh/authorized_keys` 주석은 "append" 인데 명령은 destination replace (cp 와 동일). 운영자가 무심코 따라하면 공용 `axe` 계정의 기존 직원 키 (이번 경우 ring-backup + soohun.kang) 전부 삭제 → 다른 직원 SSH 잠김. **fix**: (a) 를 `echo "" \| sudo /usr/bin/tee -a /Users/axe/.ssh/authorized_keys >/dev/null` 로 교체 (NOPASSWD whitelist `/usr/bin/tee /Users/*` 활용 확인). ⚠️ 코멘트 + (c) personal home 도 "첫 등록 시만 install OK, 두 번째부터 tee -a" 명시. 본 건 (taehun.kang) 은 Edit in-place append 로 진행 → 기존 2 키 보존. SACL `com.apple.access_ssh` 는 이미 등록되어 있어서 dseditgroup 불요. | | 2026-05-29 (PM3) | cortex | (MCP write tools + archive) | **Cortex MCP write tool 표면 완성 — 16 tools** ([D-cortex-mcp-write-tools](/ops/decisions)). 운영자 "본질 퀄리티 결과 안전 혁신" 으로 진행 위임. 5축 평가 후 본질 gap (claude.ai 가 organization/deal/relationship 등록할 채널 없음) + lifecycle gap (DELETE 가 append-only audit trigger 로 차단) 동시 해결. **신규 4 tool** = `register_organization` ((owner, name) idempotent, visibility default shared) · `register_deal` (NOT idempotent — 같은 title 의 round 가능, target_org_id 가 있으면 owner-scope + kind='organization' 검증) · `register_relationship` ((owner, edge_kind, from, to) idempotent, from/to 양쪽 owner-scope 존재 검증, self-loop 차단) · `archive_artifact` (toggle archive=true|false → payload.archived 갱신 + op='archive'/'restore' event). **혁신** — 별도 archive_person/archive_deal 안 만들고 한 일반 도구 + restore. DELETE 차단 우회의 핵심: payload.archived flag 로 lifecycle 표현하면 audit 보존 + list/count/push 자동 제외. **List 쿼리 일괄 갱신** — web(fetch_kind_counts/recent_events/artifacts_by_kind/pending_push) + MCP(list_persons/search_person via push_person_filters) + backfill(distinct_orgs/person_rows) 모두 `NOT COALESCE((payload->>'archived')::boolean, false)` default 필터. ID 직접 접근 (get_person 등) 은 archived 도 반환. **검증** (11/11): register_organization 신규 + idempotent / register_relationship 신규 + idempotent / register_deal 신규 + 동일 title 새 id / target_org_id=person uuid → kind mismatch error / archive 4 artifact / 재 archive no_op / restore / 이전 세션 SMOKE_TEST_ORG_DELETE_ME_BACKFILL MCP archive 로 정리. UI: /cortex/orgs 가 archived 항목 안 보임, home dashboard 카운트 archived 제외 정합. blue/green Docker recreate 후 edge https://axe.axelabs.ai/cortex/home 200. **남은 우선순위** (다음 세션): backfill-introductions CLI (xlsx '소개' attribute → introducer relationship) · HPE attribute → deal 자동 추출 · 운영자 #1 full backfill-orgs (1,536 org YAML 편집, 수동) · edit-before-push / partial-failure (Google push 마무리 UX). | | 2026-05-29 (eve3) | index | `c0dcd9a` | **Prj_Infinity(INEX) 축적 — 3-step surface→재수행→비교 (13.8% → −8.3% sign-flip)** ([D-index-23](/ops/decisions)). 사용자 "기존 적재 테스트 → 신규 IC 재수행 → 두개 비교". [D-index-19](/ops/decisions)/[21](/ops/decisions) 을 명시적 3-step 으로 정식화. **대상**: INEX 가상자산 거래소/VASP (보통주 0.5억 bridge, Closed). **Step1 surface**: v8 FINAL(irr_locked) 적재 → index 13.8%/MoM 1.70x 정확 재현. **Step2 재수행**: 외부 research — 실명계좌 base-rate near-zero(2021 특금법 후 원화마켓 신규 0건, P<10%) / 코빗 ceiling 1,000-1,400억(Mirae MOU) / Gopax·Binance license-shell M&A 선례 / 보통주 distress recovery 0~20%. → 모달 exit 를 won-market rerating 아닌 SI shell M&A breakeven 으로 reframe + 보통주 wipeout leg(18% @ 0.12x) 추가. **Step3 비교**: calibrated −8.3%/MoM 0.82x. **~22pp gap + sign-flip**. **핵심**: 성숙·정직한 v8(hurdle 미달 공개·3 reviewer·irr_locked)도 핵심 base-rate(실명계좌)를 mgmt optimism 에 anchor + 보통주 wipeout 누락 → 결론 역전. proceeds 메커니즘은 v8 이 이미 정확(ev_bridge·net debt 0·exit-diluted) → 재계산 0, gap 순수 analytical. **canonical = IC-approved 13.8%** (executed/Closed deal → decision-time 기록 보존; −8.3% 는 retrospective overlay = `infinity_recalibrated.yaml`, [D-index-24](/ops/decisions)). 8-deal canonical 랭킹: Hancom 23.2 > **Infinity 13.8** > Novachips 7.8 > Sendy 6.5. validate-model 0 errors. IC memo v9 = OneDrive. | | 2026-05-29 (PM2) | cortex | (axe-ui + backfill-orgs) | **Cortex 웹 UI 가 docs.axelabs.ai 와 같은 토큰을 입음 + /cortex/orgs 가 비어있던 문제 풀림** ([D-cortex-design-axe-ui](/ops/decisions) + [D-cortex-backfill-orgs](/ops/decisions)). **Design** — D-cortex-6.1 의 inline CSS (~200 line) 폐기 → `scripts/sync-axe-ui.sh` 가 axelabs SSOT (`/Users/axe/axelabs/src/lib/`) 에서 tokens 4종 + styles 6종 (reset/components base/form/data-display/dashboard/feedback) + Sarasa Fixed K woff2 (533 KB) 를 `static/axe-ui/bundle.css` (83 KB) 로 묶음. components.css 의 `@import` 중 cortex 가 안 쓰는 9 그룹은 strip. fonts.css 의 `/fonts/` 절대경로는 `/cortex/static/fonts/` 로 rewrite. `include_str!`/`include_bytes!` 로 binary stamp + Dockerfile 에 `COPY static ./static` 추가. 라우트 = `GET /cortex/static/axe-ui.css` + `/fonts/SarasaFixedK-Regular.woff2` (public, Cache-Control 1일/30일). HTML 은 `` + ``. 클래스 매핑 = `.topbar`→`.axe-topnav`, `.brand`→`.axe-logo`, `.card`→`.axe-card`, `.btn`→`.axe-btn--{primary,secondary,ghost}`, `.metric`→`.axe-metric-card`, ``→`.axe-data-table > __scroll > __table > __th/__td`, `.empty`→`.axe-empty-state`, callout box→`.axe-callout--{info,danger}`. cortex-only 개념 (visibility tag, payload pre, kv, flash, filter bar) 은 `.cortex-` prefix 로 분리 inline. **Backfill** — `cortex backfill-orgs` CLI 2단계: Phase 1 (`--owner `) → pg_trgm 활성화 + distinct payload.org 추출 (1,536개) + similarity ≥ 0.6 클러스터 (1,446개) 제안 YAML stdout. 운영자가 파일 저장 → canonical_name/aliases/skip 편집. Phase 2 (`--mapping `) dry-run, Phase 3 (`--apply`) 실제 INSERT. Idempotent — (owner, name) 으로 org 중복, (from, to, edge_kind) 로 relationship 중복 안 만듦. artifact `visibility='shared'`, `payload.backfill_source='backfill_orgs_v1'`. D-cortex-6.1 read-only 원칙 유지 — CLI 도구만, 웹 form X. **검증**: axe-ui.css 200 + 엣지 https://axe.axelabs.ai/cortex/static/axe-ui.css 200 · /cortex/home + /cortex/google/pending 모두 axe-* 클래스 + Sarasa 폰트로 렌더 · backfill Phase 1 YAML 1,536 distinct → 1,446 clusters 정상 출력 · 소규모 2-org mapping 으로 dry-run + apply (orgs created=2, rels created=2) + 재apply (existing=2, idempotent) 모두 통과 · /cortex/orgs 가 빈 axe-empty-state 에서 데이터 있는 axe-data-table 로 전환됨 확인 · /cortex/relationships 도 employed_by 2개 표시. **잔여**: 1,536개 org 의 full backfill 은 운영자가 YAML 편집 후 실행 (이번엔 smoke 만), 메모리에 도큐먼트. **남은 우선순위**: #2 backfill-introductions (xlsx '소개' 컬럼 → introducer relationship) · #3 register_organization/_deal/_relationship MCP tool · #4 HPE → deal 자동 추출. | | 2026-05-29 (eve2) | index | `ca714b7` | **proceeds 산정 강화 — EV→EqV(net debt) bridge + exit-dilution 강제** ([D-index-22](/ops/decisions)). 사용자 audit "모두 EV = EqV + Net Debt + 투자 후 dilution 고려됐나 체크". **발견**: 7-deal proceeds 비일관 — net-debt bridge 는 Canopy 만 명시(EV 797−netdebt 300=EqV 498), Sendy(v2)·Iippo 는 stake×EV 직접(EqV 누락), pre-IPO 3+Sentry 는 equity multiple(N/A); dilution 은 pre-IPO 3+Sendy exit-date 희석 명시 vs Iippo·Sentry entry F/D flat. **결정** ([D-index-20](/ops/decisions) IRR determinism 을 proceeds 로 확장): `proceeds_basis` enum (ev_bridge|equity_value|mom|legacy_ev) + ev_bridge non-writedown leaf 는 `stake×(EV−net debt)` 가 axe_proceeds 와 ±2% 일치해야 — seed load **hard reject** + validate Check 7 Error 2중 강제. negative test 거부 확인 (10%×2,225억=222.5억 ≠ 날조 300억). migration 0005 (proceeds_basis + exit_ev/net_debt/stake 컬럼). **Sendy 정정 v3**: ev_bridge, net debt=0 (asset-light + 자금니즈 equity 충족 + 차입 증거 0 → 보수적, **없는 부채 날조 안 함**), stake 12.5%→10%. **proceeds·IRR 불변 6.47%** 이나 EV≠EqV 누락 class 영구 차단 — "정정=구조적, 숫자 불변" 이 정직한 결과. **7-deal**: Sendy ev_bridge PASS / Sentry equity_value / 3 pre-IPO mom / Iippo·Canopy legacy_ev WARN — 0 errors. 잔여: [B-index-proceeds-bridge-retrofit](/ops/backlog) (Iippo·Canopy per-leaf bridge) + Iippo·Sentry entry-F/D-flat dilution ([known-gaps](/ops/known-gaps)). | | 2026-05-29 (eve) | index | `b77a236` | **센디(Sendy) IC v2 재작성 — 현 practice 재calibration (92% → 6.5%)** ([D-index-21](/ops/decisions)). 사용자 redirect "그냥 적재 말고 ic memo 를 현 practice 수준으로 새로 써라 (당시 방법론 열위)". [D-index-19](/ops/decisions) (surface→calibrated) 의 종착 = **memo 방법론 자체 재작성**. **3-tier IRR**: (a) v1 md 31%/22.8% (fabricated override), (b) v1 Excel 92.4% (math-correct, 낙관 input), (c) **v2 현 practice 6.5% ⭐ canonical**. **v1 5결함 교정**: IRR override(D-index-20 차단) / **wipeout leg 부재** (Downside 가 +31.6% gain → 손실 시나리오 자체 없음) → genuine 18% wipeout (부릉 1/6 recovery 0.15x) / **sendyX 66x 미검증** (beta 5곳 월375만→2026E 25억) → base-rate haircut(-93%) / **Exit 5x flat → segment-weighted** (freight 1.0~1.5x + TMS SaaS 4~5x, WiseTech-E2open 3.5x anchor) / instrument 분석 부재. **외부 anchor**: Convoy $3.8B→0 + 부릉 ₩5,500억→800억(1/6) → wipeout 15~25%, 한국 Series C→IPO 예외(TMAP 교훈). **결과**: 4-scenario bull(0.12,4.45x)/base(0.40,1.83x)/bear(0.30,0.72x)/wipeout(0.18,0.15x) → E[IRR] 6.5% / E[MoM] 1.51x / Sharpe-like -0.06. PASS 재확인 (v1 구조+타이밍 + fundamentals 30% hurdle -23.5pp 미달). **7-deal 랭킹**: Sentry 40.3 > Iippo 36.5 > Starnex 36.4 > Canopy 26.4 > Hancom 23.2 > Novachips 7.8 > **Sendy 6.5 (최하위로 정정)** — 92% 매력 대부분이 2 낙관 input + 누락 risk leg 산물이었음. 산출물: OneDrive `ic/memo/v2_axe_current_practice_260529.md` (5-tenet·premortem·v1→v2 교정표) + seed v2 (version:2). validate-model 7/7 PASS. | | 2026-05-29 (PM) | index | `9263179` | **센디(Sendy) 축적 — IRR override 오류 flagship + 첫 declined deal** ([D-index-20](/ops/decisions)). 사용자 "sendy 건도 비슷하게". 출처 = `1_Project/Pipeline/Sendy/`. **index 존재 이유의 결정적 증거**: `exit_matrix/v1.yaml` line 76 `irr_approx_pct: 95 # ... ≈ corrected: 35%` — 계산값 95.7% 를 근거없이 "35%" 로 손수 override (md 전반 Base 35/Downside -8/Expected 31/Devils 22.8 오류 chain; Downside -8% 는 수취 114억 > 투자 50억 → 구조적 불가). index 는 IRR 을 exit proceeds 에서 deterministic 계산 → override 물리적 불가. EV-derived seed → index 자동 교정: Base 95.7% / Downside 31.6% / Upside 146.6% / weighted 92.43% (Excel 실제값 일치). **부수 — deal.stage 'Passed' 추가** (migration 0004): GO/Closed 만이 아닌 PASS 결정 institutional memory 보존. 센디 PASS (2026-02-15) = 구조(10억서 CB 외 equity 제한)+타이밍, quality 아님. **7-deal portfolio**: Closed 3 / IC 3 / Passed 1 (full lifecycle). honesty: 92% 는 math-correct 이나 screening-grade model (sendyX 66x 미검증), PASS 는 IRR 무관 결정. validate-model 7/7 PASS. | | 2026-05-29 (PM) | cortex | (batch push UI) | **Cortex Google push 큐 — batch + skip + visibility 격리** ([D-cortex-google-push-batch](/ops/decisions)). 운영자 "216 명 하나씩 클릭 비현실적" → batch UI 추가. **신규 모듈** `src/web/google_push.rs` (~400 LoC) — `JobRegistry` (`Arc>`) in-memory, owner 동시 1 job 락, 1 req/sec 페이스 (Google quota 90/min/user 안전), MAX_BATCH_SIZE=500, 프로세스 재시작 시 손실 허용 (push idempotent — citation 중복 거절). **3 작업 일괄 ship**: (#3, **D-cortex-9 안전 fix**) `fetch_pending_push` SQL 에 `AND a.visibility='shared'` 추가 — private person 절대 push 후보 안 됨. count 216→215 검증. (#2, skip permanently) `kind='attribute', payload.key='do_not_sync_google'` 별도 artifact 마킹 (classify_person 패턴 일관, payload mutation 회피). `POST /cortex/a/:id/skip-google-push` 라우트 + 행별 `