Skip to Content

Blueprint

한 줄 소개: 인간 운영자와 AI 에이전트가 함께 일하는 통합 워크스페이스. Next.js + Claude Agent SDK 로 13종 에이전트 실행, frame · stream · magnet MCP 서버를 도구로 호출.

기술 스택

항목
언어TypeScript
프레임워크Next.js 16 (App Router) + React 19
스타일Tailwind CSS 4 + @axe/ui 디자인 토큰 (/Users/axe/axelabs/src/lib/, sync 됨 → src/app/_axe-ui/)
DBPostgreSQL 16 (Prisma 6.19) — D-config-17 cutover 2026-05-15. SQLite 는 historical (cutover 이전 prisma/data/blueprint.db)
인증NextAuth 4 + Microsoft Entra ID (Azure AD)
에이전트@anthropic-ai/claude-agent-sdk 0.2.92
실시간Socket.IO 4.8 (port 3101 WebSocket)
터미널node-pty 1.1 (PTY)
MCP 클라이언트@anthropic-ai/sdk 0.82 (HTTP/SSE transport)

포트

포트용도
3100Next.js dev / production HTTP
3101Socket.IO WebSocket
3180Caddy 외부 노출 (HTTPS)

디렉토리 구조

/Users/axe/blueprint/ ├── src/ │ ├── app/ Next.js App Router │ │ ├── layout.tsx │ │ ├── page.tsx landing │ │ ├── onboard/ 가입 흐름 │ │ ├── axe/ 대시보드 (admin, agents, issues, projects, skills, team, trinity) │ │ └── api/ 93+ route.ts │ │ ├── auth/[...nextauth] │ │ ├── agents/ │ │ ├── sessions/ │ │ ├── workspaces/ │ │ ├── graph/ Microsoft Graph API proxy │ │ └── teams-graph-* Teams 통합 │ ├── lib/ │ │ ├── auth.ts NextAuth + Azure AD │ │ ├── anthropic-client.ts │ │ ├── customers.ts customers.yaml reader (mtime cache) │ │ ├── mcp/ │ │ │ └── frame.ts frame HTTP MCP 등록, ~21개 tool wrapping │ │ ├── agents/ │ │ │ └── execution-service.ts │ │ └── teams/ │ ├── server/ │ │ └── websocket.ts Socket.IO + PTY (port 3101) │ └── prisma/ │ └── schema.prisma Member / User / Agent / Session / Issue ├── docker/ │ └── docker-compose.yml └── docs/ ├── project-blueprint.md 47KB master doc (5 분과) └── project-blueprint/ ├── division1-org.md ├── division2-tech.md ├── division3-ops.md ├── division4-claude-code.md └── division5-risk-governance.md

데이터 모델 (요약)

Entity (axec / axev / sys — D-bp-entity-3 NOT NULL) ├─ slug UNIQUE (axec | axev | sys | …) ├─ name 한국어 정식 명칭 (hive shared.entity.legal_name mirror, D-bp-entity-4) ├─ kind "corporate" | "system" | "fund" | … ├─ bizNo 사업자등록번호 (D-bp-entity-5, hive mirror) ├─ fiscalYearStartMonth 1-12 (KR default 1) ├─ countryCode ISO 3166-1 alpha-2 (KR default) └─ workspaces[] 1:N Member (인간/에이전트 통합) ├─ User (email, aadObjectId, role: admin|member, │ entityScopes: JSON ["axev","axec"] — D-bp-entity-8 raw 순서, │ defaultEntity: 사용자별 명시적 default — D-bp-entity-7, │ ownedBy: 본인 owner FK (AI agent ↔ human, D-bp-entity-9)) │ └─ EntityRole[] per-(user, entity) 권한 grant (D-bp-entity-17, │ role: admin|owner|member, grantedById audit) └─ Agent (agentId by SDK, memberId) Workspace (PARA row — Project/Area/Resource/Archive) ├─ entityId → Entity (NOT NULL — D-bp-entity-3, General bucket 폐기) ├─ paraLayer ParaLayer enum {PROJECT, AREA, RESOURCE, ARCHIVE} ├─ sourceWorkspaceId / sourceArtifactPath / copiedAt (D-bp-entity-2 fork provenance) └─ Session (mode: chat|terminal|hybrid|agent) ├─ Messages └─ TerminalSessions Issue ├─ assignee:Member └─ reporter:Member AppSetting (key-value, frame_mcp_token 회전 저장) OperatorAlert (D-bp-alert-1, L2 silent-drop 영구 기록) ├─ source 처리 진입점 (e.g. processChatBundle) ├─ chatId / messageId ├─ primaryError / ackError ├─ createdAt └─ acknowledgedAt / acknowledgedBy (NULL = unresolved) MailSendLog (D-bp-mcp-mail-1, 2026-05-29 — send_mail append-only audit; migration 20260529120000_add_mail_send_log) ├─ callerEmail 인증된 caller (MCP token email claim) ├─ sentAs 실제 발신 mailbox (caller, 또는 send-as target) ├─ subject ├─ toCount / ccCount / bccCount / attachmentCount (주소 자체는 미저장) ├─ status "sent" | "dry_run" | "failed" ├─ messageId Graph sendMail 202 no-body → null ├─ httpStatus / error └─ createdAt index: createdAt / (callerEmail, createdAt) McpSchema (D-bp-artifact-1, 2026-05-23 — schema discovery cache) ├─ service ← 'frame' | 'hive' | 'magnet' | 'stream' | … ├─ schemaId ← '[email protected]' (@version suffix 포함) ├─ envelopeVersion ← MCP /schemas envelope.version ├─ body ← JSONB { description, produced_by, key_fields, … } ├─ fetchedAt ← @updatedAt — 매 fetch 마다 갱신 ├─ firstSeenAt ← 첫 발견 시점 └─ @@unique([service, schemaId]) 새 version = 새 row, body 변경 = drift warn OAuth* (D-axe-idp-1, 2026-06-03 — Blueprint = 플랫폼 OIDC Provider; src/lib/platform-oidc.ts) ├─ OAuthAuthCode 단발 authorization code (PKCE S256, 10분 TTL, atomic 소비) ├─ OAuthRefreshToken 회전 refresh token (sha256 hash 만 저장, revokedAt/rotatedTo) └─ OAuthClient RFC 7591 DCR 동적 클라이언트 (loopback/downstream) — axe-cli 는 정적 Artifact (D-bp-artifact-1, 2026-05-23 — typed fact unit) ├─ id ← cuid PK ├─ schemaId ← '[email protected]' or 'freeform.llm@auto' — McpSchema lookup ├─ workspaceId ← Workspace FK (CASCADE delete) ├─ entityId ← cached from Workspace.entityId ├─ paraLayer ← cached from Workspace.paraLayer ├─ content ← JSONB — typed payload per schemaId ├─ citations ← JSONB array — 6-kind discriminated union │ (onedrive / frame.* / hive.* / teams.message / │ mail.thread / external.web) ├─ confidence ← Decimal(3,2) 0.00 ~ 1.00 (default 1.0) ├─ auditTrail ← JSONB array — append-only event log │ (propose / confirm / reject / edit / dispatch / │ archive / restore) ├─ parentArtifactId ← self-FK — sub-Project chain + dispatch fork ├─ createdAt / updatedAt / archivedAt └─ index: workspaceId / (entityId, paraLayer) / schemaId / parentArtifactId / archivedAt ArtifactLink (D-bp-artifact-6, 2026-05-23 — multi-parent reference) ├─ artifactId ← Artifact FK (CASCADE delete) ├─ workspaceId ← Workspace FK (CASCADE delete) ├─ linkType ← 'reference' | 'sub_project' | 'cross_link' ├─ linkedAt ← @default(now()) ├─ linkedById ← User FK (SetNull on user delete — preserve audit) └─ PK (artifactId, workspaceId) 같은 artifact 동일 workspace 중복 link 차단

entity scope flow

User.entityScopes 는 sign-in 콜백 (auth.tshydrateEntityScopesForUser) 가 customers.yaml  user_entity_map + default_entities_by_domain 에서 hydrate. admin 이 customers.yaml 미명시 시 DB 의 모든 Entity slug fallback (lock-out 방지). 각 /api/workspaces/* 라우트가 requireWorkspaceEntityAccess 로 entity gate 검증 (D-bp-entity-3: nullable early-out 제거, 모든 workspace 명시적 entity).

User.defaultEntity (D-bp-entity-7): hydration 시 null + non-empty scopes → scopes[0] 자동 set. scope 외 default → scopes[0] fallback. TopNav 의 fallback chain (D-bp-entity-8) = URL ?entity= → localStorage axe:lastEntitydefaultEntityentityScopes[0] → “all”. createWorkspace fallback = opts.entitySlug → drivePath segment → defaultEntity → entityScopes[0] → throw. 사용자 변경 endpoint = PATCH /api/user/default-entity { slug } (server-side entityScopes.includes(slug) 검증). D-bp-entity-8 정정: hydrate 가 raw customers.yaml 순서 보존 (이전 alphabetic sort 폐기) — yaml list 순서가 진정한 default SOT.

EntityRole (D-bp-entity-17, 2026-05-22)

entityScopes 가 entity 가시성만 표현하는 한계 해결. EntityRole { userId, entitySlug, role: admin|owner|member } 가 entity 별 3-tier 권한 SSOT. hierarchy admin > owner > membersrc/lib/entity-roles.tshasEntityRole(userId, slug, min) / requireEntityRole(...) 로 평가. tenant-wide User.role='admin' (Soohun/ai@) 은 모든 entity 에 implicit admin (row 없어도 bypass — admin fallback 정신 계승).

Hydration 정책: signIn 콜백이 hydrateEntityRolesForUser 호출 — entityScopes 에 새로 들어온 slug 마다 member 행 자동 생성, scope 에서 제거된 slug 의 행은 revoke. owner/admin overlay 는 오직 /axe/admin/users 에서 tenant admin 이 grant (PATCH /api/users/[id]/entity-roles { entitySlug, role: admin|owner|member|none }).

MCP scope 매핑 (내부 API GET /api/internal/entity-roles?email=..., HMAC bearer BLUEPRINT_INTERNAL_API_KEY): admin/owner → hive [read,write,approve,admin] · frame [read,write,close,admin]; member → admin 제외 동일 set; row 없음 → 빈 set (entity 비노출). hive/frame OIDC RP 측 연동은 PR 2/3 에서 별도 결정.

owned account switching (D-bp-entity-9, 2026-05-22)

User.ownedBy — owner (human) → owned (AI agent identity, e.g. ai@/cfo@) 관계. 운영자 1 인이 본인 + agent 여러 identity 운영하는 패턴. switch flow:

  1. owner 가 TopNav 의 사용자 이름 (button) 을 클릭 → popover 메뉴 펼침 (D-bp-entity-12)
  2. owned account 선택 시 POST /api/auth/switch-account { targetUserId } → server-side target.ownedBy === caller.userId 검증
  3. httpOnly cookie axe:active-user-id set + reload
  4. session callback (src/lib/auth.ts): 매 read 마다 cookie 발견 시 ownership 재검증 → session.user.* (id/email/name/role/entityScopes/defaultEntity) 모두 target 로 override. JWT 의 authenticated identity 는 untouched (audit trail 보존)
  5. revert = 메뉴 에서 self 선택 → 동일 endpoint (no-op, dropdown 즉시 close)

/api/auth/owned-accounts GET = dropdown options 채움 (caller + ownedUsers). isImpersonating=true 시 trigger button lime border + label color + tooltip. mid-session ownership revoke 시 자동 revert (매 session read 재검증). admin role 도 bypass 불가 — owned-only.

D-bp-entity-12 (TopNav UX): 사용자 이름 자체가 dropdown trigger (M365/Gmail 패턴). 별도 <select> 폐기. 메뉴 안에 + 다른 계정으로 로그인 옵션 → signIn("azure-ad", ..., { prompt: "select_account" }) 으로 Microsoft 계정 picker 호출. click-outside / Escape close + chevron rotate 120ms. data-testid nav-user-name 유지 (E2E backward compat).

Artifact + PARA 지식 레이어 (📐 설계, M6)

Blueprint 가 PARA OS 로 동작하기 위한 typed fact layer. ctx skill 의 markdown PKM 진화형. 상세 = 아키텍처 페이지.

요지:

  • Artifact = per-field typed fact (Postgres + JSONB). schema_id / content / citations / confidence / paraLayer / entityId / audit_trail / parent_artifact_id
  • Citation = OneDrive driveItemId+version / frame query (entity+account+period+queried_at) / hive employee_id / Teams chatId+msgId / mail threadId+msgId / external URL+fetched_at. data 중복 0
  • PARA dispatch = Project artifact 종료 시 Area/Resource/Archive 로 field-level routing. M3 dispatch (workspace-level) 의 진화
  • ctx 진화 = curation interface (markdown 은 보조). 흐름: agent propose → ctx review → confirmed fact + audit

근거: D-bp-artifact-1~5. 마일스톤 M6.

Schema discovery — frame/hive /schemas mirror (2026-05-23, B-bp-artifact-schema-discovery)

M6 의 첫 implementation slice. Blueprint 가 각 MCP 서비스의 GET /{service}/schemas envelope 을 fetch + cache + version. SoT 는 MCP 측 (src/{frame,hive}/mcp/schemas.py); Blueprint 는 discovery / mirror 만.

  • Prisma 모델: McpSchema { service, schemaId, envelopeVersion, body, fetchedAt, firstSeenAt }. @@unique([service, schemaId]). body 변경 시 fetchedAt 갱신 + drift warning. 신규 schema_id (예: [email protected]) = 새 row
  • 모듈: src/lib/artifact/schemas/ (services / fetcher / registry / types). MCP_SERVICES 에 frame + hive 등재 (확장 시 entry 추가만)
  • Admin API:
    • GET /api/admin/mcp-schemas — 전체 list + count_by_service + registered_services
    • POST /api/admin/mcp-schemas/refresh — fetch trigger (per_service added/changed/unchanged/total 반환)
  • CLI: npx tsx scripts/fetch-mcp-schemas.ts [--service frame] (docker exec / launchd cron 모두 사용 가능)
  • Token: 기존 frame_mcp_token AppSetting 재사용. hive 는 hive_mcp_token AppSetting 또는 HIVE_MCP_TOKEN env (현재 미배치, fetcher 가 auth_failed 로 cleanly skip)
  • 에러 모델: status ∈ ok / auth_failed / network_error / parse_error — drift 진단 surface 깨끗

다음 step: artifact 본체 Prisma migration (B-bp-artifact-prisma) + citation resolver (B-bp-artifact-citation-resolver).

hive ↔ Blueprint entity universe SOT

Sourcerole
~/.axe/customers.yaml entitiesslug list (이 instance 에 활성화된 entity)
hive.shared.entitylegal_name / biz_no / fiscal_year_start_month / country_code SOT (hive register-entity CLI)
Blueprint Entity (이 페이지의 표)hive metadata mirror + kind 분류. seed-entities.ts 가 매 boot upsert. sys 는 Blueprint 전용 (hive 미보유 — ad-hoc Teams 1:1 host)

/api/entities GET = 클라이언트 entity 카탈로그 (entityScopes 권한 검증, TopNav fetch). hive 새 entity 등록 → Blueprint seed-entities.ts 갱신 → 매 boot upsert → 다음 page load 시 클라이언트 자동 인식 (재배포 0).

Blueprint MCP 서버 (D-bp-mcp-1, 2026-05-21)

Blueprint 자신의 read-only MCP 서버. Claude 웹 / 데스크탑 / Code 가 Custom Connector 로 등록 가능.

항목
URLhttps://axe.axelabs.ai/blueprint/mcp
Client ID482598f7-540c-462c-9dfd-b957651eb804
Application ID URIhttps://axe.axelabs.ai/blueprint/mcp
Scopehttps://axe.axelabs.ai/blueprint/mcp/mcp.access
Tenant122fb574-7efa-476a-95b6-bee81bce2cce (axellc.com)
StackPython 3.12 + FastMCP + Starlette + uvicorn (frame/hive 패턴 미러)

22 tools (13 read + 7 calendar + 1 mail write + 1 teams write)

Tool분류설명
whoamiread호출 사용자 (Caller) 정보
list_workspacesread본인 멤버십 워크스페이스
get_workspaceread워크스페이스 상세
list_sessionsread본인 세션 목록
list_issuesread이슈 목록 (필터 가능)
get_issueread이슈 상세
list_agentsread등록된 에이전트
list_skillsread등록된 스킬
search_peopleread멤버 검색 (이름/이메일)
get_teams_messagereadTeams teams.microsoft.com/l/message/... deep link → 본문·발신자·타임스탬프 + 첨부·인용·인라인이미지 ID (D-bp-mcp-teams-1, D-bp-mcp-teams-2)
get_teams_hosted_contentreadTeams 인라인 이미지 (hostedContents) → MCP image content block (Claude vision 직접 활용) (D-bp-mcp-teams-2)
get_teams_attachment_filereadTeams reference 첨부 (OneDrive/SharePoint 공유 파일) contentUrl → text 또는 base64. 50 MB 기본 / 100 MB hard cap (D-bp-mcp-teams-3)
send_teams_messagewriteTeams 채팅에 AXE 봇([email protected]) 신원으로 발송. chat_id/text/content_type/prefix/dry_run. admin 전용 (봇 신원 발신 = broadcast-class). 봇이 멤버인 chat 만 (Graph invariant). 발신 시 humanizer + audit footer 자동. 경로 = /api/teams/post (CRON_SECRET) → sendChatReply (D-bp-mcp-teams-send-1)
list_teams_chatsread최근 Teams chat 목록 (TeamsInboundMessage 로그) — send_teams_message 용 chat_id 탐색. query(preview·sender ILIKE)/limit. chat당 최근 sender·preview·receivedAt. admin 전용 (D-bp-mcp-teams-send-1)
create_eventwrite호출자 본인 M365 캘린더에 이벤트 생성. subject/start/end/location/body/recurrence/attendees (외부 도메인 OK) (D-bp-mcp-calendar-1)
update_eventwrite기존 이벤트 부분 수정 (eventId + 변경 필드)
delete_eventwrite이벤트 삭제 + attendees 에게 cancellation 자동 발송
get_eventread단일 이벤트 fetch by id
list_eventsread시간 윈도우 내 이벤트 목록 (UTC 정렬)
add_attendeeswrite기존 attendees 보존 + 신규 추가 (case-insensitive dedup), Graph 가 초대 자동 발송
find_free_timeread다수 attendee 의 free/busy view (Graph /me/calendar/getSchedule)
send_mailwritecaller 본인 mailbox 에서 임의 수신자(외부 도메인 OK)로 이메일 발송. to/subject/body(html|text)/cc/bcc/attachments(base64)/save_to_sent_items/dry_run + admin as_user_email send-as (D-bp-mcp-mail-1)

Write tools (calendar) — 기본 동작: caller 본인 M365 캘린더에 적용. getClientForUser(callerUserId) 로 caller 의 delegated token (Calendars.ReadWrite) 사용. 외부 도메인 attendee (gmail/naver/personal) = Graph 가 SMTP 초대 메일 처리, 별도 provider 통합 불필요.

Admin send-as (D-bp-mcp-calendar-2): create_event/update_event/delete_event/get_event/list_events/add_attendees 6개 tool 에 as_user_email optional 파라미터. caller 의 Blueprint role === "admin" 인 경우만 작동 — target user 의 캘린더에 write, event organizer = target user 로 표시. 구현 = app-only Graph token (getAppOnlyClient() via client credentials) + /users/{targetUserId}/events endpoint. Azure App Application permission Calendars.ReadWrite + tenant admin consent 필요. 비-admin caller 가 as_user_email 사용 시 403 send_as_forbidden.

사용자 첫 호출 전 1회 re-consent 필요: Calendars.ReadWrite scope 가 2026-05-26 추가됨. 기존 사용자가 cached token 으로 calendar tool 호출 시 graph_token_unavailable / 403 응답 → Blueprint web 에 sign out + sign in 으로 incremental consent. admin 이 tenant-wide consent 시 skip.

Read-only Blueprint domain tools (whoami, list_, get_, search_people) 은 D-bp-mcp-1 의 “MCP 는 조회 surface, Blueprint web UI 가 issue/session 작성 SoT” 원칙 유지. Calendar 는 별개 도메인 (Microsoft Graph) 이므로 write 채택 — D-bp-mcp-1 의 SoT 와 무관.

Mail (send_mail, D-bp-mcp-mail-1): 기본 동작 = caller 본인 mailbox 발송 (getClientForUser delegated token; Mail.Send delegated 는 이미 graph.ts SCOPES 에 존재 → calendar 와 달리 re-consent 불필요). Graph POST /me/sendMail, body = { message: { subject, body, toRecipients, ccRecipients?, bccRecipients?, attachments? }, saveToSentItems }. 외부 도메인 수신자 = Graph 가 SMTP 처리. Admin send-as: as_user_email 지정 시 caller 의 Blueprint role === "admin" 인 경우만 작동 — app-only token (getAppOnlyClient()) + /users/{targetUpn}/sendMail, From = target user. Azure App Application permission Mail.Send + tenant admin consent 필요 (Blueprint Next.js app 2b222356-..., MCP connector app 482598f7-... 아님). 비-admin caller 가 as_user_email 사용 시 403 send_as_forbidden. dry_run=true = 메시지 조립·검증·preview 만 반환 (미발송). Graph sendMail 은 202 no-body → message_id null (Sent 폴더 사본이 durable record). 모든 발송 (sent/dry_run/failed) = append-only MailSendLog 1 row (caller · sentAs · to/cc/bcc count · subject · status · httpStatus) — injection-safety 정책 audit. 에러 코드: send_as_forbidden / recipient_invalid / mail_send_not_consented (Azure 권한 미동의 명시) / graph_error{status, detail}.

Teams tools: get_teams_message 응답 schema: { id, chatId, sender, senderId, timestamp, lastEditedDateTime, messageType, bodyText, bodyHtml, bodyContentType, mentions[], attachments[], inlineImageIds[], quotes[] }. attachments = OneDrive 파일 reference (contentUrl) / Adaptive Card 등. quotes = messageReference 자동 resolve (1-level only — quote-of-quote opaque). inlineImageIds = body HTML 에서 추출한 hostedContent ID 배열 — 각각 get_teams_hosted_content(chatId, messageId, hostedContentId) 로 fetch. attachments[i].contentUrl (contentType=reference) 은 get_teams_attachment_file(contentUrl) 로 본문까지 fetch.

Teams write (send_teams_message · list_teams_chats, D-bp-mcp-teams-send-1, 2026-06-04): Teams 발신을 1급 MCP 도구로 승격 (이전엔 운영자가 내부 API 역설계 + 수작업 DB 조회로만 가능). send_teams_message봇(ai@) 신원으로 발송하므로 calendar/mail send-as 와 동일하게 admin-gated (is_admin(callerUserId) DB 재확인). 봇은 자기가 멤버인 chat 에만 post 가능 (Graph invariant). list_teams_chatsTeamsInboundMessage 로그에서 chat_id 를 찾아준다. 구현 = MCP-only — 새 app route 대신 기존 /api/teams/post (CRON_SECRET, env_file ../.env 로 MCP 컨테이너에 이미 주입됨) 재사용 → blueprint app 무배포 (P3005 마이그레이션 drift + main WIP 회피). 후속: 일관성 위해 /api/internal/teams/send (BLUEPRINT_INTERNAL_API_KEY) + TeamsSendLog audit 로 이전 (app 청정 배포 가능해질 때 — backlog B-bp-mcp-teams-send-internal-route).

세 tool 모두 Blueprint Next.js 의 POST /api/internal/teams/{fetch-message,fetch-hosted-content,fetch-attachment-file} 를 docker 네트워크로 호출 (Bearer BLUEPRINT_INTERNAL_API_KEY 공유 비밀). Graph 호출은 bot identity ([email protected], app perm ChatMessage.Read.All + Sites/Files Read). bot 이 해당 chat 멤버여야 200. 미멤버 chat / share = 403. Channel 컨텍스트 URL 은 501 (follow-up). hostedContent 10 MB 초과 시 413. attachment file 50 MB 기본 (호출 시 maxBytes override 가능, 100 MB hard cap).

데이터 흐름

cloudflared (axe.axelabs.ai/blueprint/mcp) → axe-blueprint-mcp-proxy (Caddy, host:3151) → blueprint-mcp:3000 (docker network alias) → blueprint-mcp-blue:3000 (active) OR blueprint-mcp-green:3000 (passive) = Python+FastMCP+Starlette → JWTAuthMiddleware - Authorization: Bearer <token> - iss-dispatch (unverified peek → full RS256 verify): · iss == BLUEPRINT_ISSUER → verify_blueprint_token (auth_blueprint.py) 플랫폼 토큰 (D-axe-idp-1, `axe login`). Blueprint JWKS, aud=https://axe.axelabs.ai. ← 자기 OP 의 resource server · else → verify_microsoft_access_token (auth_oidc.py) Microsoft Entra access_token (claude.ai Custom Connector). JWKS RS256 + multi-aud (client_id GUID OR app_id_uri) - email claim → "User" 테이블 lookup (Prisma schema raw SQL) — 양 경로 공통 → Caller ContextVar 주입 → FastMCP streamable_http_app → tool handler

운영

  • 컨테이너: blueprint-mcp-blue (host port 3152, compose service mcp-blue, alias blueprint-mcp active) + blueprint-mcp-green (host port 3153, compose service mcp-green, passive). frame frame-mcp-blue/green 패턴 1:1 미러.
  • Proxy: axe-blueprint-mcp-proxy (host port 3151) → docker alias blueprint-mcp → active color.
  • DB 직결: blueprint-postgresUser/Workspace/Session/Issue/Agent/Skill 테이블 read-only
  • 헬스체크: curl http://127.0.0.1:3152/health (blue) · curl http://127.0.0.1:3153/health (green) · curl http://127.0.0.1:3151/health (Caddy → 현재 active)
  • Swap (zero-downtime): docker network disconnect blueprint_default blueprint-mcp-blue && docker network connect --alias blueprint-mcp blueprint_default blueprint-mcp-green && docker exec axe-blueprint-mcp-proxy caddy reload --config /etc/caddy/Caddyfile. cloudflared 비건드림.

Custom Connector 등록 절차

운영자: axe secret send AZURE_BLUEPRINT_MCP_CLIENT_SECRET --service blueprint --to <local-part> → URL 발급 → /api/admin/broadcast-dm 으로 수신자 Teams DM 자동 발사. 전체 흐름은 /architecture/secrets § 사람에게 전달, 수신자 화면 절차 + 함정은 /architecture/mcp-server-checklist § 7 + /onboard/claude-frame-setup (동일 절차, MCP URL 만 다름).

개발 history

frame/hive 와 패턴 동일하지만 byte-by-byte 미러 실패로 5층 양파껍질 사고 (PR #331 ~ #335 누적). 회고는 /architecture/mcp-server-checklist § 10.

Teams bot integration

Microsoft Teams 1:1 / group chat 에서 @AXE AI (또는 @CFO) 멘션 시 봇이 Claude Code 세션을 띄워 응답하는 surface. Web UI 의 chat panel 은 2026-04-25 폐기됨 (architecture-chat-panel-removed) — Teams 가 사용자-facing primary, Web UI 는 admin/observe 전용.

Agent identity

AgentUPN용도
ai[email protected]일반 워크스페이스 dispatcher. 모든 chat 에 기본 멤버. getBotGraphClient() 가 가리키는 레거시 single-bot identity.
cfo[email protected]회계 도메인 (frame MCP 보유). axe CFO 1:1 chat 의 전용 멤버.

각 agent 는 Microsoft Graph user 계정 1 개 + Blueprint Agent row 1 개. getClientForUser(agentUserId) 가 agent 의 delegated token 으로 Graph 인증 — agent 가 chat 의 member 일 때만 인증 성공 (multi-bot 1:1 chat 의 자연 invariant). getBotGraphClient() 는 ai@ 의 shared client 로 레거시 경로 (ai@ 가 chat 멤버일 때만 작동).

Inbound 처리 흐름

  1. Microsoft Graph change-notification → /api/teams/webhookTeamsInboundMessage 인큐
  2. processor: messageType === "message" filter (systemEvent / typing 제외) → 봇 mention 검증 → trigger agent 해결 (mentioned identity)
  3. agent ↔ chat 매핑 검증 (resolveChatMemberAgentUserId) → workspace ACL → 첨부 처리 → claude-invoker
  4. reply: sendChatReply 또는 정정 시 editChatMessage (PATCH in-place edit)
  5. resolve: TeamsInboundMessage.resolvedAt + resolution 마킹

첨부 처리 (src/lib/teams/attachments.ts)

processTriggerAttachments 가 trigger message 의 attachments[] 를 contentType 별 분기:

contentType처리결과 위치
reference (OneDrive/SharePoint 파일)downloadReferenceFile agent token 으로 download. sha256 dedup → 워크스페이스 기존 파일 일치 시 inbox 복사본 제거 후 기존 path 가리킴_teams_inbox/<YYYY-MM-DD>/<filename>
inline image (<img src="hostedContents/.../$value">)downloadHostedContent + content-type sniff → 확장자 추론_teams_inbox/<date>/image_<msgIdShort>_<idx>.<ext>
messageReference (Teams quote/reply)getChatMessage(chatId, refMsgId, agentId) 로 quoted body fetch → HTML → plain text → placeholder 상단에 [인용 메시지] block 으로 inlineLLM prompt only (disk 저장 X). fetch 실패 시 attachment.content.messagePreview 200자 fallback
기타 (card / file 등)[unsupported contentType: X] placeholder (LLM 이 “못 봤다” 답신 위험 — 향후 audit 대상, known-gaps)

multi-bot chat 의 SharePoint URL 은 chat 참여자에게만 read 권한이 grant 되므로 — agent token propagation 이 필수. 레거시 getBotGraphClient 만 쓰면 비참여 chat 에서 403/404 발생 (feedback_multi_bot_attachment_download_gap 2026-05-13 fix).

Reply 안전 정책

  • sanitizeBotReply: markdown + HTML defense-in-depth. system-reminder ban, table → 바이너리 only, dangling cross-reference strip 등 4 종 룰 (architecture_bot_reply_pipeline PR #157 + PR #165).
  • silent-drop defense-in-depth: 사용자 @mention 에 무응답 금지 — 5 종 silent-drop class 별 layer 차단 (feedback_silent_drop_defense_in_depth PR #261/#263/#266).
  • In-place edit: 잘못된 reply 정정 시 softDelete + repost X → editChatMessage PATCH. 수정됨 표시 1개만 (feedback_in_place_edit_over_repost).
  • System prompt vs sanitizer: 시스템 프롬프트 텍스트 룰이 반복 위반될 경우 sanitizer 에 deterministic regex enforcer 추가 (layer 2 fail-safe).

알려진 제약 / 후속

  • Context window messageReference: trigger message 만 quote 처리. TeamsContextMessage (rolling context) 의 quote 는 미해석 — B-bp-teams-context-msgref 후속.
  • Unhandled contentType audit: card / file / 기타 — known-gaps.
  • get_session + list_messages MCP tool 부재: agent transcript 외부 비공개 — Stage 1 전 추가 예정.

Admin Teams API — /api/admin/broadcast-dm

운영자 broadcast / 직원 onboarding 흐름에서 bot identity ([email protected]) 가 1:1 Teams DM 직접 발사. 봇이 @mention 받지 않는 proactive outreach 경로.

필드
POST/api/admin/broadcast-dm (:3100 내부, blueprint.axellc.com 외부)
AuthAuthorization: Bearer $CRON_SECRET (NextAuth 세션 불필요 — admin-only)
Body\{ emails: string[], text: string, contentType?: "text" | "html" \}
흐름each email → User.aadObjectId lookup → POST /chats (oneOnOne, Graph idempotent — 같은 pair 면 같은 chat id) → POST /chats/\{id\}/messages
응답\{ summary: \{total, sent, skipped, error\}, results: [\{email, status, chatId?, messageId?, reason?\}] \}

소스: src/app/api/admin/broadcast-dm/route.ts. 패턴 derivation: src/lib/agents/cron-failure-alert.ts:postAlertToAdmin. 운영자 onboarding 패턴은 /architecture/secrets § 자동 발사 참조.

Agent context reply (사용자 @mention 후 봇 응답) 은 본 엔드포인트 X — mcp__blueprint-graph__graph_chat_message_post MCP tool 사용.

MCP 클라이언트 (frame backend 호출용)

src/lib/mcp/frame.ts:

const frameServerUrl = process.env.FRAME_MCP_URL || 'http://frame-mcp:3710/mcp' const token = await getAppSetting('frame_mcp_token') ?? process.env.FRAME_MCP_TOKEN // HTTP MCP Streamable transport with Bearer

frame-mcp 는 docker network artemis_default 의 alias (frame-mcp-blue 또는 green 으로 swap).

토큰 해결 순서:

  1. AppSetting frame_mcp_token (DB 저장, cron job 으로 회전)
  2. env FRAME_MCP_TOKEN (백업)

customers.yaml 사용

src/lib/customers.ts:

export function getCustomerByPublicDomain(host: string): Customer | undefined { // host: "axe.axelabs.ai" → axe customer } export function userBelongsToCustomer(email: string, customerId: string): boolean { // email "[email protected]" → axellc.com → axe }

마운트: 호스트 /Users/axe/.axe/customers.yaml → 컨테이너 /etc/axe/customers.yaml:ro. mtime 변경 시 자동 재로드.

docker-compose

services: app: build: . (Dockerfile) container_name: blueprint-app ports: - "3100:3000" # HTTP - "3101:3001" # WebSocket - "3102:7681" # ttyd (선택) volumes: - app-data:/app/data - claude-data:/home/nextjs/.claude # skill sync - /Users/axe/.axe/customers.yaml:/etc/axe/customers.yaml:ro env_file: ../.env networks: [artemis_default] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]

환경 변수 (핵심)

# Auth NEXTAUTH_URL=https://axe.axelabs.ai NEXTAUTH_SECRET= # ≥32자 AZURE_AD_CLIENT_ID= # Microsoft Entra 'Blueprint' app AZURE_AD_CLIENT_SECRET= AZURE_AD_TENANT_ID= # customers.yaml 의 axe.sso.tenant_id # Blueprint MCP — 플랫폼 토큰 (D-axe-idp-1) resource-server 신뢰. # Blueprint MCP 가 자기 OP 가 발행한 `axe login` 토큰을 받게 한다 (미설정 시 # Microsoft Entra 경로만 → 자기 플랫폼 토큰을 unknown_kid 로 401, 2026-06-04 fix). # JWKS 는 issuer 에서 파생({issuer}/.well-known/jwks.json). 주권 테넌트는 자기 URL 로. BLUEPRINT_ISSUER=https://blueprint.axellc.com # = OP issuer (NEXTAUTH_URL) BLUEPRINT_AUDIENCE=https://axe.axelabs.ai # 플랫폼 단일 audience # Database — D-config-17 cutover 후 (2026-05-15~) Postgres. # ⚠️ D-bp-mcp-3 (2026-05-22): `.env` 에 DATABASE_URL 명시 X 권고 — compose 의 # `${DATABASE_URL:-postgresql://...}` default 가 모든 service (app + mcp blue/green) # 에 일관 적용. .env 에 SQLite legacy `file:./...` 잔존 시 blueprint-mcp 가 # SQLAlchemy parse fail 로 모든 OAuth 요청 silent 500 (8h 종일 broken incident). # mcp/config.py 가 postgresql:// → postgresql+asyncpg:// 자동 변환. DATABASE_URL=postgresql://blueprint:${BLUEPRINT_DB_PASSWORD}@postgres:5432/blueprint BLUEPRINT_DB_USER=blueprint # docker-compose default BLUEPRINT_DB_PASSWORD= # vault-pulled (D-ops-17) BLUEPRINT_DB_NAME=blueprint # Multi-tenant CUSTOMERS_YAML_PATH=/etc/axe/customers.yaml # MCP FRAME_MCP_URL=http://frame-mcp:3710/mcp FRAME_MCP_TOKEN= # AppSetting fallback # Claude CLAUDE_CODE_OAUTH_TOKEN= # 또는 ANTHROPIC_API_KEY

Admin vs Settings 경계

/axe/admin/* ← 조직 수준 (멤버 관리, 권한 부여, 감사 로그) /axe/settings ← 개인 수준 (개인 토큰, 알림 환경설정)

Admin 라우트는 User.role = 'admin' 만 접근.

디자인 시스템 (@axe/ui, D-bp-ui-1, 2026-05-22)

/Users/axe/axelabs/src/lib/{tokens,styles}/ SSOT 의 디자인 토큰 + 컴포넌트 스타일을 Blueprint 의 src/app/_axe-ui/ 로 sync. axelabs 메인 앱 · docs.axelabs.ai 와 동일한 토큰 (claret/cream/neon/noir 12-step) 사용.

동작명령
SSOT 가져오기npm run sync-axe-ui (axelabs HEAD + src/lib/ 마지막 변경 SHA 를 VERSION 에 기록)
변경 감지 (dry-run)npm run check-axe-ui — sync 후 stat 보여주고 revert
자동 syncpredev / prebuild hook 이 매 npm run dev/build 마다 실행. Docker 빌드 안에서는 axelabs 경로 부재 → graceful skip (committed 산출물 사용).

Blueprint 전용 패치 2종 (scripts/sync-axe-ui.mjs)

  1. 목적지 경로 — canonical (axelabs-docs) 는 app/_axe-ui/. Blueprint 는 src/ source root 라 src/app/_axe-ui/.
  2. fonts.css@import url(...) strip — Tailwind 4 + Turbopack 이 토큰 파일들을 globals.css 로 inline 한 뒤 fonts.css 의 @import url(pretendard/d2coding) 이 다른 rule 뒤로 밀려 CSS spec 위반으로 브라우저가 silent drop. sync 단계에서 strip → Pretendard/D2Coding 은 layout.tsx<link rel="stylesheet"> 로 로드 (SSOT 가 Clash Display 에 대해 이미 인정한 회피 패턴의 확장).

Legacy var alias bridge (src/app/globals.css)

35+ tsx 가 var(--navy) · var(--white) · var(--gray-ax) · var(--dark-border) 등 기존 토큰을 직접 사용. 비파괴 채택을 위해 globals.css 에서 alias:

--navy: var(--bg-base); /* light: #f5f1e8 / dark: #1a0610 */ --white: var(--text-primary); /* light: #1f1a13 / dark: #f2e8ee */ --gray: var(--text-muted); --dark-border: var(--border-subtle); /* ...등 */

data-theme="dark" 토글 시 기존 tsx 무수정으로 자동 전환. 향후 별도 PR 로 inline ref 를 새 토큰명 (var(--bg-base) 등) 으로 점진 마이그레이션.

테마 토글

<html lang="en" data-theme="dark"> 기본. 라이트 모드 전환은 document.documentElement.setAttribute("data-theme", "light") — 토글 UI 는 별도 후속. SSOT colors.css 가 [data-theme="…"] + .light/.dark 양쪽 지원 (next-themes 호환 — [architecture/decisions D-axe-ui-1](/ops/decisions) 참조).

외부 의존성

시스템통합 지점
Microsoft Entra IDNextAuth provider + Graph API
Microsoft GraphMail, Chat, Files, Teams 권한
frame MCPHTTP MCP (회계 도구)
Claude APIAnthropic SDK (에이전트 실행)
Cloudflare Tunnelexternal 노출
jsDelivr CDNPretendard + D2Coding 폰트 (CSP style-src + font-src 허용)
Fontshare CDNClash Display 폰트 (디스플레이 헤딩 옵션)

운영 노트

  • 세션 maxAge: 365일 (NextAuth)
  • Cookie: __Secure-next-auth.session-token (prod)
  • Skill sync: claude-data:/home/nextjs/.claude 볼륨, git-based SOT
  • PTY: max buffer 2000 lines per session
  • CORS: WebSocket origin allowlist (src/server/websocket.ts L180-183)

관련 문서

Last updated on