<!-- canonical: https://docs.axelabs.ai/ops/known-gaps -->
<!-- source: content/ops/known-gaps.mdx -->

---
title: 알려진 gap
description: docs 와 실제 구현 사이 의도된 / 미구현된 격차 목록.
---

# 알려진 gap (정직 페이지)

문서는 "운영자가 1인 멀티테넌트 플랫폼을 잡고 갈 수 있도록 하는 사실 의 SSOT" 가 목표. 그러나 일부 항목은 미구현 / 단순화 / 향후 작업. 본 페이지는 그 격차를 명시.

> **시간축 4-페이지 분리** (2026-05-22, [D-docs-updates-1](/ops/decisions)):
> - [/ops/backlog](/ops/backlog) = **현재** — 실행 큐 (`🆕→📋→🔧→✅→⏸️`). 다른 세션의 entry point
> - [/ops/roadmap](/ops/roadmap) = **미래** — M1~M5 마일스톤 큰 그림
> - [/ops/updates](/ops/updates) = **과거** — Ship Log + Highlights + [/api/changes](/api/changes) JSON feed
> - **본 페이지** = (사실) — "왜 이렇게 됐는지 · 무엇이 함정인지" 의 분석적 사실 기록. entry point 아님
>
> 새 함정·미구현을 발견하면: **실행 가능한 항목** = backlog 🆕 / **사실·맥락 기록** = 본 페이지 / **ship 됨** = updates.

## deploy-SSOT guard = host-local 강제, 서버측 백스톱 없음 (2026-06-05)

[D-ops-42](/ops/decisions) 의 pre-push guard (컴포넌트 E) 는 **이 Mac mini 의 로컬 git 훅**이다. `axe guard install` 로 frame·hive·blueprint·stream·magnet·index + axe-cli(`~/.axe/bin`) 에 설치 — `git push origin main` 직접을 기계적으로 거부하고 `axe ship`(내부적으로 `AXE_SHIP=1` set, `axe` line ~7606) 만 통과. **강제의 경계가 있다**:

| 우회 벡터 | 사실 | 완화 |
|---|---|---|
| 다른 머신/CI 에서 같은 origin 으로 push | 로컬 훅이 없어 차단 안 됨 | 모든 개발 세션이 이 Mac mini 에서 돎 → 실 위협은 로컬 훅이 커버 |
| `git push --no-verify` | git 내재 동작 — 클라이언트에서 pre-push skip, 클라이언트측 차단 불가 | 운영자 단일·악의 아님 전제로 수용 |
| GitHub branch protection (서버측 정답) | **현 plan 불가** — private repo 의 branch protection 은 Pro/Team 필요 (`gh api repos/axelabs-ai/<svc>/branches/main/protection` → 403 "Upgrade to GitHub Pro") | plan upgrade 시 require-PR / restrict-push 로 권위 백스톱 추가 |

**범위**: guard 는 **axe-ship 거버넌스 대상 한정** (위 6 서비스 + CLI). artemis/blurgram/vault/axelabs 등은 자체 배포 경로라 의도적으로 guard 밖 (universal 아님).

**설치 함정 — tracked `core.hooksPath`**: index 는 `core.hooksPath=.githooks` (추적 디렉터리, sqlx `pre-commit` 보유). guard install 이 거기에 `pre-push` 를 쓰면 **untracked 로 노출** → 세션이 `git add -A` 하면 운영자-로컬 강제 훅이 공유 repo 에 커밋되어 clone/CI 의 정상 push 까지 잘못 차단한다. 2026-06-05 `~/index/.git/info/exclude` 로 차단 (비전파·tree 무변·훅 실행엔 무영향). 나머지 6 repo 는 `.git/hooks`(git 미추적)라 무관. CLI 일반화 = [B-axe-guard-hookspath-exclude](/ops/backlog).

## SSH 세션은 GUI login keychain 에 못 쓴다 — vault 비밀 주입의 keychain 함정 (2026-06-09)

이 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 의존 경로 — `axe vault unlock` (keychain-cache crash), `axe secret push/pull/check` (`_vault_env` 가 `security find-generic-password -s axe.vault.session` 으로 keychain 세션만 읽음 → "vault session not found") — 이 전부 SSH 에서 실패한다. **현 워크어라운드** = keychain-free raw-bw (운영자가 자기 셸에서 `NODE_EXTRA_CA_CERTS` + 단독 줄 `bw unlock --raw` + raw `bw create/edit`, [D-ops-44](/ops/decisions) / [/architecture/secrets](/architecture/secrets#ssh-환경에서-vault-비밀-주입-keychain-free-raw-bw)). **더 깊은 격차** = `_vault_env` 가 **keychain 세션만** 읽고 env-session(`BW_SESSION`/stdin) 폴백이 없다는 것 — 그래서 SSH 컨텍스트가 `axe secret *` 의 1급 경로가 못 되고 매번 raw-bw 로 우회해야 한다. 이 함정을 없애는 fix 후보 = `axe secret put --stdin-session` 또는 `_vault_env` 의 env-session 폴백 (있으면 SSH 에서도 `axe secret push` 가 그대로 동작). 비밀 주입과 달리 **배포(`axe ship`/`secret pull`) 는 keychain 세션이 필요** — SSH 에선 `security unlock-keychain` headless unlock 후 `axe vault unlock` 으로 채운다. (전조: 이 keychain partition 격리 자체는 [realchoice D-day 함정 #5](#realchoice-d-day-첫-실행--6-함정-2026-05-25-발견) 에서 customer 측으로 먼저 관측됨 — 본 항목은 운영자 macmini 의 SSH 세션 + 비밀 *주입* 축.)

## OIDC 발행자가 자기 토큰을 401 (issuer ≠ resource server)

2026-06-04. D-axe-idp-1 후속. **Blueprint 가 플랫폼 토큰을 *발행*하면서 자기 MCP 는 그 토큰을 *거부*** 했다. `axe login` 토큰은 frame·hive·index·cortex·matrix 에선 동작하나 `https://axe.axelabs.ai/blueprint/mcp` 에선 **401 (`unknown_kid`)**.

**근본 원인**: trust-migration 작업이 frame·hive·cortex·index·matrix **5개**만 대상으로 잡고 **Blueprint 자체 MCP 를 빠뜨렸다** — "issuer 가 곧 resource server" 가 자명해 보여 목록에서 누락. Blueprint MCP (`blueprint_mcp/auth_oidc.py`) 는 Microsoft Entra access_token 만 검증 (`verify_microsoft_access_token`) → 플랫폼 토큰의 `kid` (= Blueprint OIDC 서명키) 가 Microsoft JWKS 에 없어 `unknown_kid` → 401.

**진단 단서** (같은 토큰을 두 서비스에 던져 비교):
- `blueprint/mcp` → `401 {"code":"UNAUTHORIZED","message":"unknown_kid: <kid>"}` (Microsoft JWKS 에서 못 찾음)
- `frame/mcp` → `401 {"code":"TOKEN_EXPIRED"}` (= **iss 분기·서명검증을 통과**하고 exp 만 걸림 → frame 경로는 정상)
- `unknown_kid` vs `token_expired` 의 차이가 "이 서비스는 Blueprint issuer 분기가 아예 없다" 를 가리킨다.

**Fix**: frame `auth_blueprint.py` + `http_server.py` iss-dispatch 를 Blueprint MCP 에 미러 (`mcp/src/blueprint_mcp/auth_blueprint.py` 신규 + `BLUEPRINT_ISSUER=https://blueprint.axellc.com` compose env). forged-iss/bad-sig → 401 검증 (unverified iss-peek 가 auth 우회 안 함). 설계: [/architecture/platform-identity](/architecture/platform-identity).

**교훈**: **OP 를 세울 때 그 OP 자신의 MCP 도 resource-server 목록의 한 행이다.** 발행자라는 사실이 자동 신뢰를 주지 않는다 — 토큰 검증은 issuer 든 아니든 동일하게 명시 배선해야 한다.

**잔여 (별개)**: `axe-cli` 의 blueprint **local** 엔드포인트 경로가 틀림 — `:3151/mcp` → 404 (proxy 는 `/blueprint/mcp` 서빙). public 은 정상. backlog `B-axe-cli-blueprint-local-path`.

## Cloudflare Universal SSL 1-level

2026-05-26, D-ops-39. 무료 plan 의 `*.axelabs.ai` Universal SSL cert 가 **1단 서브도메인만 cover** 하는 함정. zone 안에 2단 hostname (e.g. `ssh.axe.axelabs.ai`) 을 만들면 edge 가 SAN 미일치 → default cert fallback → 클라이언트 TLS handshake 실패.

| 환경 | 증상 |
|---|---|
| Windows (Schannel) | `SEC_E_ILLEGAL_MESSAGE (0x80090326)` — `cloudflared access login` 시 `failed to get app info: remote error: tls: handshake failure` |
| macOS (LibreSSL/curl) | `sslv3 alert handshake failure` |
| 브라우저 (Chrome/Edge) | `ERR_SSL_VERSION_OR_CIPHER_MISMATCH` |

진단 분기점: **`cloudflared --version` 최신 + 시스템 시간 OK + 브라우저/curl/cloudflared 3개 클라이언트 모두 동일 실패** = 서버측 cert 단정 (90%+ confidence). 동일 zone 안에서 1단 host (e.g. `axe.axelabs.ai`) 는 정상 동작하는 것도 단서.

**컨벤션 (D-ops-39)**: HTTPS 노출되는 모든 hostname = `{name}.axelabs.ai` 의 1단. flat-hostname (예: `ssh-axe`) 또는 path under apex (`axe.axelabs.ai/frame`). 룰 본문 [/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39](/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39).

**구체 사례 — `ssh.axe.axelabs.ai` (강태훈 Windows, 2026-05-25)**:
- 강태훈이 `cloudflared access login https://ssh.axe.axelabs.ai` 시도 → 위 Windows 증상.
- axelabs.ai chat 의 MAX agent 가 클라이언트 4단계 (cloudflared version / 시스템 시간 / TLS inspection / hostname 정확성) 진단 후 서버측 단정.
- 2026-05-26 운영자가 `ssh-axe.axelabs.ai` 신규 등록 (DNS CNAME → 동일 tunnel `d8efecdd`, ingress 규칙 clone, Access app `b903d8cd` self_hosted_domains 추가). 검증: `curl -v https://ssh-axe.axelabs.ai/` → 302 to `axellc.cloudflareaccess.com`.

**옛 hostname 완전 삭제 (2026-05-26 같은 날 후속)**: 강태훈이 신규 hostname 으로 1차 재시도했으나 Microsoft SSO 통과 후 callback 이 옛 `ssh.axe.axelabs.ai` 로 떨어져 동일 cert 오류 재발. 추가 진단:
- Access app 의 primary `domain` 필드가 여전히 `ssh.axe.axelabs.ai` 였음 → post-auth callback / App Launcher 흐름이 primary 를 우선시. 1차 fix: `domain` 을 `ssh-axe.axelabs.ai` 로 PUT swap.
- 2차 fix (확정): 옛 hostname 흔적 3 layer 모두 제거 — (a) DNS CNAME `ssh.axe.axelabs.ai` + `*.axe.axelabs.ai` 와일드카드 삭제, (b) axelabs tunnel (`d8efecdd`) ingress 규칙 제거, (c) Access app `self_hosted_domains` / `destinations` 에서 제거. 검증: `dig ssh.axe.axelabs.ai` → NXDOMAIN. 클라이언트 측 cache (브라우저 쿠키 + `%USERPROFILE%\.cloudflared\` token) 정리 후 `cloudflared access login https://ssh-axe.axelabs.ai` 재시도 안내.

**거부된 대안** ($10/cert/월 ACM, zone delegation, Tailscale 우회, 클라이언트 cert bypass): D-ops-39 본문 참조.

## Microsoft 자사 앱 간 consent preauthorization 정책 — az CLI Mail.Send 영구 차단 (2026-05-26)

> 운영자가 az CLI 로 `/me/sendMail` 호출하려고 시도 → `403 ErrorAccessDenied`. 명시적 scope (`--scope https://graph.microsoft.com/Mail.Send`) 요청 시 `AADSTS65002: Consent between first party application '04b07795-8ddb-461a-bbee-02f9e1bf7b46' and first party resource '00000003-0000-0000-c000-000000000000' must be configured via preauthorization`.

az CLI 의 client app id `04b07795` (Microsoft Azure CLI 자체) 가 Microsoft Graph (`00000003`) 의 Mail.Send scope 받으려면 Microsoft 가 preauthorization 등록해야 함. 자사 앱 간에도 cross-app consent 안 됨. **개별 tenant 의 admin consent 로 우회 불가**.

**영향**: az CLI 경유 mail/chat send 영구 불가. 우회 = (a) [`/api/admin/broadcast-dm`](/ops/runbook/operator-broadcast) (Blueprint REST, 본 use case 의 정공법), (b) Outlook 수동, (c) custom Azure app 신설 (Mail.Send delegated/application + admin consent + client_id/secret vault 저장, 30분+ setup — 사용 빈도 높을 시 [B-axe-mail-send-cli](/ops/backlog) 진행).

**왜 운영자가 함정에 들어갔나**: Blueprint 가 `sendEmail()` 함수 + Mail.Send permission 둘 다 보유 — 자연스레 "외부 connector 에서도 호출 가능" 가정. 실제는 graph_* tool 32+ 중 send 계열은 `blueprint-graph` 내부 MCP 에만 등록, 외부 7cb41f76 connector 는 read-only 격리 (의도). 외부에서 send 가 필요하면 admin REST (`broadcast-dm` 등) 별도 경로.

## D-bp-mcp-calendar-2 send-as 도입 시 8개 함정 (2026-05-26)

> Soohun 의 "ai 계정으로 다른 사용자 캘린더 쓸 수 있냐" 질문 → 1시간 안에 7개 별개 차단점 통과. 각각 다음 calendar/admin write feature 도입 시 동일 패턴.

### Blueprint Azure App ID 혼동

Blueprint 가 **별개의 Azure App 2개** 보유:
- `2b222356-1c36-48e0-96a3-2c5e0ecbf937` = **Blueprint Next.js app** (NextAuth Azure AD provider, `getMsalApp()` → `getAppOnlyClient()` 가 사용). DB AppSetting `azure_ad_client_id`.
- `482598f7-540c-462c-9dfd-b957651eb804` = **Blueprint MCP custom connector** (Claude → MCP OAuth용). CLAUDE.md 의 "Client ID" 항목.

내부 API route 가 app-only token 발급할 때 audience = Next.js app (`2b222356`). Application permission + admin consent **모두 `2b222356`** 에 적용해야 함. MCP app `482598f7` 에 consent 하면 무관 → 403 ErrorAccessDenied 계속 반환. 본 세션 1회 잘못 적용 → MSAL token 디코드해서 `aud` 확인하고서야 발견.

### Blueprint User.id ≠ Microsoft Entra oid

Blueprint `User.id` = Prisma 가 생성한 cuid (e.g. `e1c51fa2-0102-43ed-...`). NextAuth Azure AD callback 에서 Entra `oid` 를 별도 컬럼에 저장하지 않음 → `/users/{User.id}/events` 호출 시 Graph 가 `ErrorInvalidUser 404`. **Graph `/users/{key}` 는 oid 또는 UPN/email 받음** — UPN 사용으로 우회. Route 가 `target.email` 을 path 에 인코딩하도록 fix. 향후 D-bp-mcp-calendar-3 이상에서 직접 oid 필요한 endpoint 도입 시 `User` 에 `oid` 컬럼 추가 검토.

### MSAL `acquireTokenByClientCredential` 토큰 캐시

`@azure/msal-node` 의 ConfidentialClientApplication 은 client-credentials flow 토큰을 in-memory 캐시 (default TTL ~1시간). admin consent 직후에도 캐시된 옛 토큰 (consent 이전 발급, `roles` claim 누락) 이 계속 반환됨 → Graph 403 지속. **container 재시작 또는 `skipCache: true` 옵션으로 강제 refresh**. MSAL `.default` scope 가 "현재 consent 된 모든 perm" 의미라 cache 무효화 트리거 없음 (admin consent 가 MSAL instance 에 signal 안 보냄). 본 세션은 `docker restart blueprint-app-green` 으로 우회.

### Global Admin = soohun.kang 단독 (ai@axellc.com 은 admin 아님)

운영자 가정: "`ai@axellc.com` 으로 az 로그인 되어 있고 관리자 권한이므로 tenant 작업 가능". 실제: AXE 테넌트의 **Microsoft Entra Global Administrator role** = `soohun.kang@axellc.com` 단독. `ai@axellc.com` 은 일반 사용자. `Authorization_RequestDenied` 에러 시 `userObjectId` 필드 디코드 (Entra oid lookup) 로 실제 호출 identity 확인.

**참고**: Blueprint 자체 admin role (`User.role === "admin"`) 과 Azure AD Global Admin 은 별개. ai@axellc.com 은 Blueprint admin (Blueprint MCP send-as 호출 가능), 그러나 Azure AD admin 작업 (Application permission grant 등) 불가.

### `az login` 무구독 테넌트

M365-only 테넌트는 Azure 구독 0개. `az login --tenant <id>` 만으로는 active context 가 직전 계정에 머무름 (No subscriptions 메시지 후 무시). **`--allow-no-subscriptions` flag 필수**. 확인: `az account show` 로 active upn 검증.

### Claude.app `/dev/ptmx` fd leak

Claude 데스크탑 앱이 새 채팅/대화 열 때마다 `/dev/ptmx` open, close 안 함. 누적되면 `kern.tty.ptmx_max=511` (macOS 하드 한도) 도달 → `forkpty: Device not configured` (ENXIO) → 신규 터미널·subprocess PTY 할당 불가. **userspace 측 zsh 좀비 (Claude Code subprocess 누적 82 → 정리해도 무효)** 가 아니라 Claude.app 본체 fd leak. `lsof /dev/ptmx` 로 holder 확인 가능.

해결: Claude.app 종료 후 재실행 (fd 자동 release). sudo 로 `kern.tty.ptmx_max` raise = "Invalid argument" (macOS 가 511 이상 거부). Anthropic 리포트 대상 (`B-claudeapp-fd-leak-report` backlog).

**2026-06-04 재발 확인** (`lsof /dev/ptmx`: Claude.app PID 1개가 510/511 점유, 여유 0). 이때 신규 PTY 차단뿐 아니라 **`sudo` 자체가 막힌다** — sudoers `Defaults use_pty` 탓에 `sudo: unable to allocate pty: Device not configured`. **세션을 안 끊고 root 작업하는 우회법** = `osascript -e 'do shell script "…" with administrator privileges'` (AuthorizationServices GUI 인증 → pty 불요, sudo 미경유). netheal 데몬(D-matrix-4) 설치를 이 방법으로 완료. 단 GUI 세션 필요 (GRD/콘솔 OK, 순수 SSH 불가). 항구적 회수는 여전히 Claude.app 재시작.

### FastMCP tool 의 `Image | dict` union 반환 어노테이션

FastMCP tool registration 이 return type annotation 을 pydantic 으로 schema generate. `mcp.server.fastmcp.utilities.types.Image` 가 union (`Image | dict`) 안에 있으면 `PydanticSchemaGenerationError: Unable to generate pydantic-core schema for <class 'Image'>` → MCP 서버 startup 실패 → blue/green health check 60s timeout → swap 자동 거부. **return annotation 생략 또는 `Any`** 로 우회. 본 세션 `get_teams_hosted_content` 가 1회 발현 후 commit `3c7ae254` 로 fix.

### Pre-existing axelabs-docs `{#anchor}` MDX 3 acorn parse fail

별개 함정 (calendar 와 무관) 이지만 본 세션 docs ship 차단했음. `## Heading {#explicit-anchor}` 문법이 Nextra (MDX 3) 의 acorn 파서를 깨뜨림 → `Could not parse expression with acorn` → `npm run build` 실패 → `axe ship docs` 의 docker rebuild 차단. **MDX 3 에서 `{#...}` 명시 anchor 미지원**: 헤딩 텍스트를 슬러그와 일치시키도록 변경 (github-slugger 자동 ID) 하고 inbound 링크 그대로 유지. 운영자 별도 commit 으로 동시 fix 됨 (race condition 으로 commit `941196f`).

## realchoice D-day 첫 실행 — 6 함정 (2026-05-25 발견)

> Truvia 측 RE^6 의 D-day 즉시 시작 피벗 후 운영자가 `axe onboard realchoice --apply` 실행. **step 1-13 ✅ + step 14 차단**. 본 섹션은 분석적 기록 (실행 항목은 [B-onboard-d-day-traps-2026-05-25](/ops/backlog)).

| # | 함정 | 본질 | 영구 fix 후보 |
|---|---|---|---|
| 1 | Tailscale short host alias (`realchoice-macmini`) macOS resolver 미해석 → ssh fail | macOS resolver 는 magicDNS short alias 자동 처리 X. `~/.ssh/config` 의 Host alias + HostName FQDN 매핑 필수. axe CLI 가 short alias 로 ssh 호출 | partner/macmini-prep 함정 표 + `axe onboard` 가 자동으로 `~/.ssh/config` 추가 또는 ssh 명령에 `-o "Host alias=FQDN"` 사용 |
| 2 | SSH non-login shell PATH 에 `/usr/local/bin` 누락 → `docker not found` | macOS 의 zsh non-login 은 `~/.zshrc` 안 봄. `~/.zshenv` 만 봄. Docker Desktop 의 symlink (`/usr/local/bin/docker`) 가 default PATH 밖. Truvia 측 `~/.zshenv` 에 `export PATH=/usr/local/bin:/opt/homebrew/bin:$PATH` 추가 필요 | partner/macmini-prep 의 §5 Docker 섹션에 `~/.zshenv` 명시 (즉시 갱신 필요) |
| 3 | TXT + CNAME 같은 name 공존 시 `axe onboard` step 5 의 `_cf_dns_find(zone, name)` 가 type filter 없어 거절 | CloudFlare API 는 type filter 미적용 시 모든 record 반환. axe CLI 코드가 "exists" 로 단정 → conflict 처리. RFC 측면 TXT+CNAME 공존 OK | axe CLI `_cf_dns_find(zone, name, record_type)` 시그니처 변경 + onboard step 5 가 `record_type="CNAME"` 명시 |
| 4 | Login keychain locked + SSH non-interactive → `security add-generic-password` exit 36 (errSecAuthFailed) | macOS GUI session 의 keychain unlock 이 SSH non-interactive session 의 securityd partition 으로 propagate 안 됨 | 함정 5 가 본질적 fix (vault SOT 이전) |
| 5 | **macOS keychain partition 격리 — SSH non-interactive 의 securityd 가 GUI session 과 별도 partition** | Truvia 측 GUI 로그인 + timeout 10h 연장 했지만 SSH non-interactive session 영향 0. 즉 `axe onboard` 의 step 12 (secret generation) 가 SSH 으로 호출하면 fundamental 차단. **본 D-day 의 핵심 함정**. 즉시 우회 = Truvia 가 본인 SSH session 에서 직접 명령 실행 (operator → customer 권한 위임 패턴) | (a) `start-frame.sh` 를 customer launchd job 으로 등록 → console session 의 keychain access. (b) **secret SOT 를 keychain 에서 vault 로 이전** (D-ops-17 manifest pattern 확장). bw CLI 는 SSH session 에서도 작동 |
| 6 | **`start-frame.sh` wrapper 가 SSH non-interactive 에서 `security find -w` (value fetch) 시도 → ACL 차단 → FATAL: FRAME_JWT_SECRET empty** | 함정 5 의 구체적 발현. wrapper 가 keychain value 꺼내야 frame postgres + frame-mcp-blue 부팅 가능. SSH 으로 호출 시 차단. axe CLI line 2588 의 step 12 fix (`-w` 제거, metadata-only) 는 step 12 통과시키나 step 14 wrapper 의 fetch 는 동일 차단 | 함정 5 와 동일 영구 fix |

**즉시 우회 (본 세션 적용)**:
- 함정 1: `~/.ssh/config` 신설 (Host alias + HostName FQDN)
- 함정 2: Truvia 측 `~/.zshenv` PATH 추가
- 함정 3: Microsoft verify 완료 후 TXT 제거 + axe CLI 재실행
- 함정 4-5: Truvia 가 본인 SSH session 에서 `security add-generic-password` 직접 실행
- 함정 6: ⏳ Truvia 회신 대기 (옵션 A = 본인 SSH session 에서 `start-frame.sh up -d` 직접 / 옵션 B = `security set-generic-password-partition-list` ACL 변경)

**영구 fix 적용 (2026-05-26, B-customer-deploy-generalization Phase 1 완료)**:
- 함정 4·5·6 → **vault SoT 이전**. axe CLI 의 `_deploy_service_customer` + 새 wrapper 3종 (`start-{frame,blueprint,hive}.sh`) 이 customer 측 `bw get password` 으로 secret fetch. SSH non-interactive 의 keychain partition ACL 회피 (bw 는 파일 기반 `~/.bw-session` 으로 BW_SESSION 획득 → securityd 무관). customer 측 1회 부트스트랩 = `brew install bitwarden-cli` + `bw-bootstrap.sh https://<vault-url> <email>` (interactive). 본 변경 후 함정 4·5·6 = **다음 customer 에서 재발 0**.
- 함정 10 (env_file `$` literal) → `escape_dollar: true` flag 자동 처리. INGEST_MANIFEST_TEMPLATE 의 OAuth secrets 에 빌트인.
- 함정 7 (docker-compose default path hardcode) → `image_override` 슬롯으로 customers.yaml 에서 override 가능 (R4). compose 본체 변수화는 service repo 측 별도 PR.
- 함정 8 (axe onboard step 14 non-idempotent) → 이미 axe CLI line 2720+ 에 `docker ps` 사전 검사 + skip if running 패치 적용 (5/25). 신규 `_deploy_service_customer` 도 동일 패턴 적용 필요 (TODO).

**미해소**:
- 함정 1·2·3 = docs 측 갱신 필요 (partner/macmini-prep 의 함정 표 + axe CLI `_cf_dns_find` 시그니처 변경).
- 함정 9 (frame-proxy stale archive) → `/Users/axe/.axe/frame-proxy/` 5/24 복원 ✅. axe CLI 가 정상 경로 참조.

## 1-shot onboard 갭 — realchoice (2026-05-23 분석)

> **목표**: D-day 에 운영자가 **vault master password 1 회** 입력만으로 신규 customer (realchoice 등) 전체 배포 완료. 현재 6 개 수동 touchpoint 가 남음. [/ops/runbook/customer-onboarding#d-day-tldr--운영자-수동-touchpoints](/ops/runbook/customer-onboarding) 가 SSOT.

> ⚠️ **갱신 (2026-06-06)**: realchoice 는 2026-05-25 **sovereignty self-deploy 로 LIVE 완료** (`customers.yaml` 에서 `services:` 의도 제거). 본 섹션의 realchoice 예시는 *역사적* — 잔여 갭은 `services:` 매니페스트를 선언하는 **신규** customer 에만 적용되고, realchoice 자체엔 `axe deploy {svc} realchoice` 가 미적용된다 (→ [/ops/runbook/customer-onboarding](/ops/runbook/customer-onboarding) sovereignty caveat).

| # | 갭 | 현재 상태 | 닫는 작업 |
|---|---|---|---|
| 1 | `axe customers add {customer}` = stub (line 560-561) | customers.yaml 의 customer 메타블록 + services 슬롯 모두 운영자 수동 편집 | [B-onboard-customers-add](/ops/backlog) |
| 2 | customer IT 회신 8 개 값 → vault 로 운영자 수동 push × 3 + customers.yaml 수동 paste × 4 (tenant_id + 3 client_id) | 안전채널 받은 값 운영자 손에서 vault 로 이동 | [B-onboard-azure-pack](/ops/backlog) — `axe customer ingest {customer} pack.json` 으로 (a) yaml fill + (b) vault push 묶음 |
| 3 | `services:` 섹션이 axe customer 만 등재 (realchoice services 슬롯 부재) | `axe secret push --customer realchoice` 가 manifest lookup 실패 | [B-onboard-customers-add](/ops/backlog) 결과물의 일부 |
| 4 | ~~`axe deploy hive {customer}` subcommand 부재~~ ✅ **해소 (2026-06-06)** | `axe deploy` choices = frame / blueprint / blueprint-mcp / **hive / matrix** (`cmd_deploy_hive` customer-path live, `.axe/bin/axe`) | [B-onboard-hive-deploy](/ops/backlog) |
| 5 | `cmd_deploy_blueprint` docstring 자기 명시: "SSO (Azure AD) 미설정 — Phase 3 추가 작업 필요" | Blueprint 컨테이너는 부트되지만 첫 SSO 로그인 시 secret 부재로 실패. 운영자가 별도로 customer Keychain 또는 vault → .env 채워야 함 | [B-onboard-bp-sso](/ops/backlog) |
| 6 | Cloudflare API token vault 등재 = first-onboard chicken-and-egg | 첫 onboard 직전 1 회 `axe secret push` 필요 (현재 docs 부재) | [B-onboard-cf-token-doc](/ops/backlog) — secrets.mdx 의 bootstrap 섹션에 명시 1 줄 |

**해소 후 D-day 흐름** (목표):
```
# 0. vault unlock — master password 1 회
export BW_SESSION="$(bw unlock --raw)" && security add-generic-password ...

# 1. customer IT 회신 한 줄 ingest (yaml fill + vault push 묶음)
axe customer ingest realchoice ~/Downloads/realchoice-azure-pack.json

# 2. 모든 stack 묶음 배포 (frame + blueprint + hive + vault)
axe deploy all realchoice --apply
```

**현재 drift 발견** (본 sweep, 2026-05-23):
- ~~customer-onboarding.mdx 의 D-7 step 3-5 가 `/Users/axe/.axe/tunnels/axelabs/config.yml` 편집 + `docker restart axelabs-tunnel` 요구~~ → **정정 완료**. 실제 `axe onboard` 는 customer 별 독립 tunnel 을 customer macmini 에 생성 (`_render_cloudflared_config` `/Users/{ssh_user}/.cloudflared/config.yml`). 중앙 tunnel 편집 의무 사라짐.
- ~~`axelabs-bootstrap.sh` + `realchoice_entra_id_setup_v3.md` 가 별도 안전채널 메시지로 전달되는 의존~~ → **정정 완료 ([B-onboard-bootstrap-publish](/ops/backlog#-done-최근-7일-archive-대기), 2026-05-23)**. 스크립트가 `https://docs.axelabs.ai/axelabs-bootstrap.sh` 의 raw 로 노출. partner/registration §Option A + macmini-prep + domain-prep §A/§B + handoff JSON pack 양식 + index 4-step 흐름으로 docs.axelabs.ai 만으로 customer IT 자력 완료 가능. **customer-facing 차단은 0**. 잔여 6 갭 (위 표) 은 모두 **운영자 측 자동화 (umbrella + ingest + hive deploy + bp sso + cf token)** — 운영자의 D-day 명령 수를 5→1 로 줄이는 것이 우선순위. customer 측에는 영향 없음.

## CLI 측 미구현 (axe CLI Phase 5 stub)

| 약속된 CLI | 실제 상태 | 우회 |
|---|---|---|
| `axe restore --customer X --tier local\|ring\|cold --target X --table X --apply` | argparse stub (`--tier`, `--target`, `--table`, `--apply` flag 없음) | restic 직접 호출 (각 runbook 의 갱신된 예시 참조) |
| `axe backup --status` / `axe backup --local --tag X` | `axe backup` subcommand 없음 — `/Users/axe/.axe/bin/axe-backup` shell script 가 매일 03:00 자동 | `restic snapshots` 직접 |
| `axe onboard --skip-azure` | `--skip-azure` flag 없음 (`--skip-frame` 만 있음) | onboard 가 Azure 측 변경 안 함 — flag 없어도 무방 |
| `axe health &lt;target&gt; --customer X` | `--customer` flag 없음 (positional 만) | `axe health frame` 처럼 사용 |
| `axe secret status --customer X` | `--customer` flag 없음 | 운영자 콘솔 dashboard 에서 확인 |
| `axe ship` (release-gate) | ✅ 구현됨 (D-ops-16) | — |
| `axe secret check/pull/push/rotate` (manifest 기반) | ✅ 구현됨 (D-ops-17). pull merge-mode (D-ops-18) | — |
| `axe deploy blueprint-mcp` (blue/green swap) | ✅ 구현됨 (D-bp-mcp-2, 2026-05-21) — frame `cmd_deploy(frame)` 1:1 미러 | — |
| `axe ship blueprint` 안에서 mcp swap 자동 호출 | ✅ 구현됨 (D-bp-mcp-2 후속, 2026-05-21) — cmd_blueprint_upgrade 직후 cmd_deploy_blueprint_mcp wire-up | — |

→ Phase 5 (D) 작업 항목으로 CLI 완성 예정. 그때까지 runbook 의 명령어는 "현재 형식" 으로 사용.

## 매니페스트 / vault 측 미해결

| 항목 | 현재 상태 | 후속 |
|---|---|---|
| 매니페스트 non-secret config 흡수 결정 (Option A: 매니페스트 확장 / B: merge-mode 만 / C: customers.yaml 통합) | merge-mode pull (D-ops-18) 로 즉시 위험 X. blueprint .env 의 AZURE_AD_CLIENT_ID 등 9개는 hand-maintain | 운영 안정 후 정책 결정 |
| `axe-cli@axellc.com` Vaultwarden service account | 미생성. 현재 모든 bw 호출이 운영자 개인 token | D-ops-17 Phase 6 |
| `cmd_blueprint_upgrade` Keychain → vault 통합 | 현재 Keychain inject (line 698) + env_file 동시 사용. 두 출처 sync 책임 운영자 | vault 단일 출처로 통합 |
| `CLAUDE_CODE_OAUTH_TOKEN` 회전 | 2026-05-21 transcript 노출. 사용자 보류 (후순위) | Anthropic console 새 토큰 발급 → `axe secret rotate` |
| hive `HIVE_MAILER_*` 매니페스트 미등재 | `.env.local` 에 hand-maintain 중 (D-hive-23) | customers.yaml hive.secrets[] 에 추가 |
| hive postgres `HIVE_DB_PASSWORD` substitution (compose 변수) | volume reset 시점 default `hive_dev` 로 fall back 위험. 현재 data dir 에 vault 값 이미 init | postgres 에도 env_file 도입 또는 `.env` 명시 |
| **bw CLI `[Encrypt service] MAC comparison failed. ... Failed to decrypt user key with stretched master key`** — 재발성 (5/22 `.broken.1779431724` + 5/26 `.broken.1779783301`). KDF 원인 **아님** (ai@axellc.com 이미 Argon2id). | **Root cause 확정** (5/26 검증): bw CLI 의 local data.json 의 cached `cryptoSymmetricKey` (wrapped user key) 가 server-side patch deploy 후 bw `sync` 시점에 stale 상태로 저장됨. `bw unlock` 은 이 캐시를 invalidate 안 함 → 매 unlock 마다 stale wrapped user key 로 decrypt 시도 → MAC fail. axe Vaultwarden 의 빈번한 fork patch deploy (axe.2, axe.3, ...) 가 트리거. | **즉시 복구 (확정)**: `mv "~/Library/Application Support/Bitwarden CLI" "~/Library/Application Support/Bitwarden CLI.broken.$(date +%s)" && bw config server https://axe.axelabs.ai/vault && bw login ai@axellc.com` (fresh login). 약 1분. → [B-bw-cache-stale-autoheal](/ops/backlog) 영구 fix |

## launchd 측 미구현

| 약속된 launchd | 실제 상태 |
|---|---|
| `com.axe.secret-check` (매일 09:00) | 미구현. Vault item 의 만료 메타 + 운영자 콘솔 dashboard 에서 수동 확인 |
| `com.axe.health-check` (매분) | 미구현. `com.axe.console.refresh` (매시) 가 운영자 콘솔에 health 표시. 매분 알림은 향후 추가 |
| `axe-health-monitor` 바이너리 | 미구현 |

대안: 운영자 콘솔 (`https://admin.axelabs.ai`) 이 매시 rebuild 되며 health 표시. 즉시 알림이 필요하면 `osascript` 또는 Slack webhook 으로 임시 wrapper 작성 가능.

## Docs 측 내부 불일치 (정리 진행 중)

| 항목 | 현재 상태 | 정리 계획 |
|---|---|---|
| `partner/registration.mdx` 신모델 (Web platform + secret + App ID URI + mcp.access) | 작성됨 | axe customer 는 이미 신모델로 마이그됨. realchoice 도 6월 onboard 전 신모델로 적용 |
| `realchoice_entra_id_setup_v3.md` | ✅ 작성 완료 (2026-05-21) | 발송 (5/29 마감) |
| `axe_frame_mcp_setup.md` (구모델 기록) | historical | 본인 실작업 기록 — 사실 정확성 위해 보존 |
| `frame/docs/ops/onboarding-operator.md §0` (구모델 Azure 등록) | obsolete | registration.mdx 로 통합 → 향후 cross-link |
| `customers.yaml.axe.sso.apps.blueprint_mcp` | ✅ 추가됨 (D-bp-mcp-1) | — |
| frame ahead commits 의 docs drift (auth_oidc.py, cli.py, alembic, Dockerfile 등) | operator hive integration 세션 작업분 | operator 의 axe ship frame 사이클에 함께 |

## 정확한 tool count

| 서비스 | docs 표기 | 실제 (코드 기준) | 비고 |
|---|---|---|---|
| frame | ~44 (수정됨) | 44 (`grep -c '@mcp.tool' src/frame/mcp/server.py`) | OK |
| stream | 28 (수정됨) | 28 (admin=7, sales=5, inventory=4, settlement=3, health=3, bridge=2, signals=3, meta=1) | OK |
| magnet | 39+ headline | 51+ 표 합산, 자기 진단 1 포함 시 ~62 데코레이터 | 표 합산이 실제와 다름 — magnet 측 통합 daemon 의 자체 보고가 39 |

magnet 의 39 vs 51+ 차이는 magnet 측 `mcp/server.py` 의 importlib 흡수 시 일부 중복/내부 도구 제외. magnet team 정리 후 docs 갱신.

## hive 골든테스트 ↔ 라이브 데이터 결합 (2026-06-04)

`tests/payroll/test_golden_axec_2026_04.py` 가 라이브 `axec` DB 의 직원 종료상태에 의존 (자체 시드 없이 `compute_period` 실행). 골든 픽스처 (`golden_axec_2026_04.json`) 가 강수훈 (AXEC-001) 을 `terminated 2026-04-01` 로 인코딩 → CI 통과를 위해 **실 직원행을 수동 (out-of-band, audit_log empty-actor) UPDATE** 하게 만든 것이 강수훈 axec 종료의 근본원인 (2026-06-04 agent 조사·DB 검증). 같은 압력으로 `axev/AXEV-003` 강수훈도 수동 종료됐으나 그는 **AXEV 파트너 직급** — 정합화는 backlog [B-hive-axev-003-terminated](/ops/backlog). 구조적 해소 (테스트를 트랜잭션 내 자체 시드 또는 전용 test entity 로 분리) 는 backlog [B-hive-seed-integrity](/ops/backlog). 가드: "terminated 인데 active membership/open employment_record" 면 fail 하는 invariant 테스트.

## gh pr create ↔ repo redirect 함정 (origin=`soohunkang/*` → `axelabs-ai/*`) (2026-06-04)

로컬 git remote `origin` 이 `soohunkang/<repo>` (구 이름) 인데 GitHub 가 `axelabs-ai/<repo>` 로 **redirect** (repo transfer — fork 아님: `gh repo view soohunkang/hive` → `isFork:false, parent:null, nameWithOwner:axelabs-ai/hive`). 이 상태에서:

- ❌ `gh pr create … --head soohunkang:<branch>` (fork 스타일 head) → 존재하지 않는 fork owner 해석 시도 → GraphQL `Something went wrong` 500 (무한 반복). plain `gh pr create` 도 redirect/fork-resolution GraphQL 에서 간헐 500.
- ✅ **REST + plain same-repo head**: `gh api --method POST repos/axelabs-ai/<repo>/pulls -f head="<branch>" -f base="main" -f title=… -f body=…`. 머지도 `gh api --method PUT …/pulls/<n>/merge -f merge_method=squash`.
- `git push origin` 은 redirect 통과(axelabs-ai 에 정상 반영). **진단 순서**: GitHub status green + `gh api rate_limit` 정상이면 장애 아님 → head 형식/redirect 의심하고 REST 로 우회. 모든 세션이 동일 origin 사용 → 공통 함정 (2026-06-04 hive PR #5 에서 ~6회 헛시도 후 규명).

## 운영 자동화 미완성

| 항목 | 현재 |
|---|---|
| 분기 restore drill (`com.axe.restore-drill`) | launchd 등록 + plist 존재. 첫 실제 drill 시기 (Jul 15) 까지 검증 |
| Cold SSD rotation | tooling ready, **SSD 자체 미구매** (operator 작업) |
| Realchoice ring backup | 양방향 SSH 검증 완료 (2026-05-15). realchoice onboard (6/1) 후 첫 sync 시작 |
| **mysrt-postgres backup 정책 미명시** | `mysrt-postgres` 컨테이너 운영 중, axe-backup 범위 밖. mysrt 가 SRT 폴링 (외부 SOT 가 source-of-truth 가능성) 이라 의도적 제외일 수도 / 누락일 수도. `architecture/backup.mdx` 에 "백업 대상에서 의도적 제외" 명시 또는 추가 결정. 검출 2026-05-21. |

## Blueprint 측 미완 (2026-05-21 세션 발견)

| 항목 | 현재 상태 | 후속 |
|---|---|---|
| **Blueprint MCP `get_session` + `list_messages` tool** | 9 read-only tool 중 Message 본문 외부 비공개 → agent transcript blind. 5분야 진단 (2026-05-21) 의 최대 빈 곳 | Stage 1 전 추가. PrismaClient member-scoped pattern |
| **`cmd_deploy_blueprint_mcp` edge probe URL 부정확** | `200 expected` 인데 cloudflared 가 `/blueprint/mcp/health` path 보존 + JWTAuthMiddleware 가 `Bearer` 인증 요구 → 실제 응답 401 (정상 도달). `axe deploy blueprint-mcp` 가 매 swap 에서 "⚠ edge not 200" 경고 출력 | URL 을 `/blueprint/mcp` (auth-required endpoint) 로 + expected `401` + `WWW-Authenticate: Bearer` 헤더 검증 |
| **`axe blueprint upgrade` active color resolve 오작동** | `_blueprint_active_color()` 가 stopped 컨테이너 metadata 를 alias 보유로 인식 → 실제 active 인 컨테이너 rebuild 시도 위험 (2026-05-21 회피: docker compose 직접 호출) | `docker inspect` 결과에 `State.Running == true` 필터 추가 |
| **Trinity scheduled reconcile 부재** | `src/lib/trinity-sync.ts:1-15` 가 사용자 클릭 trigger / `resolveLocalPath` self-heal 만. 6/16 row drift 가 사용자 trigger 까지 잔존 | cron 또는 launchd 일일 reconcile |
| **`/axe/personas` UI 부재** | API `src/app/api/personas/route.ts:11-19` 만, page 없음 | persona 자기 큐레이션 UI |
| **`/api/health` 단순 200** | 14줄, DB/Postgres/Graph upstream probe 없음 | upstream probe + Cloudflare health-gate 신뢰 회복 |
| **IC DATA-FIX mode** | SKILL.md:140-173 4-mode (INITIAL/APPEND/REVISION/FINALIZE) 만. 숫자 정정 시 강제 v-bump | DATA-FIX mode 추가 |
| **PARA dispatch UI** (Project 종결 시 Area/Resource 로 artifact 이관) | schema 부분 완료 ([PR #339](https://github.com/soohunkang/blueprint/pull/339) 의 `sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt` 3 필드). [D-bp-entity-2](./decisions#d-bp-entity-2--para-dispatch-flow-sub-consensus) 정식 등재. UI 만 잔여 (PR 5 예정). 미결 3종: 분배 단위 (파일 vs 의미) / Area 인스턴스 정의 권한 (org-admin vs free) / 정비 트리거 우선순위 | Path B Spike (DB 변경 없이 단일 workspace 로 LLM 분배 정확도 검증 3-5일) → Path A 본구현 (Area/Resource UI + dispatch modal + archive search separation) |
| **Settings UI default entity dropdown** | `/api/user/default-entity` PATCH endpoint ([D-bp-entity-7](./decisions#d-bp-entity-7--userdefaultentity--사용자별-명시적-default-entity)) 있으나 UI 부재. 사용자가 직접 변경 못 함 — 운영자 SQL 또는 customers.yaml 순서 의존 | `/axe/settings/SettingsClient.tsx` 에 dropdown 한 줄 (~30 min) |
| **axe entity register 통합 명령** | hive `register-entity` + Blueprint `seed-entities.ts` 갱신 두 명령 분리. 부분 등록 함정 (한 시스템만 등록되고 다른 시스템 안 됨) 가능 | axe CLI `entity register <slug> --name ... --biz-no ...` subcommand. hive subprocess + Blueprint Entity row INSERT atomic |
| **blueprint-mcp + hive-mcp `/health` anon expose** | JWTAuthMiddleware 가 모든 path (/health 포함) Bearer 요구. frame-mcp 는 /health anon. axe CLI edge probe 가 401 expected 로 현재 workaround. frame 패턴 mirror 가 본질 fix | FastMCP middleware exclusion list 에 `/health` 추가 (blueprint-mcp + hive-mcp 각각). axe CLI expected 401 → 200 정정. mcp-server-checklist 에 항구화 |
| **Teams attachment unsupported contentType → LLM false-negative** | `src/lib/teams/attachments.ts:138` 의 unsupported-loop 가 `[unsupported contentType: X]` stub 만 LLM 에 넘김. LLM 은 stub 만 보고 "X 못 봤다" 그럴듯한 답 confabulate (실제 API 한계와 구분 불가). `messageReference` 1 종은 [PR #372](https://github.com/soohunkang/blueprint/pull/372) 에서 fix. `card` / `file` / 기타 contentType 미점검 | [B-bp-teams-attachment-contenttype-audit](/ops/backlog). production 관측 contentType enumerate + handler 추가 또는 placeholder 명확화. [feedback_bot_capability_gap_self_diagnosis](memory) |
| **Knowledge Layer 격차 — typed fact 부재** | ctx skill 이 markdown PKM 만 제공. Per-field citation / cross-functional query (frame typed × portfolio markdown join) / time-travel / 결정론적 충돌 해소 모두 불가능. 인프라는 dev-co level (per-customer isolation / blue-green / restic backup) 인데 knowledge layer 만 single-operator PKM tool 수준 | [M6 Blueprint artifact + PARA 지식 레이어](/ops/roadmap#m6--blueprint-artifact--para-지식-레이어) 가 본 격차 직접 대응. [D-bp-artifact-1~5](/ops/decisions) (2026-05-23 등재). [아키텍처 페이지](/architecture/artifacts) |

## Multi-tenant 외부 출시 차단 (Stage 0 → 1)

5분야 진단 (2026-05-21) 결과:

| 항목 | 현재 | 후속 |
|---|---|---|
| **Org tenancy FK fanout** | `Workspace/UsageLog/Agent` 에 organizationId FK 부재 (`prisma/schema.prisma:688` "별도 PR" 주석). single cross-tenant 쿼리 누락 = 전 고객 노출 | D-bp-org-fanout 신규 결정 + migration. D-bp-entity-1 의 entityId FK 와 별개 (entity = 회계 단위, org = customer 단위) |
| **Azure AD tenant env-lock** | `src/lib/auth.ts:101` `AZURE_AD_TENANT_ID` 단일 env → 외부 IdP 분기 0 | per-org `Organization.azureTenantId` + `auth.ts` multi-tenant Azure AD provider |
| **법무 페이지 / rate-limit / Sentry / `axe-health-monitor`** | TOS / Privacy / DPA / PIPA 페이지 0건. edge rate-limit 부재. monitor 미구현 (위 launchd 측 미구현 참조) | Stage 1 closed beta 진입 prereq |
| **Backup tier-A** | ✅ blueprint-postgres dump 포함 (2026-05-21 backup.mdx 갱신) | restore drill 1회 (Jul 15) |

## MCP 공통 함정 (D-bp-mcp-3 cross-cutting, 2026-05-22)

[D-bp-mcp-3](./decisions#d-bp-mcp-3) 본질이 frame · hive 에도 부분 적용. 양파껍질 6 layer 의 한 갈래:

| 서비스 | SQLite legacy 잔재 risk | startup probe (lifespan) | 비고 |
|---|---|---|---|
| **blueprint-mcp** | ✅ 해소 (D-bp-mcp-3 2026-05-22) | ✅ 적용됨 | `.env` line 제거 + config fail-fast + lifespan `SELECT 1` |
| **frame-mcp** | N/A (URL self-construct, env string 안 씀) | ❌ **미적용** — `/health/ready` 는 request-time only (`frame/src/frame/mcp/http_server.py:594`) | broken DB 로 부팅 가능, swap promote 위험. **lifespan probe 1 블록 추가 권장** (blueprint pattern mirror) |
| **hive-mcp** | N/A (frame 패턴 동일) | ❌ **미적용** — `hive/src/hive/mcp/http_server.py:364` | 동상 |

**복붙 메시지** (운영자가 frame/hive owner 에게 전달): [/architecture/mcp-server-checklist#16 startup probe pass](../architecture/mcp-server-checklist#8-운영--14-가지-체크포인트) 참조. 5-10 분 작업.

## claude.ai 멀티-커넥터: 1 커넥터의 나쁜 key 가 전체 tools/list 400 (2026-05-28)

claude.ai 는 활성화된 **모든 커넥터의 tool 을 하나의 `tools` 배열로 합쳐** API 에 보냄. 따라서 **어느 한 커넥터** tool 의 inputSchema property key 가 `^[a-zA-Z0-9_.-]{1,64}$` 를 위반하면 → `400 tools.N.custom.input_schema.properties` 로 **요청 전체 거부** → 그 대화의 *모든* 커넥터(결백한 것 포함)가 같이 막힘. `tools.N` 의 N 은 결합 배열 인덱스라 커넥터 on/off 시 바뀜 → 범인 특정이 어려움 (Cortex 함정 #6 의 cross-connector 확장판).

- **2026-05-28 발견**: cortex register_person 이 `tools.15`/`tools.10` 으로 막힘. cortex(12)·frame(51)·hive(39) 라이브 tools/list + Claude-in-Chrome 22 스키마 직접 검증 → 전부 clean. 범인 = **claude.ai 의 `GitHub 연동`(Anthropic 제공 통합)** — 우리가 소스 못 고침. 사용자가 그 커넥터만 비활성화 → 즉시 해소.
- **진단법 (사용자에게 커넥터 토글 반복 요청 금지)**: 각 서비스의 `mcp-token` CLI(frame/hive) 또는 HS256 수동 mint(cortex)로 로컬 `tools/list`(blue 컨테이너 직접) 받아 모든 property key 를 정규식 검증. Anthropic 커넥터(GitHub 등)는 소스 접근 불가 → 소거법으로 좁힌 뒤 사용자가 해당 커넥터 비활성화 + 앱 내 피드백 신고.
- **예방**: 우리 서비스는 trap #6(ASCII-only key) 준수. CI/`axe mcp publish` 에 "served tools/list 의 모든 property key 정규식 lint" 추가 권장 (backlog 가치).

## 신규 ship 된 MCP tool 이 이미 연결된 Claude 세션·서브에이전트에 안 보임 — tools/list 재동기 지연 (2026-05-29)

> Blueprint MCP `send_mail` ([D-bp-mcp-mail-1](./decisions#d-bp-mcp-mail-1)) ship + self-send Graph 202 검증 직후, 운영자의 active Claude Code 세션에서 호출 → `No such tool available`. 같은 세션에서 spawn 한 서브에이전트도 동일 (ToolSearch 4회 모두 미발견).

MCP 클라이언트(Claude Code / claude.ai)는 **연결 handshake 시점의 `tools/list` 를 캐시**한다. 서버에 tool 을 새로 배포해도 **이미 연결된 세션은 재동기 전까지 못 본다**. 서브에이전트(Agent tool)는 **부모 세션의 MCP 연결을 공유**하므로 스폰으로도 해소 안 됨 (stale registry 상속).

- **결정적 해결 = 새 세션**: `spawn_task` 칩 / 세션 재시작 / 커넥터 reconnect(`/mcp`). fresh handshake → fresh `tools/list`.
- **자가 해소되기도 함**: 본 건은 일정 시간 후 클라이언트가 재-list 하여 같은 세션에 `send_mail` 이 deferred tool 로 떠 호출 성공(재시작 없이). "전파 지연"이지 영구 차단 아님 — 타이밍은 비결정적.
- **오진 주의**: "tool 미발견 = 배포 실패" 단정 금지. 서버 측 검증(self-send 202 / 컨테이너 직접 `tools/list`)이 통과하면 배포는 정상, 클라이언트 캐시 문제다.
- **운영 수칙**: MCP tool ship 후 같은 세션 즉시 호출 실패는 정상. 급하면 새 세션(칩/재시작), 아니면 잠시 후 재시도.

## Cortex production live — 7 함정 (2026-05-28, 신규 Rust MCP 서비스 첫 배포)

cortex (Rust + axum + sqlx, 첫 비-Python MCP 서비스) 를 claude.ai connector 까지 live 시키며 밟은 함정. **index ([D-index-2](./decisions)) 가 cortex 1:1 미러라 그대로 상속** — 등재 순서대로 회피할 것.

| # | 함정 | 증상 | 회피 |
|---|---|---|---|
| 1 | **DB password 의 base64 `/`·`+`·`=` 가 connection URL 파싱 깨뜨림** | `error with configuration: invalid port number` — `postgres://user:pa/ss@host:port` 에서 `/` 뒤가 port 자리로 오인 | sqlx `PgConnectOptions::new().host().port().username().password()` 빌더 사용 (URL string 조립 금지). cortex `src/db.rs::connect_options` |
| 2 | **docker compose `env_file:` 가 따옴표를 literal 로 처리** | `axe secret pull` 이 `KEY="val"` 로 쓰면 컨테이너 안 env = `"val"` (따옴표 포함) → postgres auth 실패 / secret mismatch. compose `.env` (variable substitution) 은 따옴표 strip 하므로 postgres init 은 unquoted, mcp 컨테이너만 quoted → 불일치 | env_file 값은 unquote. 임시: `sed -i '' -E 's/^([A-Z_]+)="(.+)"$/\1=\2/'`. 영구: [B-axe-secret-pull-noquote](./backlog) (axe secret pull 이 quote 안 붙이게) |
| 3 | **RFC 9728 resource-level metadata path 누락** | claude.ai 가 `<application_id_uri>/.well-known/oauth-protected-resource` (= `/cortex/mcp/.well-known/...`) fetch 시도 → 404 (server-level `/cortex/.well-known/...` 만 있으면 부족) → discovery 실패 → origin `/authorize` fallback | server-level + resource-level **둘 다** 라우트. [D-ops-23](./decisions) frame 학습 — cortex 가 미반영했다 재발 |
| 4 | **RFC 8414 path-insertion — authorization_servers 를 자기 (path 있는 issuer) 로 두면 안 됨** | `authorization_servers: ["https://axe.axelabs.ai/cortex"]` → claude.ai 가 RFC 8414 §3.1 따라 `https://axe.axelabs.ai/.well-known/oauth-authorization-server/cortex` (host 와 path 사이 삽입) 에서 metadata 찾음 → 우리가 serve 하는 `/cortex/.well-known/...` (path append) 와 불일치 → 404 → origin `/authorize` fallback → blueprint 404 | `authorization_servers` 를 **Microsoft** 직접 (`login.microsoftonline.com/<tenant>/v2.0`) 로. claude.ai 가 Microsoft OIDC metadata fetch → Microsoft endpoint 사용. self-hosted AS proxy 불필요 ([D-cortex-8](./decisions), frame "직접-Microsoft" path) |
| 5 | **claude.ai connector 는 client_id + client_secret 입력 필수** | URL 만 입력 시 confidential client credential 없어 OAuth 미완 | Advanced field 에 client_id (Entra appId) + client_secret 입력. frame/hive/blueprint 와 동일 ([D-vault-mcp-catalog](./decisions), `axe mcp publish` 로 Bitwarden 확장 auto-suggest) |
| 6 | **MCP tool inputSchema property key 는 ASCII only** | 한글 key (`"메모"`) → Anthropic API `400 tools.N.custom.input_schema.properties: Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'` → **해당 connector 의 tools/list 전체 거부** (1개 나쁜 key 가 12 tools 모두 막음). claude.ai 에서 silent fail | property key 는 ASCII (`memo`). 한글 값은 `additionalProperties: true` 로 런타임 전달 가능 (key 만 제약). cortex commit `dfa9330` |
| 7 | **Cloudflare 터널이 Dashboard remote-managed** | 로컬 `~/.axe/tunnels/axelabs/config.yml` 편집 + 컨테이너 재시동해도 ingress 안 바뀜 (`cloudflared` log 의 `Updated to new configuration version=N` 이 remote 적재 증거). `axe-tunnel` 와 `axelabs-tunnel` 이 다른 컨테이너 (전자 = axellc.com zone, 후자 = axelabs.ai zone) — 잘못된 것 재시동하기 쉬움 | Cloudflare Dashboard 또는 API (`PUT /accounts/<acct>/cfd_tunnel/<uuid>/configurations`, token = vault `Cloudflare API - axelabs`) 로 ingress 변경. `axe` 의 `_cf_request` helper 가 이미 있음 → [B-axe-tunnel-add-ingress](./backlog) CLI 화 권장 |

**부수**: (a) Docker image 는 `cargo build` (호스트) 와 별개 — 코드 변경 후 `docker compose up -d --build` 필수 (안 그러면 옛 binary). (b) `shred` 는 macOS 미존재 (Linux 전용) — `rm -P` 또는 APFS+SSD 에선 `rm` 으로 충분. (c) **Google OAuth consent 2026-05-29 Testing → production 전환** (client `135512942819-l18ra7gkf1ac93ai8t4hf2h4jl6mi0a2.apps.googleusercontent.com`, project no. 135512942819). Testing 시절 함정(Test users 목록의 Gmail 만 허용·100명 한도·미등록=`403 access_denied`, **+ refresh_token 7일 만료**로 sync 주기적 단절 위험)은 production 전환으로 해소. **잔존 함정**: 스코프 `auth/contacts` 는 Google *sensitive* 인데 앱이 unverified → 동의 화면에 "확인되지 않은 앱" 경고 + unverified 사용자 100명 상한. 사용자는 **고급 → "&lt;앱&gt;(으)로 이동(안전하지 않음)"** 으로 통과(동작엔 지장 없음). 경고 제거 = Google 정식 검증 → [B-cortex-google-oauth-verify](./backlog) (**금전비용 0** — `contacts` 는 sensitive 라 CASA 제3자 보안평가 불필요; restricted 였으면 수천 $/yr). Internal user type 은 Workspace 조직원 한정이라 개인 Gmail 연동엔 부적합.

**Cortex — `attended` relationship 방향 불일치 (2026-05-29 PM12, 미해결)**: interaction↔person 의 `attended` 엣지가 두 생성 경로에서 반대 방향. `log_interaction` MCP = `from=interaction → to=person`, xlsx_hpe/backfill = `from=person → to=interaction`. 따라서 "interaction 의 참석자" 를 한 방향만 가정해 쿼리하면 절반 누락. **현재 대응**: web `fetch_attended_participants` 가 `$iid IN (from,to)` 후 반대쪽 endpoint=person 으로 **양방향 흡수** (display 레벨 해소, 36 interaction 전부 resolve). **미해결**: 데이터 정규화(36건 방향 통일) migration 미실시 — 새 코드가 "interaction 의 attended" 를 짤 때 반드시 양방향. 우회 invariant 는 cortex `CLAUDE.md` 운영 함정에도 기록.

## index 측 — proceeds 산정 정밀도 (2026-05-29, D-index-22 audit)

EV→EqV(net debt) bridge + 투자 후 dilution 이 7-deal 에 비일관 적용된 사실 (사용자 audit 발견). [D-index-22](./decisions) 가 schema/validate 로 강제 (`proceeds_basis` enum + Check 7 `stake×(EV−net debt)` ±2% reconcile, broken bridge hard-reject) 하고 Sendy 는 `ev_bridge` 로 정정 (net debt 0, IRR 불변 6.47%). 잔여 분석적 사실:

| 항목 | 현재 상태 | 후속 |
|---|---|---|
| **Iippo·Canopy legacy_ev** | EV-based 이나 per-leaf 에 exit_ev/net_debt/stake 미itemize → validate Check 7 = legacy_ev WARN 2건. Canopy 는 net debt 300억 을 model-level `exit_assumptions` 에 명시(EqV 498억)했으나 per-leaf 분해 안 됨; Iippo 는 asset-light net debt~0 | [B-index-proceeds-bridge-retrofit](./backlog) — per-leaf ev_bridge 전환 (40+/30+ leaf) |
| **Iippo·Sentry entry-F/D-flat dilution** | exit proceeds 가 entry fully-diluted stake (Iippo 2.065% / Sentry ~4.46%) 를 exit 까지 flat 적용 — Pre-A→exit(5~7y) 사이 Series A/B 신주 희석 미반영 → proceeds·IRR 수 pp 과대 낙관. RCPS anti-dilution/pro-rata 부분 상쇄 | Closed deal 이라 사후 정정 실익 낮음 — retrofit 시 exit_stake 를 round-별 희석 반영값으로. pre-IPO 3 + Sendy 는 이미 exit-date 희석 stake 명시라 해당 없음 |
| **net debt = screening 가정 (BS 부재)** | Sendy net debt 0 은 dataroom 에 BS 없어 asset-light + 자금니즈 equity 충족 근거의 보수적 screening 가정 | DD 단계 실 BS 확보 시 `exit_net_debt_krw` 한 줄 수정 → proceeds·IRR 자동 재산출 (deterministic) |

## frame 측 미완 (회계 도메인 확장)

| 항목 | 현재 상태 | 후속 |
|---|---|---|
| **AXEV 창업기획자 펀드 회계** | **Migration #1 + #3 일부 완료** (2026-05-22, D-ops-22). `shared.entity` 에 `entity_kind`/`fund_meta`/`closed_at` ADD + `shared.entity_relationship` 확장 (numerator/denominator/unit + `gp_managed_fund`/`lp_invested_fund`) + `shared.cross_journal_link` 신설 + `register-entity` CLI `--kind`/`--fund-meta` flag + alembic `0008_shared` 적용 + frame-mcp blue/green rebuild + tests/test_shared_fund_domain.py 16 PASS. **axec/axev/axtest** 자동 entity_kind='corporate'. 강태훈 대표 2026-05-23 자료 ingest 예정 (Evidence + entity meta + 통장내역 raw_transaction). 잔여: 펀드 전용 계정과목 (출자금·미출자약정·평가손익·분배금) + capital call / waterfall + AXEV↔조합 cross-entity mirror 분개 (GP commitment·운용보수·성과보수) 모두 미구현 | **Architecture (2026-05-21 채택 + 2026-05-22 drift 정정 + 2026-05-22 Migration #1 적용)**: <br/>• **기존 `shared` 확장 ✅완료** (frame_meta schema 신설 폐기 — 코드 탐색 결과 기존 객체와 명명 충돌 + cross-entity 메타 layer 가 이미 shared 에 존재): ① `shared.entity` 에 `entity_kind` (corporate/kip/kvf) + `fund_meta` JSONB + `closed_at` 컬럼 ADD ✅. ② `shared.entity_relationship` (PR #26) 에 `ownership_numerator/denominator/unit` 컬럼 ADD (% 폐기, 반올림 오차 회피) + kind ENUM 확장 (`gp_managed_fund`/`lp_invested_fund` 추가) ✅. 의미 매핑: entity_a=holder, entity_b=target. ③ `shared.cross_journal_link` 신설 (mirror 분개 pair, schema 격리 환경 무결성) ✅.<br/>• fund schema 내부 (미구현): `commitment_ledger` (LP 약정/call/미출자 off-BS) + `lp_master` (조합원 + optional `external_entity_id`) + (optional) `fund_waterfall_state`.<br/>• fund 계정 seed (미구현): 기존 `shared.account_template` 에 새 standard `'fund_ksme'` 추가 + 펀드 계정 row.<br/>• Blueprint = workspace↔entity **scalar 1:N** (`Workspace.entityId` 단일). 사용자 결정 (2026-05-22, [PR #339](https://github.com/soohunkang/blueprint/pull/339)): OneDrive `/Ventures/` `/Corporation/` 폴더 분리가 SOT, fund 회계의 cross-entity 는 `User.entityScopes` 로 처리. N:M `WorkspaceEntity` join 권고는 reject (over-engineering).<br/>• 권한 enforcement = Blueprint middleware (gatekeeper). frame 은 entity_id 동등 처리. 기존 `entity_session` search_path 패턴 (entity_id == schema_name) 유지.<br/>• 회계 원칙: AXEV↔fund = parent-child 아닌 운용 관계 (K-IFRS 1110 통제 미달). relation_kind 가 회계 처리 갈래 (지분법/공정가치/운용보수) 결정.<br/>**Migration 순서**: (1) ✅완료 shared 확장 + cross_journal_link 신설 (alembic `0008_shared`) [D-ops-22], (2) fund_ksme standard + seed [D-frame-N2], (3) ✅일부완료 (CLI `--kind`/`--fund-meta`) — 잔여: MCP `register_entity` tool (CLI 의존 제거) + `list_sub_entities` tool [D-frame-N3], (4) Blueprint WorkspaceEntity migration [D-bp-entity-1 수정], (5) 결성 조합 schema bootstrap + shared.entity row [조합 detail 대기], (6) fund schema 의 commitment_ledger/lp_master + 과거 결산 적재, (7) cross_journal_link 활용한 mirror 분개 tool [D-frame-N4] |

## frame shared.entity 의도 불명 row (`axep`)

2026-05-26 audit 발견. `shared.entity` 의 `axep` (legal_name "액스파트너스 유한책임회사", entity_kind=corporate, accounting_standard=ksme, 2026-05-22 08:56 등록) 의 등록 의도가 확인되지 않음. `audit_log` 은 `account` 한 table 의 chart seed 만 (운영 데이터 0). actor null, biz_no 없음, git 검색 결과 코드에도 언급 0.

후속: 강수훈 의도 확인 → (a) 실제 액스파트너스 유한책임회사 설립 의도면 보존 + biz_no/fund_meta 보강, (b) 단순 placeholder/test 면 DROP SCHEMA axep CASCADE + DELETE shared.entity. 본 row 는 [D-frame-register-entity-atomic](/ops/decisions) 2026-05-26 cleanup 사이클에서 보존 결정 (low-risk to leave, 강수훈 confirm 대기).

## 디자인 시스템 배포 (`@axe/ui` · axelabs.ai)

| 항목 | 현재 상태 | 후속 |
|---|---|---|
| **`@axe/ui` 패키지 발행 채널** | git+ssh 직접 임포트 (`pnpm add 'git+ssh://...axelabs#tag'`). dist 빌드 없이 raw TS export — 소비자는 `transpilePackages: ["@axe/ui"]` 명시 필요. npm registry / Verdaccio / GitHub Packages 모두 미사용 | v0.1 stable 후 GitHub Packages 검토. 현재 React 소비자 ≤3개라 git+ssh 로 충분 |
| **`axelabs.ai` 도메인 라이브** | ✅ LIVE — apex + www + docs + design 모두 tunnel 경유 200 (2026-05-29 검증). 컨테이너 `axelabs` (127.0.0.1:3900), `axelabs-docs` (3140). | (완료) |
| **`design.axelabs.ai` 디자인 시스템 도메인 (D-ui-3) LIVE** | ✅ 2026-05-29 — `ui.axelabs.ai` → `design.axelabs.ai` 완전 교체. DNS proxied CNAME (axelabs.ai zone, CF API) + tunnel remote config v21→v22 (ui→design 1-rule swap) + axelabs rebuild (proxy.ts host 분기). 검증: design 200(@axe/ui 페이지)·sub-path 308·ui 404·axelabs.ai/www/docs 무영향 | **함정 4**: ① tunnel ingress = remote-managed (config.yml 무효 — 본 페이지 "Cortex production live 7함정" #7) → `PUT /accounts/<acct>/cfd_tunnel/<uuid>/configurations` (token=vault `Cloudflare API - axelabs`). ② axelabs.ai zone DNS 는 반드시 **CF API 토큰** — `cloudflared tunnel route dns` 는 cert-scoped(axellc.com) 라 `<host>.axellc.com` 오생성 (이번에 `design.axelabs.ai.axellc.com` 생겨 삭제함). ③ host 분기 = `proxy.ts` (next.config rewrites 아님 — 정적 prerender `/` 우회). ④ 신규 1-level host 직후 로컬 resolver AAAA-only → IPv6 무라우팅 머신 `curl` HTTP 000 (production 무관, `curl -4`/`--resolve` 검증) |
| **Clash Display @import drop 함정** | Fontshare URL 의 `f[]=` 대괄호가 Next.js CSS @import 빌더에서 떨궈진다 (현 16.2.6 확인). 해결: `<link rel="stylesheet">` 를 layout 의 `<head>` 에 직접 추가. Pretendard·D2Coding `@import` 는 정상 | 다른 서비스 마이그레이션 시 같은 함정 안 빠지게 README + `/ui#consume` 에 명시. 향후 Next.js 픽스 시 재검토 |
| **nextra 테이블 셀 코드스팬 안 escape 안 된 파이프 → build 깨짐** | 2026-05-29 발견 (decisions.mdx D-cortex-google-drift). 마크다운 테이블 셀의 inline code 안에 escape 안 된 파이프 문자(예: JSON-ish payload 의 union 타입 표기)가 있으면 GFM 이 그 파이프를 셀 구분자로 먼저 분리 → 백틱 스팬이 깨지고 그 안 중괄호가 미닫힌 JSX expression 으로 노출 → nextra 가 "Unexpected end of file in expression" 로 **전체 build 실패**. `axe ship docs` 의 docker build 단계에서야 늦게 잡힘. **해결**: 셀 안 파이프는 항상 백슬래시 escape, 중괄호·꺾쇠도 코드스팬 의존 말고 escape (꺾쇠는 HTML entity). **예방**: ship 전 로컬 `npx next build` 로 컴파일 확인 (docker 왕복보다 빠름). | 여러 세션이 같은 mdx 편집 시 file 단위 git add 가 병행 세션 WIP 까지 휩쓸 수 있음 (이번 cortex D-cortex-google-drift 가 index 커밋에 혼입) → ship 전 staged diff 확인. docs ritual 에 "ship 전 npx next build" 추가 검토 |
| **`Button asChild` 미지원** | 현재 `Button` 컴포넌트는 raw `<button>` 만 — anchor 가 필요하면 `<a className="axe-btn axe-btn--primary">` 패턴 직접 사용. axelabs.ai 메인 페이지가 이 패턴으로 작성됨 | v0.2 에서 Radix Slot 패턴 도입 검토 |
| **dist 빌드 부재** | `package.json` exports 가 `./src/lib/index.ts` (raw TS) 를 가리킴. tsup/unbuild dist 빌드 없음 — 소비자 측 트랜스파일러에 의존 | Next.js / Vite 둘 다 transpilePackages 로 가능. dist 도입은 외부 OSS 공개 시점에 |
| **standalone build 의 `output: "standalone"` 의존** | Dockerfile 의 `.next/standalone` copy 가 `next.config.mjs` 의 standalone output 설정에 의존. 누락 시 빌드 fail | next.config 주석으로 표시. config 변경 시 Dockerfile 같이 확인 |
| **문서 템플릿 `@page` 전역 누수** (D-ui-3 Phase 2) | `.axe-doc` 인쇄용 A4 `@page` 를 전역 bare `@page` (size A4) 로 선언하면 design 사이트 **전 페이지** 인쇄가 A4 로 강제됨 (`@page` 는 문서 전역 스코프). 해결: named `@page axe-doc-portrait`/`axe-doc-landscape` + `.axe-doc` 의 `page:` 속성으로 스코프 — `document.css` 적용 완료 | 다른 인쇄 surface 추가 시 동일 패턴. jinja 템플릿 로컬 렌더·검증엔 jinja2 필요 (system `python3` 미포함 → `python3 -m venv` 후 `pip install jinja2`) |

## docs `verify:box` 오탐 — orphan `next dev` 의 IPv6 `:3140` port-shadow (2026-05-29)

`axe ship docs` 의 post-deploy `verify:box` (playwright `page.goto(localhost:3140, networkidle)`) 가 `TimeoutError: page.goto: Timeout 30000ms exceeded` 로 실패 → "❌ POST-DEPLOY VERIFY FAILED — production 박스 정렬 깨짐 (exit 3)" 출력. **그러나 배포는 성공했고 production 은 정상** — 전형적 오탐. ship 자체를 의심해 롤백/재빌드 하면 멀쩡한 배포를 헛돌린다.

근본 원인: host 에 떠 있던 **orphan `next dev --port 3140`** (parent=launchd, 4h+ 방치된 로컬 docs dev 잔여) 가 IPv6 `*:3140` 을 점유. `localhost` 가 `::1` 을 먼저 resolve 하는 macOS 특성상 host 의 모든 `localhost:3140` 접근 (playwright + curl) 이 죽은 dev 서버로 빨려감. production docker container 는 IPv4 `127.0.0.1:3140` 으로만 노출되고, 외부 `docs.axelabs.ai` 도 tunnel 이 `host.docker.internal` (IPv4) 로 origin 을 잡으므로 — **둘 다 멀쩡한데 host loopback 경유 검증만 깨진다**.

| 검증 경로 | 결과 |
|---|---|
| `curl localhost:3140` (host, `::1` 우선) | hang / timeout (orphan dev) |
| `curl -4 127.0.0.1:3140` | 200 / ~10ms (production container) |
| docker `inspect` in-container healthcheck | `FailingStreak: 0` (정상) |
| 외부 `https://docs.axelabs.ai` | 신규 본문 정상 서빙 |

진단 분기점: `lsof -nP -iTCP:3140 -sTCP:LISTEN` → listener 가 **두 개** (IPv6 `node` vs IPv4 `com.docker`). IPv6 쪽 PID 의 parent 가 launchd 이고 그 PID 가 production 트래픽 0 이면 orphan 확정. `verify:box` 만 깨지고 `curl -4` + in-container healthcheck + 외부 도메인이 모두 통과하면 = 배포 OK · 검증 경로 오염.

조치: orphan 을 `kill -TERM <pid>` (graceful, SIGKILL 불필요 — production PGID 와 무관). 이후 `localhost:3140` 이 IPv4 로 fallback → `verify:box` 재실행 PASS. 예방: 로컬 docs dev 는 끝나면 반드시 종료할 것. `:3140` 은 production container 점유 포트이므로 로컬 dev 는 **다른 포트** 권장 (`npx next dev --port 3142` 등).

## Blueprint Postgres — `prisma migrate deploy` 가 P3005 로 신규 마이그레이션 자동 적용 안 함 (2026-05-30)

`blueprint-postgres` 는 D-config-17 cutover 때 `db push` 로 부트스트랩돼서 `_prisma_migrations` 베이스라인 테이블이 **없다**. 그래서 컨테이너 기동 시 `start.sh` 의 `prisma migrate deploy` 가 **P3005 ("The database schema is not empty")** 로 거부하고 `[start] prisma migrate deploy failed (non-fatal)` 만 찍고 넘어간다. 결과: **`prisma/migrations/` 에 새 마이그레이션 파일을 추가해도 배포 시 자동 적용되지 않는다** (테이블이 생성되지 않음).

증상: route·코드는 떠 있는데 신규 테이블이 없어 `prisma.<model>.create()` 가 런타임에 실패. 감사 로그처럼 try/catch 로 감싼 경로면 **조용히** 실패할 수 있다 (D-bp-mcp-mail-1 의 `MailSendLog` 가 정확히 이 케이스 — route 정상 배포됐으나 테이블 부재로 audit write 가 silent fail 직전).

해결 (스키마 변경 시마다 1회):

```bash
# additive 변경(새 테이블 등)이면 해당 마이그레이션 SQL 만 직접 적용 — 가장 안전, 기존 테이블 무영향
docker exec -i blueprint-postgres psql -U blueprint -d blueprint -v ON_ERROR_STOP=1 \
  < prisma/migrations/<ts>_<name>/migration.sql
# 적용 확인
docker exec blueprint-postgres psql -U blueprint -d blueprint -tc "SELECT to_regclass('public.\"<Table>\"');"
```

2026-05-30 기준 같은 함정에 걸린 마이그레이션: `20260529120000_add_mail_send_log` (수동 적용 완료), `20260531000000_add_user_entra_oid` (다른 세션 작업 — `User.entraOid` 컬럼, 수동 적용 필요할 수 있음), `20260603100452_add_entity_legal_name` (C1 — `Entity.legalName` 영문 법인명 + axec/axev 백필, **미적용** — 배포 시 위 psql 직접적용 패턴 사용; additive·기존 테이블 무영향).

근본 fix 후보 (backlog): 기존 마이그레이션 전부를 `prisma migrate resolve --applied <name>` 로 베이스라인 등록 → 이후 `migrate deploy` 정상화. 또는 deploy hook 에 db-push fallback 추가.

## 문서 작업 다음 단계

- [ ] CLI Phase 5 완성 후 runbook 의 restic 명령을 `axe restore` 로 단순화
- [ ] `com.axe.secret-check` / `com.axe.health-check` launchd 구현 후 cloudflared.mdx 갱신
- [ ] partner/registration.mdx 와 frame/docs/ops/onboarding-operator.md §0 일관성 점검
- [ ] customers.yaml 에 blueprint app 슬롯 추가 (현재 frame_mcp 와 vaultwarden 만)
- [ ] Blueprint MEMORY.md 33.5KB → 24.4KB pruning (`~/.claude/projects/-Users-axe-blueprint/memory/MEMORY.md` index 항목 길이 정리, `/ctx review` 별도 세션)

## axelabs 5 표준 (2026-05-26, Truvia 측 요구로 명시화)

운영자 측 분석 (B-customer-deploy-generalization Phase 1 + Truvia D-day 후속 회신 기반) 의 axelabs platform 표준. 각 항목은 decisions.mdx 의 D-config-N 또는 D-ops-N 으로 별도 격상 검토.

| # | 표준 | 본질 | 시행 |
|---|---|---|---|
| 1 | **macmini 운영 패턴 = 기존 자산 ≡ 신규 발주 (동등 지원)** | customer 가 가동 중 macmini 에 platform 도입 시 발주·재설치 의무 없음. 14 컨테이너 기존 가동 + Tailscale 합류 + SSH key 기등록 케이스 (realchoice) 1급 지원. | `/partner/macmini-existing` (신설 예정 — B-docs-ssot-extension), `B-port-conflict-preflight` 가 충돌 게이트 |
| 2 | **Vault repo (`soohunkang/vault.git`) 표준 image = `timshel/vaultwarden:latest`** | Vaultwarden Timshel fork 가 axelabs platform 표준. customer 측이 다른 vault image (e.g. official vaultwarden/server) 사용 시 SSO/sso_nonce/healthcheck/path mount 패턴 모두 다름. Truvia 측 5/26 D-ops-32 검증 = Timshel + collection 구조 정렬. | `/services/vaultwarden` (신설 예정), `image_override` slot 으로 customer-별 version pin 허용 |
| 3 | **bw CLI 표준 version = `2025.7.0`** (D-ops-28 line) | bw 2026.4.x 가 Timshel SSO user 호환성 차단 ("Master password unlock data was not found", trap #12). 2025.7.0 = 최후 호환 release. | `bw-bootstrap.sh` 가 install hint + warning gate. `_svc_step_customer_vault_check` 가 version mismatch warning. backlog `B-bw-cli-version-pinning`. |
| 4 | **Customer-측 self-deploy 일반화 = R1-R5 빌트인** (B-customer-deploy-generalization Phase 1) | (R1) `${HOME}` 변환 — hardcoded `/Users/axe/` 금지. (R2) vault SoT — bw CLI 기반, keychain 의존 폐기. (R3) env_file `$` → `$$` escape — `escape_dollar` flag 자동. (R4) `image_override` slot — customer-별 service repo image pin. (R5) `${CUSTOMER_PREFIX}-` container name 정규화 (Phase 2). | axe CLI `_deploy_service_customer` + wrapper 3종 + customers.yaml schema. |
| 5 | **customers.yaml schema = secret manifest + service_tenant_map + image_override + escape_dollar slots** | customer 메타 + `sso.apps[].client_id` + `services.<svc>.<env_file, secrets[], image_override>` + `service_tenant_map` (D-magnet-tenant-map-1) + `ring_backup` + `entity_meta`. ruamel.yaml round-trip 으로 comment 보존. `axe customers ingest` 가 자동 fill. | `/Users/axe/.axe/customers.yaml` (SSOT), `INGEST_MANIFEST_TEMPLATE` (axe CLI), schema doc = `/architecture/customers` (신설 검토). |

## 세션 의례 (다음 세션 / 다른 세션 모두)

**모든 axelabs.ai 작업 세션은 본 페이지를 의례로 사용**:

1. **세션 시작 시 — READ**: 작업 주제와 관련된 known-gap 이 있는지 본 페이지 확인. 이미 등재된 gap 을 모르고 작업하면 다른 사람의 작업과 중복되거나 충돌 가능.

2. **세션 중 — DISCOVER**: 작업 중 새 gap / 미구현 / 약속 vs 실제 불일치 발견 시 즉시 본 페이지에 한 줄 추가 (해당 섹션에 표 row 추가). 메모리에 들고만 있지 말 것.

3. **세션 종료 시 — WRITE**: 본 세션이 해소한 gap 은 즉시 제거. 새로 발견한 gap 은 등재. 다음 세션의 다른 사람이 본 페이지만 보고도 현재 상태 파악할 수 있어야 함.

4. **페이지가 비어가는 것이 목표**. 단 의도된 deferral (예: "Phase 5 에서 통합") 은 등재 후 phase 표기.

운영자 또는 협업 세션에 본 ritual 이 적용되도록 root `CLAUDE.md` + per-repo `CLAUDE.md` 에 명시.

## 본 페이지의 의도

다음 세션의 본인 (또는 협업자) 이 문서를 보고 작업할 때, **약속된 것** 과 **실제 작동하는 것** 의 격차를 명확히 알도록 함. 격차 자체가 함정이 되지 않게 정리.
