트렌딩 딥다이브 · 2026-07-18 · TrendShift Featured / OSSInsight #37

oomol-lab/open-connector 딥다이브
— AI 에이전트에게 "열쇠를 주지 않고" 1,000개 SaaS를 열어주는 게이트웨이

OpenConnector사용자가 자기 SaaS 계정(GitHub·Gmail·Notion·Slack·Supabase…)을 한 번 연결해 두면, AI 에이전트가 그 계정으로 실제 작업을 실행할 수 있게 해주는 오픈소스 커넥터 게이트웨이다. 상용 제품 Composio의 오픈소스 대안을 표방한다. 핵심 설계는 단순하고 강력하다 — 자격증명(토큰·API 키)은 게이트웨이 안에만 남고, 에이전트는 "무엇을 할 수 있는지"의 스키마와 실행 결과만 받는다. 저장소에는 프로바이더 1,097개 디렉터리, TypeScript 파일 4,078개(그중 4,040개가 프로바이더 코드), 테스트 58개가 들어 있다. 라이선스는 Apache-2.0, 버전 1.3.0(2026-07-17 커밋 기준). 기술적으로 재미있는 지점은 "1,000개 프로바이더를 어떻게 죽지 않고 로딩하는가"와 "에이전트가 시키는 대로 아무 URL이나 때리지 않게 어떻게 막는가" — 지연 로딩 레지스트리와 SSRF 방어 fetch가 이 저장소의 진짜 볼거리다.

(저장소: oomol-lab/open-connector · TypeScript · Apache-2.0 · Node.js 22+ / Cloudflare Workers 양쪽 실행 · MCP 지원)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — Composio 대안이 필요한 이유
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 게이트웨이 경계와 5가지 설계 패턴
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 여기서 훔쳐올 설계
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

한 문장으로 압축하면

"에이전트에게 API 키를 넘기지 말고, 대신 '이 계정으로 이 동작을 실행해 줘'라고 부탁할 창구를 하나 세워라" — 그 창구를 1,000개 서비스 분량으로 미리 만들어 둔 오픈소스 런타임이다.

용어
커넥터 게이트웨이 (connector gateway)
여러 외부 서비스로 나가는 호출을 한 곳으로 모아 인증·권한·로깅·정책을 일괄 처리하는 중간 서버. 각 앱이 제각각 토큰을 들고 다니는 대신, 게이트웨이가 토큰을 보관하고 "대신 호출"해 준다.
비유로 먼저

호텔 컨시어지 데스크

투숙객(=AI 에이전트)이 "203호 금고 좀 열어줘", "세탁 맡겨줘", "택시 불러줘"라고 요청한다. 컨시어지(=OpenConnector)는 마스터키를 자기 서랍에 두고 요청을 대신 처리한 뒤 결과만 알려준다.

투숙객은 마스터키를 만져본 적이 없다. 그래서 투숙객이 실수하거나 나쁜 마음을 먹어도 피해 범위가 "컨시어지가 허용한 업무 목록" 안으로 제한된다. 게다가 컨시어지는 모든 요청을 장부(run log)에 남기고, "이 투숙객은 금고는 못 열고 세탁만 가능" 같은 규칙(action policy)도 적용한다.

OpenConnector가 하는 일이 정확히 이것이다. 다른 점은 마스터키가 1,000개 서비스분이라는 것뿐.

구체적으로는 네 가지 입구를 제공한다. SDK(앱 코드에서 호출), oo CLI(로컬 에이전트 릴레이), MCP(Claude 같은 에이전트 호스트가 붙는 표준 프로토콜), HTTP/OpenAPI(그 외 모든 클라이언트). 그리고 브라우저에서 쓰는 Web Console이 관리·디버깅용으로 함께 들어 있다.

# 인증이 필요 없는 액션으로 런타임이 살아있는지 확인
curl -s -X POST http://localhost:3000/v1/actions/hackernews.get_top_stories \
  -H 'content-type: application/json' \
  -d '{"input":{}}'

# GitHub 개인 액세스 토큰을 게이트웨이에 "보관"시킨다 (에이전트에겐 안 준다)
curl -s -X PUT http://localhost:3000/api/connections/github \
  -H 'content-type: application/json' \
  -d '{"authType":"api_key","values":{"apiKey":"github_pat_..."}}'

# 이제 에이전트는 토큰 없이 액션 ID만으로 실행한다
curl -s -X POST http://localhost:3000/v1/actions/github.get_current_user \
  -H 'content-type: application/json' \
  -d '{"input":{}}'

2왜 주목받는가

Composio 대안이 필요해진 배경, 그리고 이 구현이 고른 차별점

배경 — 2026년 에이전트 개발의 병목은 "연결"이다

LLM 자체는 이제 충분히 똑똑하다. 문제는 에이전트가 실제로 뭔가를 하려면 사용자의 Gmail·Notion·Slack에 접근해야 하는데, 그 인증을 붙이는 일이 지루하고 위험하다는 것이다. 서비스마다 OAuth 흐름이 다르고, 토큰 갱신 주기가 다르고, 스코프 이름이 다르다. 프로바이더 10개만 붙여도 인증 코드가 앱 코드를 잡아먹는다.

Composio 같은 상용 서비스가 이 문제를 대신 풀어주면서 빠르게 성장했다. 그런데 상용 SaaS에 사용자의 모든 앱 자격증명을 위탁하는 구조는 보안·컴플라이언스 관점에서 부담이 크다. "자체 호스팅 가능한 같은 것"에 대한 수요가 자연스럽게 생겼고, OpenConnector가 그 자리를 노린다.

차별점 5가지

차별점 1
Apache-2.0 · 완전 자체 호스팅

docker compose up 한 줄이면 로컬에서 전부 돈다. 데이터는 SQLite 파일 하나(/app/data 볼륨)에 들어간다. 클라우드 계정도, 회원가입도 필요 없다.

차별점 2
Node와 Cloudflare Workers를 동시에 지원하는 이중 런타임

같은 코드베이스가 Node 서버(SQLite 저장)Cloudflare Workers(D1 + R2 저장) 양쪽에서 돈다. src/server/storage/sqlite-runtime-store.tsd1-runtime-store.ts가 나란히 있고, 비밀 암호화도 Node용 secret-codec.ts(node:crypto)와 Worker용 worker-secret-codec.ts(WebCrypto)로 나뉜다. 서버리스 무료 티어로도 굴릴 수 있다는 뜻이다.

차별점 3
MCP를 1급 시민으로 취급

POST /mcp 하나만 에이전트 호스트에 등록하면 끝난다. 노출하는 MCP 툴은 딱 4개 — list_apps, search_actions, get_action_guide, execute_action. 액션 1만 개를 툴 1만 개로 쏟아붓지 않고 "검색해서 찾아 쓰는" 디스커버리 방식을 택한 게 핵심 설계 판단이다(→ 4장에서 자세히).

차별점 4
검사 가능한 액션 계약(contract)

모든 액션이 입력 스키마 · 출력 스키마 · 필요 스코프 · 실행기 소스를 공개한다. 에이전트가 "이 액션을 부르면 정확히 뭐가 필요하고 뭐가 돌아오는지"를 실행 전에 알 수 있다. 블랙박스 툴 호출이 아니다.

차별점 5
규모 — 프로바이더 1,097개가 이미 들어 있음

src/providers/ 아래에 실제로 1,097개 디렉터리가 있다. GitHub·Gmail·Notion·BigQuery·Slack 같은 대형 서비스부터 geocodio, kraken_io, alpha_vantage, coinmarketcap, tencent_maps 같은 틈새 API까지. "쓸 만한 게 있나"를 걱정할 단계는 지났다.

냉정하게
과장하지 말아야 할 부분

"1,000+ 프로바이더"가 곧 "1,000개 전부 로컬 실행 가능"은 아니다. 액션 정의에는 execution.locallyExecutable 플래그가 있고, 이게 false면 로컬 실행기 없이 카탈로그에만 존재한다. 실제 커버리지는 프로바이더마다 편차가 크다.

또 OAuth를 쓰려면 각 서비스에 본인 명의 OAuth 앱을 직접 등록해야 한다. 이건 자체 호스팅의 숙명이다. README도 "OAuth 승인 대기 때문에 막힌 팀"에게는 호스팅 옵션(OOMOL)을 권하는데, 이 지점이 오픈소스 버전의 실질적 진입 장벽이다.

3기술 스택 전체 지도

package.json · Dockerfile · web/package.json을 실제로 뜯어본 결과

백엔드 (런타임 코어)

영역선택왜 이걸 골랐는지
런타임Node.js 22+ / Cloudflare WorkersDocker 이미지는 node:24-alpine. Workers도 타깃이라 Node 전용 API 사용이 코드 전반에서 격리돼 있다.
언어TypeScript 7빌드 산출물 없이 node src/server/index.ts.ts를 직접 실행한다(Node의 네이티브 TS 실행). 번들러 단계가 사라져서 배포가 단순해진다.
HTTP 프레임워크Hono 4Web 표준 Request/Response 기반이라 Node와 Workers에서 같은 라우터 코드가 돈다. 이중 런타임 지원의 핵심 전제.
스키마·검증zod 4 + @cfworker/json-schemazod는 내부 타입 검증, @cfworker/json-schema는 액션 입출력의 JSON Schema 검증. Workers에서도 도는 순수 JS 구현이 필요해서 후자를 별도로 쓴다.
에이전트 프로토콜@modelcontextprotocol/sdk 1.29MCP 서버 구현. stateless POST JSON-RPC만 지원(SSE 스트림 유지 안 함) — 서버리스 친화적 선택.
액션 검색minisearch 7액션 1만 개 중에서 search_actions로 찾아내는 전문 검색. 외부 검색 엔진 없이 인메모리로 해결.
인증·토큰jose 6JWT 액세스 토큰 검증(JWKS URI 기반). 런타임 토큰과 공존.
로깅pino 10구조화 JSON 로그. 실행 로그는 별도로 DB에 적재된다.
저장소SQLite / Cloudflare D1마이그레이션 SQL 6개(migrations/0001~0006)를 양쪽이 공유한다.
파일 전송R2 / KV / 로컬액션이 파일을 주고받을 때 쓰는 transit file 저장소. 3가지 백엔드 구현이 같은 인터페이스를 만족.
메일imapflow · nodemailer · mailparserGmail 외 일반 IMAP/SMTP 계정도 프로바이더로 붙일 수 있게 하는 전용 모듈(src/mail/).
린트·포맷oxlint 1.71 · oxfmtRust로 작성된 초고속 툴체인. 파일 4,000개짜리 저장소에서 ESLint는 현실적으로 느리다 — 규모가 도구 선택을 강제한 사례.
테스트vitest 4*.test.ts를 소스 옆에 두는 co-location 방식. 58개.
용어
MCP (Model Context Protocol)
AI 에이전트가 외부 도구·데이터에 접근하는 방식을 표준화한 프로토콜. USB-C 포트에 비유하면 이해가 빠르다 — 도구마다 다른 연결 방식을 만들지 말고, 하나의 규격으로 꽂자는 것.

프론트엔드 (Web Console)

영역선택메모
UI 라이브러리React 19최신 메이저. 별도 상태 관리 라이브러리 없음.
빌드Vite 8개발 서버는 5173, API는 3000으로 프록시.
스타일Tailwind CSS 4 (+ @tailwindcss/vite)CSS-first 설정을 쓰는 v4 방식.
컴포넌트Radix UI 11종 + CVA + tailwind-merge사실상 shadcn/ui 구성. web/src/components/ui/에 배치.
라우팅react-router 7콘솔이 SPA로 동작.
차트recharts 3Overview 페이지의 호출 추이 그래프.
상태·i18n@embra/reactivity · @embra/i18nOOMOL이 만든 자체 경량 라이브러리. web/src/locales/에 다국어 문자열.
아이콘lucide-react + @iconify-json/logos후자는 1,000개 프로바이더의 브랜드 로고 표시용.

인프라 · 배포

경로구성특징
Docker멀티스테이지 node:24-alpinebuild 스테이지에서 generate:catalog + 웹 빌드 → 런타임 스테이지는 dev 의존성 제외. HEALTHCHECK로 30초마다 자체 점검.
Fly.iofly.toml + 영속 볼륨Node Docker 런타임 + 볼륨 위 SQLite. TLS·헬스체크 포함.
CloudflareWorkers + D1 + R2 + Static Assetswrangler.example.jsonc를 복사해 로컬 설정 생성 → npm run deploy:cloudflare. 콘솔은 Static Assets로 서빙.
CIGitHub Actions.github/workflows/. GHCR로 ghcr.io/oomol-lab/open-connector:latest 배포.
스택을 한 줄로

"Web 표준으로만 짜서 Node든 Workers든 어디든 던져 넣을 수 있게 한다"가 이 저장소의 스택 선택 전체를 관통하는 원칙이다. Hono를 고른 것도, JSON Schema 검증기를 순수 JS 구현으로 고른 것도, 암호화 모듈을 두 벌 만든 것도 전부 같은 이유에서 나온 결정이다.

4아키텍처 심화 분석

시스템 구조도 + 이 저장소가 실제로 푼 5가지 어려운 문제

전체 구조도

┌──────────────────────────────────────────────────────────────────────┐ │ 호출자 (Callers) │ │ │ │ [AI 에이전트] [앱 코드] [oo CLI] [브라우저] │ │ │ MCP │ SDK │ relay │ Web Console │ └────────┼─────────────┼─────────────┼──────────────┼───────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ POST /mcp POST /v1/... POST /v1/... GET / · /api/* └─────────────┴─────────────┴──────────────┘ │ ╔═════════════▼══════════════════════════════════════╗ ║ Hono 라우터 (src/server/api/runtime-api.ts) ║ ║ · 인증: 런타임 토큰 / JWT / 관리자 토큰 ║ ║ · 응답 봉투: {success, message, data, meta} ║ ╚═════════════╤══════════════════════════════════════╝ │ ┌─────────────▼───────────────────────────────┐ │ ActionRunner (모든 호출자의 공통 실행 경계) │ │ src/server/actions/action-runner.ts │ └──┬────────┬────────┬────────┬────────┬──────┘ │ │ │ │ │ ┌────────▼──┐ ┌───▼────┐ ┌─▼──────┐ ┌▼──────┐ ┌▼─────────┐ │ Catalog │ │Policy │ │Conn. │ │Idem- │ │ Run Log │ │ 액션 조회 │ │허용/차단│ │자격증명 │ │potency│ │ 감사 기록 │ └────────┬──┘ └───┬────┘ └─┬──────┘ └───────┘ └────┬─────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌───────────────┐ ┌──────────────────┐ │ │ │ SecretCodec │ │ RuntimeStore │ │ │ │ AES-256-GCM │ │ SQLite 또는 D1 │ │ │ └───────────────┘ └──────────────────┘ ▼ ▼ ┌────────────────────────────────────────────┐ │ ProviderLoader (지연 로딩) │ │ 액션 실행 시점에만 해당 프로바이더 모듈 import │ └────────────────────┬───────────────────────┘ │ ┌──────▼───────┐ │ guardedFetch │ ◀── SSRF 방어선 │ · 사설 IP 차단 │ │ · DNS 재검증 │ │ · 리다이렉트 시 │ │ 인증헤더 제거 │ └──────┬───────┘ ▼ ══════════ 신뢰 경계 (trust boundary) ══════════ ▼ [ GitHub ] [ Gmail ] [ Notion ] ... 1,097개

그림에서 가장 중요한 선은 아래쪽 신뢰 경계다. 위쪽(에이전트·앱)은 자격증명을 한 번도 만지지 않는다. 아래쪽(외부 API)으로 나가는 순간에만 SecretCodec이 복호화한 토큰이 헤더에 실린다. 그리고 그 나가는 길목에 guardedFetch가 서 있다.

설계 패턴 ① — 지연 로딩 레지스트리 (프로바이더 1,097개를 살려내는 법)

문제
모든 프로바이더를 import하면 서버가 못 뜬다

프로바이더 4,040개 TS 파일을 시작 시점에 전부 로드하면 Node에서도 느리고, Cloudflare Workers에서는 번들 크기 제한과 CPU 시간 제한에 그냥 걸려 죽는다. 그런데 카탈로그(=목록)는 시작 즉시 필요하다.

해법
"목록"과 "실행기"를 완전히 분리한다

목록은 빌드 시점에 scripts/generate-catalog.ts가 정적 JSON(catalog/apps/*.json)으로 미리 뽑아 둔다. 런타임은 이 JSON만 읽는다 — TS 모듈을 import하지 않는다.

실행기ProviderLoader가 액션이 실제로 호출될 때 그 서비스 하나의 모듈만 동적 import한다.

// src/providers/provider-loader.ts (주석 발췌 — 의도가 명시돼 있다)
/**
 * Loads provider executor modules only when an action is executed.
 *
 * Provider definitions are intentionally not exposed here. Runtime catalog
 * reads should use generated `catalog/apps/*.json` instead of importing
 * hundreds of provider definition modules at startup.
 */
export interface IProviderLoader {
  loadActionExecutor(service, actionId, providerDisplayName?): Promise<ActionExecutor | undefined>;
  loadProxyExecutor(service, providerDisplayName?): Promise<ProviderProxyExecutor | undefined>;
  loadCredentialValidators(service): Promise<CredentialValidators | undefined>;
}
비유

도서관 카드 목록함과 같다. 어떤 책이 있는지 알려면 서가 전체를 뒤질 필요 없이 색인 카드만 보면 된다(=생성된 catalog JSON). 실제로 읽을 책이 정해지면 그때 그 한 권만 서가에서 꺼내 온다(=동적 import).

도서관 전체를 매일 아침 책상 위에 올려두는 짓을 안 하는 것 — 이게 1,097개 프로바이더를 감당하는 유일한 방법이다.

설계 패턴 ② — SSRF 방어선 guardedFetch

이 저장소에서 가장 배울 게 많은 파일이다. 소스 14.5KB, 테스트 18KB — 테스트가 구현보다 크다는 것 자체가 이 코드의 위험도를 말해준다.

용어
SSRF (Server-Side Request Forgery)
공격자가 서버를 속여서 서버 자신만 접근할 수 있는 내부 주소로 요청을 보내게 만드는 공격. 예를 들어 클라우드의 메타데이터 주소(169.254.169.254)를 때리게 만들면 서버의 클라우드 자격증명이 통째로 유출된다.

왜 이 게이트웨이가 특히 위험한가? URL의 일부가 사용자 입력에서 온다. 프로바이더 중에는 API 베이스 URL을 사용자가 직접 설정하는 것들이 있고(예: clickhouse, dataforseo), 에이전트가 만든 파라미터가 경로에 들어간다. 즉 "LLM이 지어낸 문자열이 서버의 아웃바운드 요청 주소가 될 수 있는" 구조다.

방어 계층 3중

계층무엇을 하는가왜 이것만으론 부족한가
1. URL 검사assertPublicHttpUrl — 스킴 확인, 사설/예약/루프백/링크로컬 IP 리터럴 차단호스트명이 evil.com인데 DNS가 127.0.0.1을 반환하면 통과해 버린다(DNS rebinding).
2. DNS 재검증요청 직전 실제 해석된 주소를 isBlockedIpAddress로 다시 검사비용이 든다. 그래서 호스트가 코드에 고정된 경우 skipDnsValidation으로 끌 수 있게 했다.
3. 리다이렉트 추적최대 20홉까지 Location 헤더를 직접 검사하며 따라간다플랫폼 기본 redirect:"follow"에 맡기면 리다이렉트 목적지를 검사할 기회가 없다.

세 번째 계층에서 나오는 디테일이 특히 좋다. 교차 출처(cross-origin) 리다이렉트가 발생하면 인증 헤더를 전부 떼어낸다.

// src/core/guarded-fetch.ts — 명시적 allowlist로 관리한다
const crossOriginCredentialHeaders = new Set([
  "authorization", "proxy-authorization", "cookie",
  "api-key", "apikey", "x-api-key", "x-apikey",
  "api-token", "x-api-token",
  "auth-token", "x-auth-token", "x-auth-key",
  "access-token", "x-access-token", "app-key",
  // ...
]);
설계 감상
"이름 패턴"이 아니라 "명시적 목록"을 쓴 이유

주석에 이유가 적혀 있다 — x-*token* 같은 정규식으로 뭉뚱그리면 idempotency-keyx-correlation-id처럼 인증과 무관한데 이름만 비슷한 헤더까지 잘려나간다. 그래서 "새 프로바이더가 새로운 인증 헤더를 쓰면 여기 추가하라"는 유지보수 부담을 감수하고 allowlist를 골랐다.

보안 코드에서 "편한 정규식" 대신 "귀찮은 명시 목록"을 고르는 판단은 그대로 배워둘 만하다. 오탐(false positive)이 조용한 버그를 만드는 영역에서는 명시성이 항상 이긴다.

설계 패턴 ③ — 액션 정의를 "선언"으로 만드는 팩토리

프로바이더 1,097개를 일관된 모양으로 유지하려면 보일러플레이트를 줄여야 한다. defineProviderAction이 그 역할을 한다.

// src/core/provider-definition.ts
export function defineProviderAction<TName extends string>(
  service: string,
  input: DefineProviderActionInput<TName>,
): ProviderActionDefinition<TName> {
  return {
    id: `${service}.${input.name}`,   // github + get_issue → "github.get_issue"
    service,
    name: input.name,
    description: input.description,
    requiredScopes: input.requiredScopes ?? [],
    providerPermissions: input.providerPermissions ?? [],
    inputSchema: input.inputSchema,
    outputSchema: input.outputSchema,
    followUpActions: input.followUpActions,
    asyncLifecycle: input.asyncLifecycle,
  };
}

주목할 필드가 둘 있다. followUpActions"이 액션 다음에 보통 뭘 하게 되는지"를 에이전트에게 힌트로 준다 — 예를 들어 이슈를 만들면 다음은 라벨 붙이기. asyncLifecycle은 오래 걸리는 작업(영상 인코딩 등)의 폴링 계약을 표현한다. 둘 다 "에이전트가 스스로 다음 수를 두게" 돕는 메타데이터다.

프로바이더 정의 자체는 놀랍도록 짧다. GitHub 전체가 이 정도다:

// src/providers/github/definition.ts (전문에 가깝게)
export const provider: ProviderDefinition = {
  service: "github",
  displayName: "GitHub",
  categories: ["Developer Tools"],
  authTypes: ["oauth2", "api_key"],
  auth: [
    { type: "oauth2",
      authorizationUrl: "https://github.com/login/oauth/authorize",
      tokenUrl: "https://github.com/login/oauth/access_token",
      scopes: githubOAuthScopes,
      tokenEndpointAuthMethod: "client_secret_post" },
    { type: "api_key",
      label: "Personal access token",
      placeholder: "github_pat_...",
      description: "...Fine-grained tokens are recommended." },
  ],
  homepageUrl: "https://github.com",
  actions: githubActions,
};

파일 분할 규칙도 일관적이다 — definition.ts(메타), actions.ts(스키마), executors.ts(자격증명 배선), runtime-*.ts(도메인별 실제 호출), scopes.ts(스코프 상수). GitHub은 runtime-issue.ts, runtime-pull-request.ts, runtime-release.ts, runtime-search.ts, runtime-activity.ts, runtime-repository.ts로 도메인을 쪼갰다.

설계 패턴 ④ — 정책 엔진: allow와 block은 서로를 모른다

// src/core/action-policy.ts
evaluate(action: ActionDefinition): ActionPolicyDecision {
  // 1) 차단 목록이 먼저 — 항상 이긴다
  if (this.blocked.some((m) => m(action.id)))
    return { allowed: false, code: "action_blocked", ... };

  // 2) 허용 목록이 비어있지 않으면, 거기 없는 건 전부 거부
  if (this.allowed.length > 0 && !this.allowed.some((m) => m(action.id)))
    return { allowed: false, code: "action_not_allowed", ... };

  return { allowed: true };
}

규칙 세 가지가 명확하다. ①차단이 허용을 이긴다. ②허용 목록이 비어 있으면 "제한 없음"이다(빈 목록 = 전부 거부가 아니다). ③액션 정책과 프록시 정책은 완전히 독립적이다 — 소스 주석이 "Neither pair reads the other"라고 못박아 뒀다.

패턴 매칭은 딱 세 가지만 지원한다 — *(전부), github.*(서비스 단위), 정확히 일치. 정규식을 허용하지 않은 것도 판단이다. 보안 정책 문법이 표현력을 얻으면 그만큼 "의도와 다르게 매칭되는" 사고가 늘어난다.

# docker-compose.yml 환경변수로 바로 설정된다
OOMOL_CONNECT_ALLOWED_ACTIONS=github.*,gmail.get_message
OOMOL_CONNECT_BLOCKED_ACTIONS=github.delete_repository
실전 팁
에이전트에게 처음 붙일 때의 안전한 기본값

읽기 액션만 allowlist에 넣고 시작하라. github.delete_repository 같은 파괴적 액션은 allowlist에서 빠뜨리는 것에 의존하지 말고 blocklist에 명시하는 게 낫다. 나중에 allowlist를 github.*로 넓혀도 차단이 유지되기 때문이다.

설계 패턴 ⑤ — 멱등성 키로 "재시도 = 중복 실행"을 막는다

용어
멱등성 (idempotency)
같은 요청을 몇 번 보내도 결과가 한 번 보낸 것과 같은 성질. 엘리베이터 버튼을 다섯 번 눌러도 엘리베이터가 다섯 대 오지는 않는 것.

에이전트는 타임아웃이 나면 그냥 다시 시도한다. 그런데 "이슈 생성"이나 "메일 발송"에서 재시도는 중복을 만든다. OpenConnector는 HTTP 표준 방식으로 해결한다:

IDEMPOTENCY_KEY=$(openssl rand -hex 16)

curl -s -X POST http://localhost:3000/v1/actions/github.create_issue \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'content-type: application/json' \
  -d '{"input":{"owner":"me","repo":"x","title":"버그"}}'

키가 없으면 매번 정상 실행된다(기본값이 안전한 쪽이 아니라 예측 가능한 쪽이다). 키는 공백 제거 후 비어있지 않아야 하고 255 UTF-8 바이트 이하여야 하며, 위반 시 400 invalid_input. 저장은 migrations/0003_action_idempotency.sql의 전용 테이블에서 담당한다.

보너스 — MCP 툴을 4개로 줄인 결정

문제
액션 1만 개를 MCP 툴 1만 개로 노출하면?

에이전트의 컨텍스트 윈도우가 툴 정의만으로 터진다. 툴이 많아질수록 모델이 잘못된 툴을 고를 확률도 올라간다. 이건 MCP를 쓰는 모든 대규모 통합이 부딪히는 벽이다.

해법
"툴 목록"이 아니라 "검색 인터페이스"를 준다

노출하는 툴은 넷뿐이다 — list_apps(앱 목록), search_actions(액션 검색), get_action_guide(한 액션의 사용법 문서), execute_action(실행).

에이전트의 작업 흐름은 이렇게 바뀐다: "깃허브 이슈 만들고 싶다" → search_actions("create issue") → get_action_guide("github.create_issue")로 스키마 확인 → execute_action. 툴 정의 대신 검색 능력을 준 것이다.

GET /api/actions/:actionId/agent.md가 액션별 마크다운 가이드를 뽑아주는 것도 같은 철학 — 에이전트가 읽을 문서를 런타임이 생성한다.

5디렉토리 구조 해부

어디부터 읽어야 하는가
open-connector/ ├── src/ │ ├── core/ ◀── ★ 여기부터 읽어라 (23개 파일, 순수 로직) │ │ ├── types.ts 모든 타입의 원천. 460줄+, 여기가 계약서 │ │ ├── guarded-fetch.ts ★ SSRF 방어 fetch (14.5KB, 테스트 18KB) │ │ ├── request.ts IP/URL 판별 유틸 (사설망·예약대역 판정) │ │ ├── action-policy.ts 허용/차단 정책 엔진 │ │ ├── action-search.ts minisearch 기반 액션 검색 │ │ ├── json-schema.ts 스키마 빌더 `s.string()`, `s.looseObject()` │ │ ├── provider-definition.ts defineProviderAction 팩토리 │ │ ├── execution.ts 액션 실행의 최소 단위 │ │ └── cast.ts 프로바이더 응답 → 스키마 형변환 │ │ │ ├── server/ ◀── 런타임 (HTTP·저장소·비밀·파일) │ │ ├── index.ts Node 진입점 │ │ ├── cloudflare.ts Workers 진입점 ← 같은 앱, 다른 부트스트랩 │ │ ├── connect-app.ts Hono 앱 조립 │ │ ├── api/ │ │ │ ├── runtime-api.ts /v1/* 라우트 │ │ │ ├── openapi.ts OpenAPI 3.1 문서 생성 │ │ │ ├── action-markdown.ts agent.md 생성기 │ │ │ ├── auth.ts / runtime-jwt.ts 토큰·JWT 검증 │ │ │ └── oauth-completion-page.ts OAuth 콜백 화면 │ │ ├── actions/ │ │ │ ├── action-runner.ts ★ 모든 호출자의 공통 실행 경계 │ │ │ ├── action-idempotency.ts │ │ │ └── run-log-summary.ts 로그에 남길 값 마스킹/요약 │ │ ├── storage/ │ │ │ ├── sqlite-runtime-store.ts Node용 │ │ │ ├── d1-runtime-store.ts Workers용 │ │ │ └── runtime-token-service.ts 토큰 해시·폐기 │ │ ├── secrets/ │ │ │ ├── secret-codec.ts AES-256-GCM (node:crypto) │ │ │ └── worker-secret-codec.ts 같은 역할 (WebCrypto) │ │ ├── files/ transit file: R2 / KV / 로컬 3구현 │ │ ├── proxy/ 프로바이더 원시 프록시 │ │ └── cloudflare/ 바인딩·에셋 처리 │ │ │ ├── providers/ ◀── 1,097개 디렉터리 (전체 코드의 99%) │ │ ├── provider-loader.ts ★ 지연 로딩 │ │ ├── http-json-runtime.ts 단순 JSON API용 공용 헬퍼 │ │ ├── github/ gmail/ notion/ slack/ ... │ │ └── (알파벳 순: 7_shifts, a_leads, ably, abstract, ...) │ │ │ └── mail/imap-smtp/ 일반 IMAP/SMTP 계정 지원 │ ├── web/ React 19 + Vite 8 콘솔 (npm workspace) │ └── src/{components,lib,locales,assets} │ ├── migrations/ 0001_runtime ~ 0006_connection_identity ├── scripts/ ★ 빌드 파이프라인의 핵심 │ ├── generate-provider-registry.ts 프로바이더 → 레지스트리 │ ├── generate-catalog.ts → catalog/apps/*.json │ ├── generate-provider.ts 새 프로바이더 스캐폴딩 │ └── ensure-generated.ts 부팅 전 생성물 확인 │ ├── docs/ runtime-api · credentials · cloudflare · fly-io ... ├── examples/ openai-tools / local-http / mcp-client ├── .codex/skills/add-provider/ ◀── AI에게 기여시키는 스킬 문서 ├── docker/Dockerfile 멀티스테이지 ├── wrangler.example.jsonc · fly.toml └── AGENTS.md 에이전트용 저장소 규칙
읽는 순서 추천

1일차: src/core/types.ts → 이 프로젝트의 모든 개념(액션·프로바이더·자격증명·실행결과)이 타입으로 정의돼 있다. 460줄짜리 계약서를 먼저 읽는 셈.

2일차: src/providers/hackernews/(인증 없음, 가장 작음) → src/providers/github/(OAuth+API키 둘 다) 순서로. 프로바이더 하나의 전체 모양이 손에 잡힌다.

3일차: src/core/guarded-fetch.ts와 그 테스트. 여기가 이 저장소에서 가장 밀도 높은 코드다.

특이한 파일 둘

주목

.codex/skills/add-provider/SKILL.md

AI 에이전트가 이 저장소에 프로바이더를 추가할 때 따라야 할 절차서다. 사람용 CONTRIBUTING.md와 별개로 "에이전트용 기여 가이드"를 두는 것이 2026년의 새 관행이 되고 있다.

내용이 흥미롭다. "Pattern Picker" 섹션에서 "이 상황이면 이 프로바이더를 참고하라"를 케이스별로 지정한다 — 무인증이면 hackernews/nasa, API키면 avoma/attention, OAuth+API키 둘 다면 github, 커스텀 자격증명이면 clickhouse. 그러면서 "기계적으로 복사하지 말고 현재 헬퍼 API를 발견하는 용도로 쓰라"고 못박는다.

"Public Boundary" 섹션은 더 실용적이다 — 서드파티 로고·API 스펙·문서 발췌를 배포 권한 없이 복사하지 말 것. 1,000개 브랜드를 다루는 프로젝트의 법무 리스크를 문서로 방어하고 있다.

주목

AGENTS.md

저장소 루트에 있는 에이전트용 전역 규칙. 스타일과 검증 절차를 담는다. npm run fix-check(린트 수정 + 타입체크)가 사실상 기여의 관문이다.

6학습 포인트

이 저장소를 안 써도 가져갈 수 있는 것들

① 보안: 아웃바운드 요청도 입력 검증 대상이다

대부분의 개발자는 들어오는 요청만 검증한다. 이 저장소는 나가는 요청을 검증하는 법을 통째로 보여준다. 배울 항목:

실습 아이디어: 당신의 프로젝트에서 사용자 입력이 URL의 일부가 되는 지점을 전부 찾아보라. 웹훅 등록, 이미지 URL 프록시, OAuth 리디렉트 URI — 대부분의 앱에 최소 하나는 있다.

② 아키텍처: "생성물"과 "런타임"을 분리하는 규모 대응

1,097개 프로바이더가 강제한 설계지만, 원리는 훨씬 작은 규모에도 적용된다.

단계산출물비용을 언제 치르는가
빌드 타임catalog/apps/*.jsonCI에서 한 번. 런타임은 비용 0.
부팅카탈로그 JSON 로드 + minisearch 인덱스메모리만. 코드 import 없음.
액션 실행 시점해당 프로바이더 모듈 1개만 동적 import첫 호출에만. 이후 캐시.

실습 아이디어: 앱 부팅 시간이 느리다면 "부팅에 필요한 것"과 "쓸 때 필요한 것"을 나눠보라. 라우트 핸들러, 관리자 페이지, 리포트 생성기 같은 것들은 대개 후자다.

③ 인터페이스 설계: 이중 런타임을 지탱하는 추상화

같은 코드가 Node와 Workers에서 도는 건 마법이 아니라 인터페이스를 좁게 뽑은 결과다.

// src/server/secrets/secret-codec-core.ts — 인터페이스는 딱 3개 멤버
export interface ISecretCodec {
  readonly encrypted: boolean;
  encode(plaintext: string): Promise<string>;
  decode(stored: string): Promise<string>;
}

// 구현체 3개가 이걸 만족한다:
//  PlainTextSecretCodec  (암호화 키 미설정 시 — 아무것도 안 함)
//  AesGcmSecretCodec     (Node: node:crypto의 scryptSync + aes-256-gcm)
//  WorkerSecretCodec     (Workers: WebCrypto)

같은 패턴이 저장소(runtime-store.ts ← SQLite/D1), 파일 전송(transit-file-store.ts ← R2/KV/로컬)에도 반복된다. "인터페이스는 3~5개 메서드를 넘기지 않는다"는 규율이 이식성을 만든다.

코드 읽기
AES-256-GCM + scrypt
scryptSync(passphrase, salt, 32)로 사용자 패스프레이즈를 32바이트 키로 늘리고(키 파생), aes-256-gcm으로 암호화한다. GCM은 암호화와 무결성 검증을 동시에 해주는 모드 — 저장된 값이 변조되면 복호화가 실패한다. 저장 포맷은 enc:v1:<iv>.<tag>.<ciphertext>로, 버전 접두사를 넣어 나중에 알고리즘을 바꿀 여지를 남겼다.

④ 에이전트 인터페이스 설계: 툴을 줄이고 검색을 줘라

MCP나 함수 호출로 도구를 노출하는 모든 개발자에게 해당한다. 정리하면:

⑤ 운영: 감사 가능성을 처음부터 설계에 넣기

마이그레이션 파일 이름만 봐도 이 프로젝트가 무엇을 중요하게 여겼는지 보인다:

마이그레이션추가한 것읽히는 의도
0001_runtimeconnections, oauth_*, runtime_tokens, runs기본 골격
0002_run_service실행 서비스 분리실행 기록을 1급 개념으로
0003_action_idempotency멱등성 키 저장재시도 안전성
0004_action_run_audit감사 로그"누가 언제 뭘 실행했나"
0005_run_retention로그 보존 기간무한 증식 방지 + 개인정보 관리
0006_connection_identity연결 신원다중 사용자·다중 계정 지원

그리고 run-log-summary.tssummarizeForRunLog / safeRunLogError로그에 남기기 전에 민감값을 마스킹하고 크기를 줄이는 전용 모듈이다. 로깅을 나중에 붙이면 절대 나오지 않는 구조다.

가져갈 원칙
토큰은 해시로만 저장한다

migrations/0001runtime_tokens 테이블을 보면 컬럼이 token_hash text not null unique다. 원본 토큰은 어디에도 없다. revoked_atlast_used_at을 함께 두어 폐기와 사용 추적까지 스키마 레벨에서 지원한다. 비밀번호와 똑같이 다루는 것 — 당연해 보이지만 실제로 빼먹는 프로젝트가 아주 많다.

7하드웨어 / 시스템 요구사항

돌려보려면 뭐가 필요한가
경로요구사항비고
Docker (권장)Docker + Compose, 포트 3000, 디스크 여유 1GB 내외docker compose up. GHCR 이미지를 받아온다. GPU 불필요.
Node 로컬 개발Node.js 22+ (이미지는 24), npmnpm inpm run dev. 콘솔은 5173, API는 3000.
CloudflareWorkers + D1 + R2 + Static Assets, wrangler무료 티어로도 가능. wrangler.example.jsonc 복사 후 D1 마이그레이션 적용.
Fly.ioFly 계정 + 영속 볼륨SQLite를 볼륨에 두는 구성. fly.toml 포함.
메모리수백 MB 수준LLM을 돌리는 게 아니라 HTTP 게이트웨이다. 무거운 건 카탈로그 인덱스 정도.

필수 환경변수 정리

# 저장 위치 (Docker는 /app/data 볼륨)
OOMOL_CONNECT_DATA_DIR=/app/data

# ★ 자격증명 암호화 키 — 설정 안 하면 평문 저장된다(PlainTextSecretCodec)
OOMOL_CONNECT_ENCRYPTION_KEY=<충분히 긴 랜덤 문자열>

# 관리 API·콘솔 보호
OOMOL_CONNECT_ADMIN_TOKEN=...
# /v1/*, /mcp 호출자 인증
OOMOL_CONNECT_RUNTIME_TOKEN=...

# OAuth 콜백 주소 계산에 쓰임
OOMOL_CONNECT_ORIGIN=http://localhost:3000

# JWT 검증 (셋을 함께 설정할 때만 활성 · Node 전용)
OOMOL_CONNECT_JWKS_URI=...
OOMOL_CONNECT_JWT_ISSUER=...
OOMOL_CONNECT_JWT_AUDIENCE=...

# 정책
OOMOL_CONNECT_ALLOWED_ACTIONS=...
OOMOL_CONNECT_BLOCKED_ACTIONS=...
OOMOL_CONNECT_ALLOWED_PROXIES=...
OOMOL_CONNECT_BLOCKED_PROXIES=...
함정
암호화 키를 안 넣으면 조용히 평문으로 저장된다

createSecretCodec(encryptionKey)는 키가 undefined에러를 던지지 않고 PlainTextSecretCodec을 돌려준다. 로컬 실험에는 편하지만, 이 상태로 서버에 올리면 DB 파일 하나가 유출되는 순간 연결된 모든 SaaS 토큰이 함께 나간다.

또 하나 — 관리 엔드포인트(/api/*)와 런타임 엔드포인트(/v1/*, /mcp)는 인증 토큰이 다르다. 둘 다 설정하지 않으면 열린 상태로 뜬다. 외부에 노출할 거라면 배포 전에 반드시 확인하라.

8직접 해볼 수 있는 실습 과제

난이도 순 5개
과제 1난이도 ★☆☆☆☆ · 30분

무인증 액션으로 런타임 감 잡기

docker compose up으로 띄우고 hackernews.get_top_stories를 호출한다. 그 다음 GET /v1/actions로 전체 목록을, GET /openapi.json으로 생성된 스펙을, GET /api/actions/hackernews.get_top_stories/agent.md로 에이전트용 가이드를 각각 열어본다.

확인할 것: 같은 액션 하나가 세 가지 형식(런타임 JSON · OpenAPI · 마크다운)으로 어떻게 다르게 표현되는가. 왜 셋 다 필요한지 생각해보라.

과제 2난이도 ★★☆☆☆ · 1~2시간

GitHub 연결 + 정책으로 잠그기

fine-grained PAT를 발급해 연결하고, github.get_current_user를 실행한다. 그 다음 환경변수로 정책을 걸어본다:

OOMOL_CONNECT_ALLOWED_ACTIONS=github.get_*,github.list_*
OOMOL_CONNECT_BLOCKED_ACTIONS=github.delete_repository

확인할 것: github.create_issueaction_not_allowed로 거부되는가? 그 다음 allowlist를 github.*로 넓혔을 때 delete_repository는 여전히 막히는가(=blocklist 우선 규칙 검증)? github.get_*는 매칭될까? — 힌트: createMatcher.*로 끝나는 패턴만 접두사 매칭한다. 이 함정을 직접 확인하는 게 이 과제의 핵심이다.

과제 3난이도 ★★★☆☆ · 반나절

MCP로 붙여서 에이전트에게 도구 쥐여주기

http://localhost:3000/mcp를 MCP 지원 에이전트 호스트에 등록하고, "내 GitHub에서 최근 이슈 3개 요약해줘" 같은 자연어 요청을 던진다.

관찰할 것: 에이전트가 search_actionsget_action_guideexecute_action 순서를 스스로 밟는가? 어느 단계에서 헤매는가? Web Console의 최근 실행 목록으로 실제 호출 흐름을 역추적해보라. "툴 4개 + 검색"이라는 설계가 실제로 작동하는지를 눈으로 확인하는 과제다.

과제 4난이도 ★★★★☆ · 1~2일

프로바이더 직접 추가하기

npm run generate:provider로 스캐폴딩을 만들고, 좋아하는 공개 API(예: 공공데이터포털, OpenWeather)를 프로바이더로 붙인다. .codex/skills/add-provider/SKILL.md의 Pattern Picker를 따라 참고 대상을 고를 것 — 무인증이면 hackernews, API 키면 avoma.

순서:definition.ts에 인증 방식 선언 → ② actions.tss.* 빌더로 입출력 스키마 작성 → ③ executors.ts+runtime.ts에 실제 호출(반드시 guardedFetch 경유) → ④ npm run generate:catalog → ⑤ npm run fix-check로 검증.

배우는 것: 남의 API를 스키마로 번역하는 훈련. 이건 MCP 서버를 직접 만들 때 그대로 쓰이는 근육이다.

과제 5난이도 ★★★★★ · 며칠

guardedFetch를 뚫어보기 (레드팀 연습)

src/core/guarded-fetch.test.ts(18KB)를 정독한 뒤, 테스트가 아직 커버하지 않은 우회 시나리오를 찾아 실패하는 테스트를 먼저 작성해본다.

탐색 방향 예시: IPv6 매핑 주소(::ffff:127.0.0.1), 8진수·16진수 IP 표기(0177.0.0.1), DNS TTL을 이용한 rebinding 타이밍, 리다이렉트 21홉째, 유니코드 도메인(punycode) 혼동.

주의: 이건 자기 로컬 환경에서 자기 코드를 대상으로 하는 연습이다. 실제 서비스 대상 테스트는 하지 말 것. 결과가 진짜 취약점이면 SECURITY.md의 절차대로 비공개 제보하는 게 올바른 순서다.

9관련 기술 심화 학습 로드맵

6주 코스 — 이 저장소를 축으로
주차주제구체적 활동
1주차타입으로 도메인 읽기src/core/types.ts 정독. ActionDefinition, ProviderDefinition, ResolvedCredential, ExecutionResult의 관계를 직접 다이어그램으로 그려본다. TypeScript의 판별 유니온(discriminated union)이 AuthType별 분기에서 어떻게 쓰이는지 관찰.
2주차Hono와 Web 표준 서버Hono 공식 문서 + connect-app.ts/runtime-api.ts. "Node 전용 API를 안 쓰면 어디서든 돈다"를 직접 검증: 작은 Hono 앱을 만들어 Node와 wrangler dev 양쪽에서 띄워본다.
3주차SSRF와 아웃바운드 보안OWASP SSRF 치트시트 → guarded-fetch.ts/request.ts 정독 → 과제 5. IP 주소 표기법(사설 대역, 링크로컬 169.254.x, 클라우드 메타데이터)을 정리해둘 것.
4주차OAuth 2.0 실전docs/credentials.md + oauth-completion-page.ts. 직접 GitHub OAuth 앱을 만들어 authorization code 흐름을 끝까지 태워보고, 토큰 갱신·스코프 부족 에러를 일부러 재현해본다.
5주차MCP와 에이전트 도구 설계MCP 명세 + @modelcontextprotocol/sdk. 이 저장소의 "툴 4개" 전략과, 툴을 전부 나열하는 일반적 MCP 서버를 둘 다 만들어서 비교. 어느 쪽이 에이전트 성공률이 높은지 직접 측정.
6주차엣지 배포와 저장소 추상화Cloudflare Workers + D1 + R2 튜토리얼 → storage/files/의 인터페이스 대칭 구조 분석 → 이 저장소를 실제로 Workers에 배포. 서버리스에서 무엇이 안 되는지(SSE 스트림, Node crypto, 긴 실행) 몸으로 익히기.
이 로드맵의 초점

OpenConnector를 쓰는 법을 배우는 게 아니다. "신뢰할 수 없는 호출자와 신뢰할 수 없는 목적지 사이에 안전한 중개자를 세우는 법"을 배우는 것이다. 이 문제는 API 게이트웨이, 웹훅 릴레이, 결제 프록시, 사내 통합 서버 — 에이전트와 아무 상관 없는 곳에서 계속 다시 나타난다.

10핵심 키워드 사전

이 문서와 저장소에서 반복되는 말들
용어
Provider연결 대상 외부 서비스 하나(github, gmail, notion…). service 문자열이 식별자.
Action프로바이더가 제공하는 개별 동작. ID는 {service}.{name} 형식(github.create_issue).
Catalog프로바이더·액션의 전체 목록. 빌드 시점에 정적 JSON으로 생성된다.
Connection사용자의 특정 계정 자격증명 한 벌. (service, connection_name)이 기본키 — 같은 서비스의 여러 계정을 이름으로 구분한다.
Alias어떤 연결을 쓸지 지정하는 이름. 헤더 x-oo-connector-alias: work 또는 쿼리 ?alias=work.
Executor액션을 실제로 수행하는 함수. 지연 로딩되며 ActionExecutor 타입.
Runtime token/v1/*·/mcp 호출자 인증용 토큰. DB에는 해시만 저장된다.
Admin token/api/*·콘솔·/docs용 관리자 인증. 런타임 토큰과 별개.
Action policy액션 실행 허용/차단 규칙. *, service.*, 정확일치 세 가지 패턴.
Proxy미리 정의된 액션이 아니라 프로바이더 API를 원시 형태로 중계하는 경로. 별도 정책(ALLOWED_PROXIES)으로 관리.
Transit file액션이 주고받는 임시 파일. R2·KV·로컬 중 하나에 저장되고 수명이 짧다.
Run log액션 실행 기록. 민감값은 run-log-summary가 마스킹한 뒤 적재.
Idempotency key재시도가 중복 실행이 되지 않게 하는 요청 식별자. 255바이트 이하.
guardedFetchSSRF 방어가 내장된 fetch 래퍼. 모든 외부 호출이 여기를 지난다.
SSRF서버를 속여 내부 주소로 요청을 보내게 하는 공격. 클라우드 메타데이터 탈취가 대표 시나리오.
MCP에이전트가 외부 도구에 접근하는 표준 프로토콜. 여기서는 POST /mcp 하나로 노출.
locallyExecutable액션이 로컬 실행기를 가지는지 나타내는 플래그. false면 카탈로그에만 존재.
followUpActions"이 다음에 보통 하는 액션" 힌트. 에이전트의 다단계 계획을 돕는다.
asyncLifecycle오래 걸리는 액션의 진행 확인·폴링 계약.
D1 / R2Cloudflare의 SQLite 호환 DB / 오브젝트 스토리지. Workers 배포 시 각각 SQLite·로컬 파일을 대체.

11참고 링크

더 파고들 곳

이 저장소

주변 도구

기반 기술