<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BBT 한국어</title>
    <description>The latest articles on DEV Community by BBT 한국어 (bbt-ko).</description>
    <link>https://dev.to/bbt-ko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15904%2F499fc360-443f-429f-a129-0195e60f1d3a.png</url>
      <title>DEV Community: BBT 한국어</title>
      <link>https://dev.to/bbt-ko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbt-ko"/>
    <language>en</language>
    <item>
      <title>osFoundry에서 지난 Claude Code 세션을 찾아 다시 이어가는 방법</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:41:44 +0000</pubDate>
      <link>https://dev.to/bbt-ko/osfoundryeseo-jinan-claude-code-sesyeoneul-caja-dasi-ieoganeun-bangbeob-32b2</link>
      <guid>https://dev.to/bbt-ko/osfoundryeseo-jinan-claude-code-sesyeoneul-caja-dasi-ieoganeun-bangbeob-32b2</guid>
      <description>&lt;p&gt;지난주에 돌렸던 Claude Code 세션을 다시 화면에 띄우는 게 목표다. &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt;에서 프로젝트에 들어간 단어로 검색해서 그 세션이 뭘 했는지 확인하고, 버튼 한 번이면 세션이 작업하던 폴더에서 &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt;가 실행된다. 같은 목록에 Codex 세션도, 이 머신에 깔린 다른 AI 앱 10개의 세션도 같이 뜬다. osFoundry는 Claude Code와 통신하지 않는다. Claude Code가 이미 써 둔 파일을 읽을 뿐이다.&lt;/p&gt;

&lt;h2&gt;
  
  
  누가 뭘 하나
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claude Code / Codex&lt;/th&gt;
&lt;th&gt;osFoundry 데스크톱 앱&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인을 갖고 있다. osFoundry는 로그인 파일이나 토큰을 읽지 않는다.&lt;/td&gt;
&lt;td&gt;각 앱이 직접 남긴 세션 파일을 이 기기 안에서 읽는다. 네트워크로는 아무것도 안 나간다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대화 기록을 쓴다 (&lt;code&gt;~/.claude/projects/…/&amp;lt;id&amp;gt;.jsonl&lt;/code&gt;, &lt;code&gt;~/.codex/sessions/…&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;세션 목록을 보여 주고, 기록된 폴더·브랜치·모델·승인 모드를 표시한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재개한 뒤의 도구 호출은 전부 CLI가 실행하고, 승인 프롬프트도 CLI가 띄운다&lt;/td&gt;
&lt;td&gt;세션 폴더에서 Terminal 패널을 열고 CLI를 실행한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션 ID를 갖고 있다&lt;/td&gt;
&lt;td&gt;그 ID로 재개 명령을 만든다: &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;codex resume &amp;lt;id&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  설정
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;데스크톱 앱 사이드바에서 Main → External agents(외부 에이전트)로 들어간다(라우트는 &lt;code&gt;/external-agents&lt;/code&gt;). 로컬 파일을 읽는 화면이라 데스크톱 앱에서만 열린다. 부제에도 그대로 적혀 있다. 다른 AI 앱의 세션을 각 앱의 파일에서 읽어 오고, 업로드는 하지 않는다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp66b9csp24dd2vuzk2wj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp66b9csp24dd2vuzk2wj.jpg" alt="osFoundry의 External agents 화면. Codex CLI, Cursor, Gemini CLI, Claude app, Codex app, Claude Code, Copilot CLI, OpenCode, Qwen Code의 세션 12개가 있고, 위쪽에 앱별 칩이 있다" width="800" height="1152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;위쪽 칩은 세션이 있는 앱마다 하나씩, 개수와 함께 붙는다. osFoundry가 아는 앱은 12개다. Claude Code, Claude app, Codex CLI, Codex app, Gemini CLI, Qwen Code, OpenCode, Copilot CLI, Cursor, Devin, Antigravity, Kiro. Claude Code를 누르면 그 세션만 남는다. 오른쪽 Apps 메뉴에서 앱을 끄면 이 기기에서는 그 앱의 폴더를 아예 읽지 않는다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;"Search titles, folders, branches"(제목, 폴더, 브랜치 검색)에 프로젝트에 들어간 단어를 넣는다. 검색 대상은 제목, 폴더 경로, git 브랜치, 앱 이름, 세션 ID다. 메시지 본문은 검색하지 않으니까, Claude가 했던 말 말고 프로젝트 이름이나 브랜치 이름으로 찾는 게 맞다.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;세션을 클릭한다. 오른쪽에 Claude Code가 기록한 정보와 대화 전체가 뜨고, 도구 호출은 하나하나 별도 행으로 나온다.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdow80yx2oyrnl2qlf7w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdow80yx2oyrnl2qlf7w.jpg" alt="osFoundry에서 연 Claude Code 세션 &amp;quot;Fix double charge on checkout retry&amp;quot;. Open in Terminal 버튼, 기록된 폴더·브랜치·모델·승인 모드, 그리고 Read, Grep, Edit, Bash 도구 행이 있는 대화" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"From the app"(앱에서) 패널에는 다시 실행할 때 기록된 값을 각각 어떻게 다룰지가 나온다. 폴더는 그대로 가져가고, 브랜치는 지금 폴더의 브랜치와 대조하고, 모델과 승인 모드는 보여 준다. 기록된 승인 모드가 CLI 기본값보다 덜 묻는 모드라면(Claude Code 기준으로 &lt;code&gt;default&lt;/code&gt;나 &lt;code&gt;plan&lt;/code&gt;이 아닌 것), 그 실행에서 직접 체크하지 않는 한 넘기지 않는다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open in Terminal(터미널에서 열기)을 누른다. 세션이 돌던 때와 폴더 브랜치가 달라졌으면 먼저 다이얼로그가 뜬다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gspft4ha898m034x09g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gspft4ha898m034x09g.jpg" alt="osFoundry 확인 다이얼로그: 폴더가 다른 브랜치에 있다. 이 세션은 fix/checkout-retry에 있었고, 폴더는 지금 main에 있다. main에서 다시 열리고, 브랜치는 바뀌지 않는다." width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;원래 브랜치에서 이어가야 하는 작업이면 Cancel을 누르고 직접 &lt;code&gt;git switch&lt;/code&gt; 하면 된다. Open anyway를 누르면 폴더의 지금 상태 그대로 재개한다. osFoundry는 &lt;code&gt;git -C &amp;lt;folder&amp;gt; rev-parse --abbrev-ref HEAD&lt;/code&gt;로 브랜치를 확인만 하고, checkout은 절대 실행하지 않는다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;세션 폴더에서 Terminal 패널이 열리고, Claude Code가 같은 세션을 이어받는다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehf288kidnsncv0j5zq8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehf288kidnsncv0j5zq8.jpg" alt="osFoundry Terminal 패널. ~/code/shop-api의 main에서 claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58이 실행되고 있다" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  실제로 돌려 보면
&lt;/h2&gt;

&lt;p&gt;예시다. 아래 세션은 앱이 읽는 형식에 맞춘 데모 데이터(개발자 한 명이 여러 앱에서 보낸 2주)고, 화면과 명령은 실제 앱 그대로다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External agents › search: checkout
  Claude Code  Fix double charge on checkout retry   shop-api  fix/checkout-retry  6d ago
  Cursor       Fix flaky checkout e2e test           shop-web  test/checkout-e2e   8d ago

Conversation (read from ~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-….jsonl)
  you     Customers get charged twice when they press Pay again after a timeout. Find out why.
  ↳ Read  src/payments/charge.ts
  ↳ Grep  createPaymentIntent
  claude  Found it. Every retry creates a new payment, so a second press charges again. …
  ↳ Edit  src/payments/charge.ts
  ↳ Bash  npm test -- payments
  claude  Fixed. A retry now reuses the first charge. All 42 payment tests pass, …

Open in Terminal
  confirm  This session was on fix/checkout-retry; the folder is on main now.
  ~/code/shop-api (main) $ claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 명령은 사람이 치는 것도, 추측한 것도 아니다. 데스크톱 앱이 기록된 세션으로 만든다. 기본은 &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt;이고, 대화 기록에 남아 있으면 &lt;code&gt;--model&lt;/code&gt;과 &lt;code&gt;--effort&lt;/code&gt;가 붙고, &lt;code&gt;--permission-mode&lt;/code&gt;는 모드가 엄격하거나 직접 체크했을 때만 붙는다. Codex 세션이면 같은 버튼이 &lt;code&gt;codex resume &amp;lt;id&amp;gt;&lt;/code&gt;를 실행하고, &lt;code&gt;-m&lt;/code&gt;, &lt;code&gt;-a&lt;/code&gt;, &lt;code&gt;-s&lt;/code&gt;도 같은 규칙으로 붙는다.&lt;/p&gt;

&lt;p&gt;Open in Terminal 옆의 Continue here(여기서 계속)를 쓰면 Claude Code나 Codex 세션을 CLI 대신 osFoundry 채팅 안에서 이어갈 수 있다.&lt;/p&gt;

&lt;h2&gt;
  
  
  어디까지 하나
&lt;/h2&gt;

&lt;p&gt;osFoundry는 읽기만 하고, 가로채지 않는다. 앱별 어댑터는 그 앱의 세션 파일과 SQLite 인덱스만 읽기 전용으로 열고, 로그인 파일은 건드리지 않는다. 그래서 연결할 계정도 없다. Claude Code의 로그인은 Claude Code에 그대로 있고, 재개한 세션은 내 권한으로, CLI 자체의 승인 프롬프트 아래에서 돈다.&lt;/p&gt;

&lt;p&gt;검색을 메타데이터로 한정한 것도 일부러다. Claude Code 대화 기록은 꽤 커진다(이 글을 쓴 Mac에서는 중앙값이 168 MB였다). 그래서 목록은 각 파일의 앞 몇 줄과 끝부분만으로 만들고, 전체 내용은 지금 연 세션 하나만 읽는다.&lt;/p&gt;

&lt;p&gt;승인 설정이 저절로 느슨해지는 일은 없다. 허용 범위가 넓은 모드로 돌던 세션도, 그 한 번의 실행에서 체크하지 않으면 CLI 기본값으로 다시 열린다. 브랜치는 확인하고 알려 줄 뿐 건드리지 않는다. 세션 폴더가 없어졌으면 다른 곳에서 열지도 않는다.&lt;/p&gt;

&lt;p&gt;앱 자체 창에만 있는 건 거기 그대로 남는다. Codex app의 브라우저 패널과 리뷰 화면, Claude app의 커넥터와 아티팩트, Cursor의 에디터는 세션에 이름만 표시되고 터미널로 넘어오지 않는다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다음 단계
&lt;/h2&gt;

&lt;p&gt;아직 돌고 있는 세션을 열어 보고, &lt;a href="https://osfoundry.io/ko/platform/agent-sessions" rel="noopener noreferrer"&gt;osFoundry의 Sessions 보드&lt;/a&gt;도 확인해 보자. osFoundry 자체 에이전트 중 어떤 게 작업 중인지, 내 응답을 기다리는지, 끝났는지 보여 준다. 데스크톱 앱이 아직 없다면 &lt;a href="https://osfoundry.io/ko/download" rel="noopener noreferrer"&gt;Mac, Windows, Linux용 osFoundry를 다운로드&lt;/a&gt;하면 된다.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>osfoundry</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Codex CLI를 osFoundry 워커로 돌리기</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:33:51 +0000</pubDate>
      <link>https://dev.to/bbt-ko/codex-clireul-osfoundry-weokeoro-dolrigi-1f9g</link>
      <guid>https://dev.to/bbt-ko/codex-clireul-osfoundry-weokeoro-dolrigi-1f9g</guid>
      <description>&lt;p&gt;이 글을 따라 하고 나면 &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 데스크톱 앱의 에이전트 모델이 &lt;code&gt;cli:codex:agent&lt;/code&gt;가 된다. 작업이 그 에이전트에 넘어오면 앱이 내 Mac에서 &lt;code&gt;codex exec&lt;/code&gt;를 띄운다. 인증은 내 ChatGPT 로그인 그대로고, 실행은 내가 허용한 폴더 안에서만 일어난다. Codex가 돌려주는 내용, 즉 추론과 실행한 명령, 마지막 메시지가 osFoundry 실행 화면에 한 줄씩 찍힌다. OpenAI API 키는 처음부터 끝까지 한 번도 안 나온다.&lt;/p&gt;

&lt;h2&gt;
  
  
  누가 뭘 맡나
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex CLI&lt;/th&gt;
&lt;th&gt;osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인&lt;/td&gt;
&lt;td&gt;Codex 자체 ChatGPT 로그인(&lt;code&gt;codex login&lt;/code&gt;). 저장도 Codex가 한다&lt;/td&gt;
&lt;td&gt;로그인 정보는 아예 안 본다. 자식 프로세스 환경에서 &lt;code&gt;CODEX_API_KEY&lt;/code&gt;, &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, &lt;code&gt;OPENAI_BASE_URL&lt;/code&gt;을 빼서 실행이 구독 쪽에 머물게 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 호출&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;codex&lt;/code&gt; 프로세스에서 OpenAI로 바로 간다&lt;/td&gt;
&lt;td&gt;비용은 &lt;code&gt;0.0&lt;/code&gt;(구독, 종량 과금 없음)으로 기록한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;도구&lt;/td&gt;
&lt;td&gt;작업 폴더 안에서 자체 셸과 파일 편집&lt;/td&gt;
&lt;td&gt;자기 도구(예: &lt;code&gt;memory_search&lt;/code&gt;)를 &lt;code&gt;127.0.0.1&lt;/code&gt; 루프백 MCP 브리지로 제공한다. 단, git worktree로 실행이 격리될 때만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;승인&lt;/td&gt;
&lt;td&gt;실행 중엔 없다. &lt;code&gt;exec&lt;/code&gt;는 비대화형이고 &lt;code&gt;approval_policy="never"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;브리지를 거치는 도구 호출은 전부 &lt;code&gt;orchestrator::execute_tool&lt;/code&gt;과 그 정책 게이트를 통과한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력&lt;/td&gt;
&lt;td&gt;완료된 아이템마다 stdout에 JSON 이벤트 한 줄(&lt;code&gt;--json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;이벤트를 진행 줄이나 답 텍스트로 바꾼다. 에러용으로 stderr 끝부분을 남겨 둔다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내 파일&lt;/td&gt;
&lt;td&gt;폴더나 worktree 안에서 읽고 쓴다&lt;/td&gt;
&lt;td&gt;폴더는 osFoundry가 고른다. Code 화면의 저장소, 없으면 허용된 첫 번째 로컬 폴더&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  설정
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Codex CLI를 설치하고 한 번 로그인한다. osFoundry는 바이너리를 &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;~/.codex/bin&lt;/code&gt;, 그리고 &lt;code&gt;/Applications/Codex.app&lt;/code&gt; 안에서 찾는다(cask로 설치하면 바이너리가 &lt;code&gt;Contents/Resources/codex&lt;/code&gt;에 들어가서 &lt;code&gt;which codex&lt;/code&gt;로는 안 잡힌다).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   codex login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;헤더의 열쇠 아이콘(툴팁은 API Keys (BYOK))을 열고 External AI(외부 AI) 탭으로 넘어간다. Codex 행에는 Installed, No live stream 배지, 그리고 자율 모델 ID가 보인다. Codex에는 채팅용 ID가 없어서 ID는 하나만 나온다.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Codex   Installed   No live stream   Enabled
   cli:codex:agent (autonomous)
   osFoundry tools require git-worktree isolation for Codex (it trades its
   own sandbox for git safety). Point it at a git repo folder.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhk1bcsadgwstydos8ywx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhk1bcsadgwstydos8ywx.jpg" alt="External AI 탭의 Claude Code와 Codex" width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: External AI 탭의 Claude Code와 Codex&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;아직 로그인 전이라면 Sign in 버튼을 누르면 &lt;code&gt;codex login&lt;/code&gt;을 실행하는 터미널이 열린다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify sign-in(로그인 확인)을 누른다. 실제 실행과 같은 빌더로 아주 작은 완성 한 번을 돌린다.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   codex &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"Reply with the single word: ok"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--skip-git-repo-check&lt;/span&gt; &lt;span class="nt"&gt;--ignore-user-config&lt;/span&gt; &lt;span class="nt"&gt;--ignore-rules&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-s&lt;/span&gt; read-only &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'approval_policy="never"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--ignore-rules&lt;/code&gt;는 &lt;code&gt;codex exec --help&lt;/code&gt;에 그 플래그가 있을 때만 붙는다. 구버전은 이 플래그를 거부하고 실행 전체가 죽어 버리기 때문에, osFoundry는 바이너리에 직접 물어본다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;에이전트에 폴더를 준다. Settings → Local Folders에서 추가하면 된다. Codex가 osFoundry 자체 도구까지 쓰게 하고 싶다면 그 폴더를 git 저장소로 만들고, Code 화면에서 Enable for this workspace를 누른다. 그러면 실행마다 전용 브랜치와 작업 사본이 생긴다.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Studio에서 에이전트를 열고 Model Preference를 설정한다. 피커의 CLI agents 섹션에 Codex가 &lt;code&gt;cli:codex:agent&lt;/code&gt;(프로바이더 표기는 "External AI (subscription)")로 나온다. 에이전트가 워커 단계를 맡아야 한다면 Runtime Role을 &lt;code&gt;worker&lt;/code&gt;로 둔다.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;작업이 넘어가면 명령은 이렇게 생긴다. worktree가 없으면 Codex 자체 샌드박스를 그대로 쓴다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;system prompt&amp;gt;

User: &amp;lt;task&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--skip-git-repo-check&lt;/span&gt; &lt;span class="nt"&gt;--ignore-user-config&lt;/span&gt; &lt;span class="nt"&gt;--ignore-rules&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/folder &lt;span class="nt"&gt;-s&lt;/span&gt; workspace-write &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'approval_policy="never"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;worktree가 있으면 샌드박스 플래그가 빠지고 대신 브리지가 붙는다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;system prompt&amp;gt;

User: &amp;lt;task&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--skip-git-repo-check&lt;/span&gt; &lt;span class="nt"&gt;--ignore-user-config&lt;/span&gt; &lt;span class="nt"&gt;--ignore-rules&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-C&lt;/span&gt; /path/to/worktree &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dangerously-bypass-approvals-and-sandbox&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'mcp_servers.osfoundry.url="http://127.0.0.1:&amp;lt;port&amp;gt;/mcp"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'mcp_servers.osfoundry.http_headers={Authorization="Bearer &amp;lt;per-run token&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex에는 시스템 프롬프트를 넣는 플래그가 없다. 그래서 osFoundry는 에이전트의 시스템 프롬프트를 빈 줄 하나로 구분해서 대화 앞에 붙인다. 이전 턴은 그 뒤에 &lt;code&gt;User:&lt;/code&gt; / &lt;code&gt;Assistant:&lt;/code&gt; 대화 기록으로 이어진다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실행 한 번
&lt;/h2&gt;

&lt;p&gt;예시다. Codex가 쓰는 이벤트 줄과 osFoundry가 그걸 보고 그리는 줄을 나란히 뒀다. 문구는 설명용이지만, 이벤트 타입과 줄 형식은 코드가 실제로 매핑하는 그대로다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ codex exec … --json
{"type":"item.completed","item":{"type":"reasoning","text":"Check what the tests cover first."}}
{"type":"item.completed","item":{"type":"mcp_tool_call","server":"osfoundry","tool":"memory_search"}}
{"type":"item.completed","item":{"type":"command_execution","command":"npm test"}}
{"type":"item.completed","item":{"type":"file_change","path":"src/date.ts","change":"edit"}}
{"type":"item.completed","item":{"type":"agent_message","text":"Fixed the off-by-one in parseRange; 42 tests pass."}}
{"type":"turn.completed","usage":{"input_tokens":…,"cached_input_tokens":…,"output_tokens":…}}

osFoundry run
  Thinking: Check what the tests cover first.
  Using memory_search…
  Using shell — npm test
  Edited src/date.ts
  Fixed the off-by-one in parseRange; 42 tests pass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex는 브리지 도구 이름을 &lt;code&gt;server&lt;/code&gt;와 &lt;code&gt;tool&lt;/code&gt; 두 필드로 나눠서 준다. 그래서 줄에는 &lt;code&gt;mcp__osfoundry__&lt;/code&gt; 접두사 없이 &lt;code&gt;memory_search&lt;/code&gt;만 보인다. 답은 토큰 단위가 아니라 &lt;code&gt;agent_message&lt;/code&gt; 아이템 단위로 통째로 온다. 진행 줄은 Codex가 아직 일하는 동안에도 하나씩 뜨지만, 메시지 본문은 Codex가 그 메시지를 다 쓴 다음에야 나온다. 토큰 수는 &lt;code&gt;turn.completed&lt;/code&gt;에서 가져온다. 여기서 &lt;code&gt;input_tokens&lt;/code&gt;는 캐시된 부분을 빼고 센 값이라, osFoundry가 &lt;code&gt;cached_input_tokens&lt;/code&gt;를 다시 더해서 컨텍스트 크기로 쓴다.&lt;/p&gt;

&lt;h2&gt;
  
  
  선은 어디에 그어져 있나
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cli:codex&lt;/code&gt; 채팅 모드는 없다. Codex 설정에는 셸 도구를 끄는 스위치가 없어서, Codex로 "도구 없는 완성"을 돌려도 결국 셸을 든 에이전트가 된다. osFoundry는 그걸 조용히 돌리는 대신 채팅 ID 자체를 거부한다. 메시지는 "codex has no tool-less chat mode — use 'cli:codex:agent' for an autonomous run instead."다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-s&lt;/code&gt;는 파일 시스템과 명령 실행에 거는 샌드박스다. &lt;code&gt;workspace-write&lt;/code&gt;는 Codex가 쓸 수 있는 위치와 명령이 할 수 있는 일을 좁힌다. 하지만 모델이 어떤 도구를 부를 수 있는지는 막지 않고, Codex에는 &lt;code&gt;--disallowedTools&lt;/code&gt; 같은 것도 없다. 그래서 Codex를 채널에 추가할 때 Add agent 시트에 이 하네스에서는 도구 범위를 강제할 수 없다는 안내가 뜬다.&lt;/p&gt;

&lt;p&gt;osFoundry 도구를 쓰려면 worktree가 필요하다. 비대화형 &lt;code&gt;exec&lt;/code&gt;에서 Codex는 승인과 샌드박스를 둘 다 우회하지 않는 한 MCP 도구 호출을 전부 취소한다. 도구별 승인 모드도, trusted project 설정도, &lt;code&gt;approval_policy="never"&lt;/code&gt;도 결국 다 취소된다. 그래서 앱은 딱 하나를 맞바꾼다. Codex의 OS 샌드박스를 내려놓는 건 git 격리가 그 자리를 대신할 때뿐이다. worktree가 없으면 실행은 &lt;code&gt;-s workspace-write&lt;/code&gt;를 유지하고, 브리지는 안 붙고, 실행 화면에 그렇다는 줄이 하나 찍힌다.&lt;/p&gt;

&lt;p&gt;내 개인 Codex 설정은 끼어들지 않는다. &lt;code&gt;--ignore-user-config&lt;/code&gt;가 &lt;code&gt;~/.codex/config.toml&lt;/code&gt;을 건너뛰니까, 내가 따로 설정해 둔 MCP 서버는 osFoundry 실행에 안 들어온다. 인증은 여전히 &lt;code&gt;CODEX_HOME&lt;/code&gt;에서 읽는다. &lt;code&gt;--ignore-rules&lt;/code&gt;는 &lt;code&gt;.rules&lt;/code&gt; execpolicy 파일도 빼 준다.&lt;/p&gt;

&lt;p&gt;실행마다 벽시계 기준 상한이 있다. 자율 실행은 1,800초, 로그인 확인은 300초다. 시간이 다 되면 자식 프로세스는 drop되면서 kill된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다음으로 해 볼 것
&lt;/h2&gt;

&lt;p&gt;작은 git 저장소를 에이전트에 물리고, 워크스페이스에서 코드 에이전트를 켠 다음, 셸 명령과 osFoundry 메모리가 둘 다 필요한 작업을 던져 보자. 그러면 &lt;code&gt;Using shell&lt;/code&gt;과 &lt;code&gt;Using memory_search…&lt;/code&gt;가 같은 실행 안에 나란히 찍히는 걸 볼 수 있다. Model Preference가 어디 있는지는 &lt;a href="https://osfoundry.io/ko/platform/studio-agents" rel="noopener noreferrer"&gt;Studio 에이전트 페이지&lt;/a&gt;에 나와 있고, 데스크톱 빌드는 &lt;a href="https://osfoundry.io/ko/download" rel="noopener noreferrer"&gt;다운로드 페이지&lt;/a&gt;에서 받을 수 있다.&lt;/p&gt;

</description>
      <category>codex</category>
      <category>osfoundry</category>
      <category>cli</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>osFoundry에서 내 승인을 기다리는 Claude Code 세션 찾는 법</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:26:02 +0000</pubDate>
      <link>https://dev.to/bbt-ko/osfoundryeseo-nae-seungineul-gidarineun-claude-code-sesyeon-cajneun-beob-3o2m</link>
      <guid>https://dev.to/bbt-ko/osfoundryeseo-nae-seungineul-gidarineun-claude-code-sesyeon-cajneun-beob-3o2m</guid>
      <description>&lt;p&gt;이 글대로 따라 하면 터미널 pane 하나에서는 Claude Code가, 다른 pane에서는 Codex가 돌고, 옆에서는 채팅이랑 에이전트 몇 개가 같이 돌아가는 상태가 된다. &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 보드 하나에 이게 전부 올라온다. Claude Code가 "Do you want to make this edit?"에서 멈추면 1초 안에 카드가 Needs you 열로 옮겨 간다. 클릭 한 번이면 그 프롬프트가 떠 있는 pane이 열린다. 대답하면 카드는 다시 Running으로 돌아간다.&lt;/p&gt;

&lt;h2&gt;
  
  
  누가 뭘 맡나
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;osFoundry 데스크톱 앱&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인&lt;/td&gt;
&lt;td&gt;자기 Claude 로그인을 자기 프로세스 안에서 쓴다&lt;/td&gt;
&lt;td&gt;아무것도 들고 있지 않다. 자기가 띄운 에이전트에서는 상속된 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;를 지워서, 구독으로 돌린 작업이 API 요금으로 청구되지 않게 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;어디서 도나&lt;/td&gt;
&lt;td&gt;Terminal 화면의 진짜 PTY, 프로젝트 폴더 안&lt;/td&gt;
&lt;td&gt;PTY를 소유하고, 화면을 에뮬레이트한 사본을 들고 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;승인은 누가&lt;/td&gt;
&lt;td&gt;사용자. Claude Code 자체 권한 프롬프트에서 답한다&lt;/td&gt;
&lt;td&gt;절대 대신 답하지 않는다. 카드를 옮기고 pane을 열어 줄 뿐이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태는 어떻게 아나&lt;/td&gt;
&lt;td&gt;Claude Code는 평소처럼 UI를 그린다&lt;/td&gt;
&lt;td&gt;700 ms마다 화면을 공개된 규칙과 대조한다. 자기가 띄운 에이전트라면 훅 보고도 받는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터는 어디에&lt;/td&gt;
&lt;td&gt;내 파일, 내 터미널&lt;/td&gt;
&lt;td&gt;화면 텍스트는 Mac 위 앱 메모리에만 있다. 에이전트 세션을 끝내면 요약이 지식 파일(knowledge files)에 기록된다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  세팅
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Terminal 화면(사이드바 Main → Terminal, &lt;code&gt;/terminal&lt;/code&gt;)을 열고 평소처럼 에이전트를 띄운다. pane마다 독립된 PTY다.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ~/code/shop-api &lt;span class="o"&gt;(&lt;/span&gt;fix/checkout-retry&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$ &lt;/span&gt;claude
   ~/code/shop-web &lt;span class="nv"&gt;$ &lt;/span&gt;codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;osFoundry는 터미널을 잡고 있는 프로그램을 보고 Claude Code를 알아본다. 래퍼 스크립트로 띄웠다면 화면 내용으로 알아본다. 따로 설정할 건 없다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sessions(사이드바 Main → Sessions, &lt;code&gt;/sessions&lt;/code&gt;)를 연다. 채팅, 단일 에이전트, 팀, 루프, 터미널 세션이 전부 카드 한 장씩이다. 열은 Running(실행 중), Needs you(확인 필요), Paused(일시정지), Idle(대기), Done(완료), Failed(실패)다. 카드를 열에 끌어다 놓는 걸로 상태를 정할 수는 없다. 어느 열에 들어갈지는 런타임이 보고하는 상태가 정한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzv6xwranntbhaa0g5umr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzv6xwranntbhaa0g5umr.jpg" alt="Running, Needs you, Paused, Idle, Done 열이 있는 osFoundry Sessions 보드. Claude Code의 shop-api가 Needs you에 있다(예시 데이터)" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Filter를 누르고(필터가 걸리면 Filtered로 바뀐다) Needs me only(내 확인이 필요한 것만)에 체크한다. 종류(Chat, Agent, Team, Loop, Terminal)와 기간 설정은 그대로다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frksa5koxmh0vgl4axyq5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frksa5koxmh0vgl4axyq5.jpg" alt="Sessions 보드의 Filter 메뉴. Mine only 아래에 Needs me only가 있다(예시 데이터)" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;카드는 두 장 남는다. 질문을 던진 채팅 하나, 그리고 &lt;code&gt;shop-api&lt;/code&gt;의 Claude Code 터미널.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdglo6lkmlylog7fgqmbu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdglo6lkmlylog7fgqmbu.jpg" alt="Needs me only로 거른 보드. shop-api Claude Code 카드와 Pricing page layout 채팅만 남았다(예시 데이터)" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;채팅은 마지막 메시지가 아직 답을 기다리는 질문으로 끝나 있으면 여기로 온다. 터미널은 상태가 &lt;code&gt;blocked&lt;/code&gt;일 때 여기로 오는데, 다음 단계가 바로 그 장면이다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;shop-api&lt;/code&gt; 카드를 클릭한다. Terminal 화면이 그 pane으로 열리고, 프롬프트는 아직 떠 있다. 왼쪽 레일에는 Claude Code가 "Needs you · shop-api", Codex가 "Working · shop-web"으로 표시된다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqov76hy3kej02szom0b7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqov76hy3kej02szom0b7.jpg" alt="osFoundry 터미널 pane에서 &amp;quot;Do you want to make this edit to charge.ts?&amp;quot;에 멈춰 있는 Claude Code(예시 데이터)" width="799" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pane이 여러 개라면 Mod+.(Mac에서는 ⌘.)로 확인이 필요한 다음 에이전트로 바로 넘어간다. 상태 표시줄에도 "⌘. to jump to what needs you"라고 떠 있다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;답은 평소 터미널에서 하듯 pane에서 하면 된다. &lt;code&gt;1. Yes&lt;/code&gt;에서 Enter. Claude Code가 작업을 이어 가고, 다음 스윕에서 레일과 보드가 다시 Working / Running으로 바뀐다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyegh20s377buloico4k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyegh20s377buloico4k.jpg" alt="답한 뒤의 같은 pane. 수정이 반영되고 테스트가 돌고, Claude Code가 수정 완료를 보고한다(예시 데이터)" width="799" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;끝난 에이전트 세션이 Idle에 있으면 카드를 Done으로 끌어다 놓는다. 앱이 먼저 확인을 받는다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfzq3554bn0pn76tutwv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfzq3554bn0pn76tutwv.jpg" alt="세션 종료 대화상자: &amp;quot;It is summarised into your knowledge files first, then filed away. Nothing is deleted.&amp;quot;(예시 데이터)" width="799" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;확인을 누르면 요약을 쓴 다음 세션을 끝낸다. 요약이 실패하면 세션은 원래 자리에 그대로 남는다. 실행 중인 세션은 아예 Done에 놓을 수 없다. "This session is still running. Pause it first, then end it."(아직 실행 중이니 먼저 일시정지한 뒤 끝내라)라는 메시지가 뜬다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실제로 돌려 보면
&lt;/h2&gt;

&lt;p&gt;예시다. 스크린샷에 나온 pane과, 화면마다 osFoundry가 내린 판정이다. 판정은 Claude Code용 규칙 파일 &lt;code&gt;claude.toml&lt;/code&gt;에서 나온다. 앱에 같이 들어 있고, &lt;a href="https://osfoundry.io/agent-detection/claude.toml" rel="noopener noreferrer"&gt;osfoundry.io/agent-detection/claude.toml&lt;/a&gt;에서도 받을 수 있다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/code/shop-api (fix/checkout-retry) $ claude
● Retry now reuses the first charge. Updating the payment code.

Edit file  src/payments/charge.ts
  41 -  const intent = await stripe.paymentIntents.create(params)
  41 +  const intent = await stripe.paymentIntents.create(params, {
  42 +    idempotencyKey: `charge-${order.id}`,
  43 +  })

Do you want to make this edit to charge.ts?
❯ 1. Yes
  2. Yes, allow all edits during this session
  3. No, and tell Claude what to do differently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;화면                          state     걸린 규칙                                                카드
스피너, "esc to interrupt"    working   esc to interrupt                                         Running
위의 프롬프트                 blocked   Do you want to (proceed|continue|make this edit|create)  Needs you
  ... "1. Yes"에서 Enter
다시 스피너                   working   esc to interrupt                                         Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;● Update(src/payments/charge.ts)
  ⎿ Updated src/payments/charge.ts with 3 additions and 1 removal
● Bash(npm test -- payments)
  ⎿ Tests: 42 passed, 42 total
● Fixed. A second press of Pay now reuses the first charge.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blocked&lt;/code&gt; 규칙이 &lt;code&gt;working&lt;/code&gt; 규칙보다 위에 있는 건 일부러다. 프롬프트가 떠 있는 동안에도 Claude Code 푸터에는 "esc to interrupt"가 남아 있을 수 있고, 먼저 매칭된 규칙이 이긴다.&lt;/p&gt;

&lt;h2&gt;
  
  
  어디까지 해 주나
&lt;/h2&gt;

&lt;p&gt;osFoundry는 프롬프트에 절대 대신 답하지 않는다. 다른 에이전트가 앱의 &lt;code&gt;runtime_send&lt;/code&gt; 도구로 pane에 입력할 수는 있지만, 결정을 기다리는 pane이면 거부당한다. 답하는 건 이름부터 다른 별도 도구 &lt;code&gt;runtime_answer&lt;/code&gt;를 거치고, 답할 때마다 사용자가 직접 확인한다. 이 검사는 마지막 스윕 결과가 아니라 입력하는 그 순간 화면을 새로 읽어서 한다. 그래서 방금 뜬 프롬프트가 평범한 텍스트로 치려던 키 입력을 먹어 버리는 일은 없다.&lt;/p&gt;

&lt;p&gt;osFoundry가 못 읽는 화면은 Idle로 보고하고, 절대 Needs you로 보내지 않는다. 이유는 &lt;code&gt;detect.rs&lt;/code&gt; 주석에 적혀 있다. 오탐이 한 번만 나도 사람들은 배지를 안 믿게 되고, 그다음부터는 진짜 알림까지 헛수고가 된다. 그래서 규칙이 애매할 때 앱은 더 많이가 아니라 더 적게 보고한다.&lt;/p&gt;

&lt;p&gt;규칙은 코드가 아니라 데이터다. 내려받은 규칙 파일이 바꿀 수 있는 건 어떤 패턴이 매칭되느냐뿐이다. 어떤 프로그램을 실행할지는 못 건드린다. 직접 덮어쓰고 싶으면 &lt;code&gt;~/.osfoundry/agent-detection/claude.toml&lt;/code&gt;에 두면 된다.&lt;/p&gt;

&lt;p&gt;터미널 텍스트는 데스크톱 앱 메모리 안에 머문다. pane 밖에 흔적이 남는 건 사용자가 끝낸 세션뿐이다. 그 요약이 지식 파일에 기록되고, 그다음에야 보관 처리된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다음에 해 볼 것
&lt;/h2&gt;

&lt;p&gt;다른 폴더에서 Claude Code를 하나 더 띄우고, 둘 다 수정이 필요한 작업을 맡겨 보자. 그리고 &lt;a href="https://osfoundry.io/ko/platform/agent-sessions" rel="noopener noreferrer"&gt;Sessions 보드&lt;/a&gt;를 Needs me only로 걸어 둔 채 다른 일을 하면 된다. 데스크톱 앱이 아직 없다면 &lt;a href="https://osfoundry.io/ko/download" rel="noopener noreferrer"&gt;다운로드 페이지&lt;/a&gt;에서 받을 수 있다.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>osfoundry</category>
      <category>tutorial</category>
      <category>terminal</category>
    </item>
    <item>
      <title>osFoundry는 왜 Claude Code의 툴 허용 목록을 CLI 플래그가 아니라 MCP 서버에 둘까</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:15:22 +0000</pubDate>
      <link>https://dev.to/bbt-ko/osfoundryneun-wae-claude-codeyi-tul-heoyong-mogrogeul-cli-peulraegeuga-anira-mcp-seobeoe-dulgga-1f7b</link>
      <guid>https://dev.to/bbt-ko/osfoundryneun-wae-claude-codeyi-tul-heoyong-mogrogeul-cli-peulraegeuga-anira-mcp-seobeoe-dulgga-1f7b</guid>
      <description>&lt;p&gt;&lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 데스크톱 앱은 Claude Code를 에이전트 모드 워커로 띄울 때 아래 MCP 설정을 넘긴다. 포트와 토큰은 내가 Python으로 다시 짠 브리지를 돌렸을 때 나온 값이다. 구조는 &lt;code&gt;src-tauri/src/mcp_bridge.rs&lt;/code&gt;와 같다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"osfoundry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:51377/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer 8dd5de…"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;볼 건 네 가지다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;호스트는 &lt;code&gt;127.0.0.1&lt;/code&gt;이다. &lt;code&gt;0.0.0.0&lt;/code&gt;이 아니다.&lt;/li&gt;
&lt;li&gt;포트는 OS가 고른다(&lt;code&gt;bind("127.0.0.1:0")&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;토큰은 이번 실행 하나만을 위해 만든 64자리 hex다.&lt;/li&gt;
&lt;li&gt;툴 목록은 어디에도 없다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;같이 넘기는 커맨드라인에도 &lt;code&gt;--allowedTools&lt;/code&gt;는 없다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;conversation&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; stream-json &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nt"&gt;--include-partial-messages&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; bypassPermissions &lt;span class="nt"&gt;--add-dir&lt;/span&gt; &amp;lt;folder&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mcp-config&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;위의 JSON&amp;gt;'&lt;/span&gt; &lt;span class="nt"&gt;--strict-mcp-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;허용 목록이 없는 게 아니라 서버 쪽에 있는 거다. 왜 그런지 정리하고, 직접 돌아가는 최소 버전을 만들어서 재 봤다.&lt;/p&gt;

&lt;h2&gt;
  
  
  허용 목록을 왜 서버에 두나
&lt;/h2&gt;

&lt;p&gt;에이전트 모드는 Claude Code를 &lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt;로 띄운다. 터미널 앞에서 한 단계씩 승인해 줄 사람이 없으니까. &lt;code&gt;build_claude_command&lt;/code&gt;의 주석에 허용 목록을 안 넘기는 이유가 적혀 있다. &lt;code&gt;bypassPermissions&lt;/code&gt;에서는 Claude Code가 &lt;code&gt;--allowedTools&lt;/code&gt;를 무시한다. 넘겨 봐야 장식이다. &lt;code&gt;mcp_bridge.rs&lt;/code&gt; 맨 위에도 같은 말이 한 줄로 박혀 있다. "Never trust the client; the server decides."&lt;/p&gt;

&lt;p&gt;이건 Claude Code만의 얘기가 아니다. 플래그는 클라이언트한테 주는 지시고, 클라이언트는 모델이 움직이는 코드 그 자체다. 절대 뚫리면 안 되는 체크 하나를 거기 두는 건 맞지 않다. 서버는 모든 &lt;code&gt;tools/call&lt;/code&gt;을 받고, 모델이 돌리는 코드도 아니다. 그래서 체크는 서버 몫이다.&lt;/p&gt;

&lt;p&gt;osFoundry는 두 군데서 거른다.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;tools/list&lt;/code&gt;는 이번 실행의 목록에 있는 툴만 돌려준다. 그 목록은 &lt;code&gt;agent_cli::tool_policy&lt;/code&gt;가 먼저 좁혀 둔다. 나머지 툴은 모델한테 존재 자체가 안 보인다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/call&lt;/code&gt;은 실행하기 전에 같은 목록으로 이름을 다시 확인한다. 모델은 본 적 없는 툴 이름도 내뱉을 수 있다. 학습 데이터에서 떠올린 이름일 수도 있고, 읽은 파일에 심어진 프롬프트 인젝션일 수도 있다. 1번은 이걸 못 막고, 2번이 막는다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  CLI는 처음에 뭘 보나
&lt;/h2&gt;

&lt;p&gt;POST는 세 번이고, 매번 새로 연결한다. 서버가 &lt;code&gt;Connection: close&lt;/code&gt;로 응답하기 때문이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;→ initialize                      ← 200 {"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"osfoundry",…}}
→ notifications/initialized       ← 202, Content-Length: 0
→ tools/list                      ← 200 {"tools":[{"name":"memory_search","description":…,"inputSchema":{…}}]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;바디 없는 202는 괜한 고집이 아니다. Rust 쪽 기록에 따르면 Codex의 MCP 클라이언트(rmcp)는 알림에 &lt;code&gt;{}&lt;/code&gt;와 200이 오면 트랜스포트를 내려 버렸다. 그 바람에 실행이 핸드셰이크에서 통째로 죽었다고 한다. 삽질하기 딱 좋은 부분이다. Claude Code는 넘어가 주지만 Codex는 안 넘어간다. 그래서 &lt;code&gt;id&lt;/code&gt;가 없는 메시지와 &lt;code&gt;notifications/&lt;/code&gt;로 시작하는 메서드에는 전부 바디 없는 202를 준다.&lt;/p&gt;

&lt;p&gt;그다음 Claude Code는 툴을 &lt;code&gt;mcp__&amp;lt;서버&amp;gt;__&amp;lt;툴&amp;gt;&lt;/code&gt;로 부른다. &lt;code&gt;memory_search&lt;/code&gt;는 &lt;code&gt;mcp__osfoundry__memory_search&lt;/code&gt;가 된다. 가운데 &lt;code&gt;osfoundry&lt;/code&gt;는 설정에 적힌 서버 이름에서 온다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다시 만들어 보기
&lt;/h2&gt;

&lt;p&gt;이 Mac에는 &lt;code&gt;claude&lt;/code&gt; 바이너리가 없다. 그래서 클라이언트는 스크립트로 흉내 냈다. 헤드리스 에이전트가 HTTP MCP 서버에 하는 일을 그대로 따라 한다. &lt;code&gt;--mcp-config&lt;/code&gt;를 파싱하고, 핸드셰이크를 하고, 툴을 부른다. 서버는 Rust 브리지의 로직을 Python 표준 라이브러리만으로 옮겼다. &lt;code&gt;bridge.py&lt;/code&gt;, &lt;code&gt;fake_cli.py&lt;/code&gt;, &lt;code&gt;run_demo.py&lt;/code&gt; 세 파일이고, 다 합쳐 230줄 정도다.&lt;/p&gt;

&lt;p&gt;핵심은 &lt;code&gt;bridge.py&lt;/code&gt;의 이 부분이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;  &lt;span class="c1"&gt;# bound now, never read from a request
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REGISTRY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;# 64 hex chars, new every run
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
            &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unauthorized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notifications/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="bp"&gt;...&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# the only gate that counts
&lt;/span&gt;                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rpc_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is not available to this agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rpc_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}]}))&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# loopback, OS-assigned port
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;execute_tool&lt;/code&gt;은 &lt;code&gt;orchestrator::execute_tool&lt;/code&gt; 자리를 대신한다. 앱에서는 어디서 온 툴 호출이든 이 함수 하나를 거치고, 승인, 플랜 모드, 채널별 오버라이드, 샌드박스가 다 여기서 걸린다. 브리지는 자기만의 실행 경로가 없다. 이 함수까지 보낼지 말지만 정한다.&lt;/p&gt;

&lt;p&gt;레지스트리에는 &lt;code&gt;memory_search&lt;/code&gt;, &lt;code&gt;execute_command&lt;/code&gt;, &lt;code&gt;delete_file&lt;/code&gt; 세 개를 넣고, &lt;code&gt;allowed=["memory_search"]&lt;/code&gt;로 실행을 시작했다. 가짜 CLI로 네 번 찔러 봤다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools seen by the CLI: ['mcp__osfoundry__memory_search']
call 1: memory_search {"query":"budget"}
  → {"result":{"content":[{"type":"text","text":"Q4 budget review: Thu 14:00."}]}}
call 2: execute_command {"cmd":"rm -rf ~/work"}
  → {"error":{"code":-32601,"message":"tool 'execute_command' is not available to this agent"}}
call 3: memory_search {"query":"contract","workspace_id":"ws-client"}
  → {"result":{"content":[{"type":"text","text":"no matching notes"}]}}
call 4 (wrong token): (401, {'error': 'unauthorized'})
audit: [('ws-personal','ch-ops','memory_search',{'query':'budget'}),
        ('ws-personal','ch-ops','memory_search',{'query':'contract','workspace_id':'ws-client'})]
after the run: connect to 51377 -&amp;gt; refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;call 2가 플래그로는 못 막는 경우다. 모델이 목록에 없던 툴 이름을 대고, 서버가 &lt;code&gt;execute_tool&lt;/code&gt; 앞에서 거절한다. audit에 &lt;code&gt;execute_command&lt;/code&gt;가 한 번도 안 찍힌 게 증거다.&lt;/p&gt;

&lt;p&gt;call 3은 인자로 다른 워크스페이스를 노린다. "Client X contract renews 2027-01-31"이라는 노트는 실제로 있지만 &lt;code&gt;ws-client&lt;/code&gt;에만 있다. 브리지는 자기가 고정해 둔 &lt;code&gt;workspace_id&lt;/code&gt;를 넘기기 때문에 검색은 &lt;code&gt;ws-personal&lt;/code&gt;에서 돌고, 아무것도 안 나온다. Rust 쪽도 &lt;code&gt;workspace_id&lt;/code&gt;와 &lt;code&gt;channel_id&lt;/code&gt;는 &lt;code&gt;start()&lt;/code&gt; 시점부터 &lt;code&gt;SessionCtx&lt;/code&gt;에 들어 있고, 요청에서 읽는 곳은 없다.&lt;/p&gt;

&lt;p&gt;마지막 줄은 실행이 끝나면 엔드포인트도 사라지는지 확인한 거다. 앱에서는 &lt;code&gt;McpSession&lt;/code&gt;의 &lt;code&gt;Drop&lt;/code&gt;이 셧다운 신호를 보낸다. panic이 나든 중간에 return하든, 툴을 돌릴 수 있는 리스너가 남지 않는다.&lt;/p&gt;

&lt;h2&gt;
  
  
  비용은 얼마나 드나
&lt;/h2&gt;

&lt;p&gt;숫자는 전부 &lt;code&gt;run_demo.py&lt;/code&gt;로 쟀다. 환경은 Apple M1(8 GB), Python 3.9.6이고, 다른 렌더 작업이 돌고 있어서 load average가 10 안팎이었다. 실제 브리지가 &lt;code&gt;Connection: close&lt;/code&gt;로 강제하는 것에 맞춰 요청마다 TCP 연결을 새로 열었다. 시간은 호출 앞뒤를 &lt;code&gt;time.perf_counter()&lt;/code&gt;로 감싼 실제 시간이다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;브리지 시작(bind + 서버 스레드)&lt;/td&gt;
&lt;td&gt;1.20 ms&lt;/td&gt;
&lt;td&gt;2.69 ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;핸드셰이크(initialize, 알림, tools/list)&lt;/td&gt;
&lt;td&gt;1.82 ms&lt;/td&gt;
&lt;td&gt;4.98 ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tools/call&lt;/code&gt; 허용&lt;/td&gt;
&lt;td&gt;0.41 ms&lt;/td&gt;
&lt;td&gt;1.06 ms&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tools/call&lt;/code&gt; 허용 목록에서 거절&lt;/td&gt;
&lt;td&gt;0.41 ms&lt;/td&gt;
&lt;td&gt;1.08 ms&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;토큰 틀림, 401&lt;/td&gt;
&lt;td&gt;0.40 ms&lt;/td&gt;
&lt;td&gt;1.09 ms&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종료 &lt;code&gt;serve_forever(poll_interval=0.5)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;500.91 ms&lt;/td&gt;
&lt;td&gt;501.98 ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종료 &lt;code&gt;poll_interval=0.01&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;10.88 ms&lt;/td&gt;
&lt;td&gt;11.07 ms&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;실행마다 브리지를 새로 띄워도 시작과 핸드셰이크를 합쳐 3 ms 정도다. Claude Code 프로세스는 부팅만 해도 이보다 훨씬 오래 걸리니까, 실행별 서버는 사실상 공짜다. 거절하는 비용도 허용과 똑같다. 체크는 리스트 조회 한 번이라 TCP 왕복에 묻혀 버린다.&lt;/p&gt;

&lt;p&gt;유일하게 걸린 건 종료였다. Python의 &lt;code&gt;serve_forever&lt;/code&gt;는 &lt;code&gt;poll_interval&lt;/code&gt;마다 한 번씩만 셧다운 요청을 확인한다. 기본값이 0.5초라서 실행이 끝날 때마다 반 초씩 기다렸다. Rust 버전에는 이 문제가 없다. accept 루프가 &lt;code&gt;accept()&lt;/code&gt;와 &lt;code&gt;watch&lt;/code&gt; 채널을 &lt;code&gt;tokio::select!&lt;/code&gt;로 묶고 있어서 신호가 오면 바로 빠져나온다. Python으로 짤 거면 &lt;code&gt;poll_interval&lt;/code&gt;을 줄이거나, 실행 종료 경로에서 소켓을 직접 닫으면 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  왜 stdio가 아니라 HTTP인가
&lt;/h2&gt;

&lt;p&gt;Claude Code에 붙이는 MCP 서버는 보통 stdio 프로세스다. osFoundry가 그렇게 안 한 이유는 &lt;code&gt;mcp_bridge.rs&lt;/code&gt;에 나와 있다. 툴 실행기는 앱의 &lt;code&gt;AppHandle&lt;/code&gt;이 필요한데, 이건 직렬화도 안 되고 다른 프로세스로 넘길 수도 없다. stdio 서버를 따로 붙여 봐야 모든 호출을 앱으로 다시 넘겨주는 프로세스가 하나 늘 뿐이다. Claude Code는 bearer 헤더가 붙은 &lt;code&gt;"type": "http"&lt;/code&gt; 서버를 지원하니까, 앱이 자기 안에서 루프백 포트로 MCP를 서빙한다. 대가로 포트와 토큰이 생기고, 그래서 둘 다 실행마다 새로 만든다.&lt;/p&gt;

&lt;h2&gt;
  
  
  직접 붙여 볼 거라면
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;허용 목록은 서버의 &lt;code&gt;tools/call&lt;/code&gt; 핸들러에 두고, &lt;code&gt;tools/list&lt;/code&gt;도 같은 목록으로 거른다. &lt;code&gt;--allowedTools&lt;/code&gt; 같은 CLI 플래그는 힌트로만 본다. "bypass" 계열 권한 모드에서는 특히 그렇다.&lt;/li&gt;
&lt;li&gt;워크스페이스, 사용자, 채널은 세션 시작 때 고정한다. 툴 인자나 클라이언트가 쓸 수 있는 헤더에서 가져오지 않는다.&lt;/li&gt;
&lt;li&gt;bearer는 실행마다 새로 만들고 상수 시간으로 비교한다(&lt;code&gt;hmac.compare_digest&lt;/code&gt;). 오래 쓰는 에이전트 토큰을 툴 실행 엔드포인트에 재활용하지 않는다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1:0&lt;/code&gt;으로 bind해서 포트는 OS한테 맡긴다. 리스너 수명은 자식 프로세스에 묶고, 종료가 정말 바로 끝나는지도 확인한다.&lt;/li&gt;
&lt;li&gt;알림에는 바디 없는 202를 준다. 다른 응답이 오면 연결을 끊는 클라이언트가 있다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--strict-mcp-config&lt;/code&gt;(또는 그 CLI에서 같은 역할을 하는 옵션)를 넘겨서, 범위를 좁혀 둔 실행에 사용자 본인의 MCP 서버가 끼어들지 않게 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLI가 아닌 에이전트에서 osFoundry가 툴 범위를 어떻게 좁히는지는 &lt;a href="https://osfoundry.io/ko/platform/tool-scope" rel="noopener noreferrer"&gt;에이전트별 툴 스코프&lt;/a&gt;에 있다. 외부 AI의 키와 구독 로그인을 어떻게 다루는지는 &lt;a href="https://osfoundry.io/ko/platform/byok-keys" rel="noopener noreferrer"&gt;BYOK 페이지&lt;/a&gt;에 정리돼 있다.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>osFoundry는 Claude Code의 승인 대기를 어떻게 잡아낼까</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:05:50 +0000</pubDate>
      <link>https://dev.to/bbt-ko/osfoundryneun-claude-codeyi-seungin-daegireul-eoddeohge-jabanaelgga-h4a</link>
      <guid>https://dev.to/bbt-ko/osfoundryneun-claude-codeyi-seungin-daegireul-eoddeohge-jabanaelgga-h4a</guid>
      <description>&lt;p&gt;Claude Code 2.1.303이 명령 실행 권한을 물어볼 때 터미널로 나오는 바이트는 이렇다. osFoundry가 테스트용으로 저장해 둔 실제 캡처(&lt;code&gt;resources/agent-detection/fixtures/claude.blocked.raw&lt;/code&gt;)에서 잘라 왔다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\r\x1b[1C\x1b[1B\x1b[39mDo\x1b[5Gyou\x1b[9Gwant\x1b[14Gto\x1b[17Gproceed?
\r\x1b[1C\x1b[1B\x1b[38;2;177;185;249m❯\x1b[4G\x1b[38;2;153;153;153m1. \x1b[38;2;177;185;249mYes
\r\x1b[3C\x1b[1B\x1b[38;2;153;153;153m2. \x1b[39mYes,\x1b[12Gand\x1b[16Galways\x1b[23Gallow ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(읽기 편하게 줄을 나눴다. 원본에는 줄바꿈이 없다.)&lt;/p&gt;

&lt;p&gt;여기서 흔히 짜는 감지 로직은 두 번 막힌다. 먼저 "Do"와 "you" 사이에 공백 문자가 없다. &lt;code&gt;\x1b[5G&lt;/code&gt;로 커서를 5번째 칸에 옮길 뿐이고, 공백처럼 보이는 건 아무것도 안 쓴 셀이다. 그리고 &lt;code&gt;\n&lt;/code&gt;이 하나도 없다. 줄마다 &lt;code&gt;\r&lt;/code&gt;과 커서 아래 이동 &lt;code&gt;\x1b[1B&lt;/code&gt;로 시작한다. fixtures 폴더에 있는 Claude Code 캡처 11개(각 2,099~7,672바이트) 모두 개행 바이트가 0개였다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 데스크톱 앱은 Sessions 보드에 올라간 터미널 pane마다 "이 에이전트가 지금 내 대답을 기다리나?"를 판정한다. 이 글에서는 그 구조를 뜯어보고, 흔한 방식 세 가지(프롬프트 문구 매칭, 출력이 멈춘 시간 타이머, 프로세스 상태)를 Python으로 짠 작은 PTY 슈퍼바이저로 비교했다. 이 Mac에는 &lt;code&gt;claude&lt;/code&gt; 바이너리가 없어서, 라이브 측정은 Claude Code의 문구와 화면 그리는 습관을 흉내 낸 가짜 에이전트로 돌렸다. fixture 검증은 실제 캡처 바이트 그대로다.&lt;/p&gt;

&lt;h2&gt;
  
  
  osFoundry는 뭘 매칭하나
&lt;/h2&gt;

&lt;p&gt;에이전트별 TOML 파일이다. Claude Code용은 &lt;code&gt;apps/web/src-tauri/resources/agent-detection/claude.toml&lt;/code&gt;이고, 같은 파일이 &lt;code&gt;https://osfoundry.io/agent-detection/claude.toml&lt;/code&gt;에도 올라가 있다. 규칙은 이 순서다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"blocked"&lt;/span&gt;
&lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'Do you want to (proceed|continue|make this edit|create)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'❯\s*1\.\s*Yes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'Esc to cancel · Tab to amend'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'Enter to select · ↑/↓ to navigate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'Exit plan mode\?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"working"&lt;/span&gt;
&lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'esc to interrupt'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"idle"&lt;/span&gt;
&lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'\? for shortcuts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'(?:⏸|⏵⏵) [a-z ]+ mode on'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;처음 매칭된 규칙이 이긴다(&lt;code&gt;runtime/detect.rs&lt;/code&gt;의 &lt;code&gt;CompiledManifest::evaluate&lt;/code&gt;). &lt;code&gt;blocked&lt;/code&gt;가 맨 위인 이유는, 프롬프트가 떠 있는 동안에도 푸터에 "esc to interrupt"가 남아 있어서다. &lt;code&gt;working&lt;/code&gt;을 먼저 보면 프롬프트는 영영 안 잡힌다. 어떤 규칙에도 안 걸린 화면은 &lt;code&gt;idle&lt;/code&gt;이 되고, 절대 &lt;code&gt;blocked&lt;/code&gt;로 가지 않는다.&lt;/p&gt;

&lt;p&gt;파일 주석에는 뺀 패턴과 이유도 적혀 있다. &lt;code&gt;Would you like to&lt;/code&gt;는 Codex의 프롬프트 문구고, Claude는 평범한 답변에서도 "Would you like me to…"를 자주 써서 뺐다고 한다.&lt;/p&gt;

&lt;p&gt;판정은 출력이 올 때마다 하지 않는다. &lt;code&gt;runtime/mod.rs&lt;/code&gt;가 700 ms마다 모든 pane을 한 바퀴 돈다(&lt;code&gt;SWEEP_INTERVAL&lt;/code&gt;). 스피너는 1초에 여러 번 다시 그려지니까, 쓸 때마다 판정하면 같은 답을 내려고 정규식을 초당 수백 번 돌리게 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  출력 끝 몇 KB를 grep하면 안 되나
&lt;/h2&gt;

&lt;p&gt;맨 위 바이트가 답이다. 규칙을 Python으로 옮기고 Claude Code fixture를 두 가지 방식으로 판정해 봤다. 하나는 마지막 8 KB에서 이스케이프 시퀀스를 지우고 줄마다 마지막 &lt;code&gt;\r&lt;/code&gt; 뒤만 남기는 방식. 다른 하나는 바이트를 32×120 화면 에뮬레이터(100줄 정도, 아래 &lt;code&gt;vt.py&lt;/code&gt;)에 넣고 행을 다시 읽는 방식이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixture                            truth    last 8 KB, stripped    emulated 32x120 screen
claude.blocked.edit.raw            blocked  idle  ✗                blocked
claude.blocked.plan.raw            blocked  idle  ✗                blocked
claude.blocked.question.raw        blocked  idle  ✗                blocked
claude.blocked.raw                 blocked  idle  ✗                blocked
claude.idle.after.raw              idle     idle                   idle
claude.idle.auto.raw               idle     idle                   idle
claude.idle.edit.raw               idle     idle                   idle
claude.idle.question.raw           idle     idle                   idle
claude.idle.raw                    idle     idle                   idle
claude.working.auto.raw            working  idle  ✗                working
claude.working.raw                 working  idle  ✗                working
correct: tail 5/11, screen 11/11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tail 방식이 &lt;code&gt;idle&lt;/code&gt;을 다 맞힌 건 &lt;code&gt;idle&lt;/code&gt;이 기본값이라서일 뿐이다. 프롬프트는 한 번도 못 봤다. &lt;code&gt;\n&lt;/code&gt;이 없으니 캡처 전체가 한 "줄"이 되고, 마지막 &lt;code&gt;\r&lt;/code&gt; 뒤만 남기면 끝 조각밖에 안 남는다. 그 처리를 빼도 단어가 붙어서 &lt;code&gt;Doyouwanttoproceed?&lt;/code&gt;가 된다.&lt;/p&gt;

&lt;p&gt;osFoundry 코드도 같은 결론이다. &lt;code&gt;terminal_pty.rs&lt;/code&gt;는 세션마다 &lt;code&gt;vt100::Parser&lt;/code&gt;를 하나씩 들고 있고, 판정에는 에뮬레이트한 화면만 돌려주는 &lt;code&gt;screen_text()&lt;/code&gt;를 쓴다. 화면 위로 남겨 두는 히스토리 200행은 일부러 판정에서 뺐다. 주석에 따르면, 이미 대답해서 위로 스크롤된 프롬프트를 읽으면 에이전트가 "계속 blocked"로 남아 버린다.&lt;/p&gt;

&lt;p&gt;에뮬레이터는 잘 만들 필요가 없다. 여기서 쓴 건 문자, &lt;code&gt;\r \n \b&lt;/code&gt;, 커서 이동(&lt;code&gt;A B C D E F G H f d&lt;/code&gt;), 지우기(&lt;code&gt;J K&lt;/code&gt;)만 처리하고 색·모드·OSC 타이틀은 건너뛴다. 셀 하나에 문자 하나라서 전각 문자에서 칸이 밀릴 수는 있는데, 행 전체에 정규식을 거는 데는 상관없다. 이것만으로 11개 캡처 모두 규칙이 먹힐 만큼 재현됐다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CSI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\x1b\[([0-9;?]*)([ -/]*)([@-~])&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OSC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OSC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CSI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_csi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# move x/y or blank cells
&lt;/span&gt;                    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;                        &lt;span class="c1"&gt;# ESC 7, ESC =, ...
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_lf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                  &lt;span class="c1"&gt;# scrolls at the bottom row
&lt;/span&gt;            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_lf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  출력이 멈추면 대기 중이라고 보면 안 되나
&lt;/h2&gt;

&lt;p&gt;다들 제일 먼저 짜 보는 방식이다. N초 동안 아무것도 안 찍으면 아마 날 기다리는 거겠지. 화면 규칙과 비교하려고, PTY에 에이전트를 띄우고 50 ms마다 감지기별 판정을 로그로 남기는 슈퍼바이저를 붙였다.&lt;/p&gt;

&lt;p&gt;에이전트는 &lt;code&gt;fake_agent.py&lt;/code&gt;라는 대역이다(이 Mac에는 &lt;code&gt;claude&lt;/code&gt; 바이너리가 없다). 결과에 영향을 주는 습관만 흉내 냈다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;\r&lt;/code&gt;로 10 Hz 다시 그리는 스피너. 프레임당 60바이트, "esc to interrupt" 포함&lt;/li&gt;
&lt;li&gt;실행 중에 아무것도 안 찍는 도구 호출(0.5~6초). CLI 상당수가 이렇다&lt;/li&gt;
&lt;li&gt;Claude Code 문구 그대로의 권한 프롬프트. 실제 캡처처럼 단어마다 &lt;code&gt;CSI n G&lt;/code&gt;로 배치하고, 대답하면 Ink처럼 커서를 올려 &lt;code&gt;\x1b[J&lt;/code&gt;로 지운다&lt;/li&gt;
&lt;li&gt;턴이 끝나면 &lt;code&gt;? for shortcuts&lt;/code&gt;가 붙은 대기 상태. 가끔 "Would you like me to add a regression test?"로 끝나는 답변 뒤에 온다&lt;/li&gt;
&lt;li&gt;프롬프트 중에도 대기 중에도 프로세스는 PTY의 &lt;code&gt;readline()&lt;/code&gt;에서 블록돼 있다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;실제 상태는 별도 파일에 쓰고, 채점 스크립트만 그걸 읽는다. 사용자 역할 봇은 프롬프트가 뜨고 3~8초 뒤에 대답하고, 턴이 끝나고 3~10초 뒤에 다음 작업을 친다. 슈퍼바이저 핵심만 추리면 이렇다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fake_agent.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TIOCSWINSZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HHHH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Screen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;codecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getincrementaldecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;last_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;next_tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# every 50 ms
&lt;/span&gt;        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;         &lt;span class="c1"&gt;# the rules above, on the emulated screen
&lt;/span&gt;            &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;strip_tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# the same rules, on the stripped 8 KB tail
&lt;/span&gt;            &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# for the idle timers
&lt;/span&gt;        &lt;span class="n"&gt;next_tick&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;seed 7로 한 번 돌렸다. 작업 30개, 10.6분, 권한 프롬프트 25번, 기다린 시간 합계 144초. 지연은 프롬프트가 그려지고 감지기가 "대기 중"이라고 말하기까지 걸린 시간, 잔류는 대답한 뒤에도 그렇게 말한 시간이다. 오탐은 "횟수 / 합계 초"로, 에이전트가 실제로 뭘 하고 있었는지로 나눴다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;감지기&lt;/th&gt;
&lt;th&gt;지연(중앙값)&lt;/th&gt;
&lt;th&gt;최대&lt;/th&gt;
&lt;th&gt;놓침&lt;/th&gt;
&lt;th&gt;잔류&lt;/th&gt;
&lt;th&gt;오탐(작업 중)&lt;/th&gt;
&lt;th&gt;오탐(대기 중)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;화면 규칙, 50 ms마다&lt;/td&gt;
&lt;td&gt;0.03 s&lt;/td&gt;
&lt;td&gt;0.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;화면 규칙, 700 ms 주기&lt;/td&gt;
&lt;td&gt;0.26 s&lt;/td&gt;
&lt;td&gt;0.70 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0.4 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 규칙을 끝 8 KB에&lt;/td&gt;
&lt;td&gt;0.03 s&lt;/td&gt;
&lt;td&gt;0.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;77.0 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;화면에 "?"로 끝나는 줄&lt;/td&gt;
&lt;td&gt;0.03 s&lt;/td&gt;
&lt;td&gt;0.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;58.0 s&lt;/td&gt;
&lt;td&gt;0 / 231 s&lt;/td&gt;
&lt;td&gt;4 / 143 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 0.5 s&lt;/td&gt;
&lt;td&gt;0.52 s&lt;/td&gt;
&lt;td&gt;0.56 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;31 / 78 s&lt;/td&gt;
&lt;td&gt;30 / 168 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 1 s&lt;/td&gt;
&lt;td&gt;1.02 s&lt;/td&gt;
&lt;td&gt;1.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;28 / 63 s&lt;/td&gt;
&lt;td&gt;30 / 153 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 2 s&lt;/td&gt;
&lt;td&gt;2.03 s&lt;/td&gt;
&lt;td&gt;2.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;21 / 38 s&lt;/td&gt;
&lt;td&gt;30 / 123 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 3 s&lt;/td&gt;
&lt;td&gt;3.02 s&lt;/td&gt;
&lt;td&gt;3.06 s&lt;/td&gt;
&lt;td&gt;0 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;17 / 19 s&lt;/td&gt;
&lt;td&gt;30 / 93 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 5 s&lt;/td&gt;
&lt;td&gt;5.02 s&lt;/td&gt;
&lt;td&gt;5.05 s&lt;/td&gt;
&lt;td&gt;9 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;3 / 1 s&lt;/td&gt;
&lt;td&gt;18 / 46 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출력 정지 ≥ 8 s&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;25 / 25&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;6 / 7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;idle 타이머에는 제대로 먹히는 임계값이 없다. 지연은 정의상 임계값 그 자체다. 5초 미만이면 조용히 도는 도구 호출마다 울린다. 3초 이하에서는 턴 종료 30번에 전부 울린다. "끝내고 다음 작업을 기다리는 중"은 "권한을 기다리는 중"과 똑같이 조용하니까. 5초 이상으로 올리면 제때 대답한 프롬프트를 놓치기 시작한다. 8초에서 25개를 다 놓친 건 봇이 8초 넘게 안 기다려서일 뿐이지만, 경향은 대답 속도와 상관없다. 시끄럽거나 늦거나 둘 중 하나다.&lt;/p&gt;

&lt;p&gt;"?"로 끝나는 줄은 프롬프트를 다 잡는다. 실제 캡처에서는 완벽해 보인다. 그런 줄이 있는 건 &lt;code&gt;blocked&lt;/code&gt; 4개뿐이다. 그런데 라이브에서는 에이전트가 직접 묻는 "Would you like me to add a regression test?"에도 반응하고, 그 줄은 다음 작업까지 화면에 남는다. &lt;code&gt;claude.toml&lt;/code&gt;이 &lt;code&gt;Would you like to&lt;/code&gt;를 뺀 것과 같은 이유다.&lt;/p&gt;

&lt;p&gt;끝 8 KB 방식이 이번 측정에서 프롬프트를 잡은 건 가짜 에이전트가 &lt;code&gt;❯ 1. Yes&lt;/code&gt;를 진짜 공백으로 찍어서다. 대신 대답한 뒤에도 평균 77초 동안 프롬프트가 있다고 우긴다. 지운 프롬프트의 바이트가 새 출력 8 KB에 밀려날 때까지 끝에 남아 있어서다. 스피너가 초당 600바이트인데 중간에 조용한 구간까지 있으니, 밀려나기까지 오래 걸린다. 에뮬레이트한 화면은 커서 이동과 지우기가 도착하는 순간 프롬프트를 잊는다.&lt;/p&gt;

&lt;p&gt;700 ms 주기의 비용은 예상대로다. 최대 한 주기만큼의 지연(중앙값 0.26초, 최대 0.70초)과 최대 한 주기만큼의 잔류. 사람이 보는 배지라면 이걸로 충분하다.&lt;/p&gt;

&lt;h2&gt;
  
  
  OS한테 "stdin 읽는 중이냐"고 물어보면 안 되나
&lt;/h2&gt;

&lt;p&gt;macOS에서는 안 됐다. 슈퍼바이저가 &lt;code&gt;ps -o stat=,time=&lt;/code&gt;를 초당 2번 찍어 뒀다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;working  samples  603  state S  99.5%  R  0.5%  CPU 0.04% of a core
idle     samples  357  state S 100.0%  R  0.0%  CPU 0.02% of a core
blocked  samples  280  state S 100.0%  R  0.0%  CPU 0.02% of a core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;터미널 &lt;code&gt;read()&lt;/code&gt;에서 블록된 프로세스도, 스피너 프레임 사이에 자고 있는 프로세스도 둘 다 &lt;code&gt;S&lt;/code&gt;다. 시스템에 따라 커널 대기 채널 이름이 나오는 &lt;code&gt;wchan&lt;/code&gt; 열도 둘 다 &lt;code&gt;-&lt;/code&gt;였다. 스피너는 CPU를 거의 안 먹어서 CPU 시간으로도 못 가른다. Claude Code 같은 Node 기반 CLI는 작업 중이든 아니든 이벤트 루프에서 기다리니까, 상태 문자로 알 수 있는 건 더 적을 거다.&lt;/p&gt;

&lt;p&gt;프로세스 테이블로 확실히 알 수 있는 건 누가 터미널을 쥐고 있느냐다. osFoundry는 &lt;code&gt;runtime/signals.rs&lt;/code&gt;에서 터미널의 포그라운드 프로세스 그룹(&lt;code&gt;MasterPty::process_group_leader&lt;/code&gt;, 즉 &lt;code&gt;tcgetpgrp&lt;/code&gt;)을 읽는다. 그래서 그냥 셸에 친 &lt;code&gt;claude&lt;/code&gt;도 알아보고, 에이전트가 종료돼서 셸이 다시 앞으로 나온 것도 안다. 화면에는 마지막 프롬프트가 그대로 남아 있어도 말이다.&lt;/p&gt;

&lt;h2&gt;
  
  
  화면 읽는 것보다 나은 방법은 없나
&lt;/h2&gt;

&lt;p&gt;있다. 에이전트가 직접 말하게 하면 된다. &lt;code&gt;runtime/signals.rs&lt;/code&gt;는 정보원 세 개를 맞춰 본다. osFoundry가 Claude Code를 직접 띄울 때는 &lt;code&gt;runtime/integrations.rs&lt;/code&gt;가 &lt;code&gt;--settings&lt;/code&gt;로 훅을 붙인다. 이 플래그는 그 실행 한 번에만 설정을 더하는 거라, 사용자 본인의 훅도 그대로 돈다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osf-pane report working &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true"&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osf-pane report working &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true"&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osf-pane notify &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true"&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osf-pane report done &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true"&lt;/span&gt;&lt;span class="p"&gt;}]}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code의 &lt;code&gt;Notification&lt;/code&gt;은 권한 프롬프트 때도, 입력 대기가 1분 이어졌을 때도 발생한다. 그래서 &lt;code&gt;osf-pane notify&lt;/code&gt;가 payload를 보고 &lt;code&gt;permission&lt;/code&gt;이면 blocked, 나머지는 idle로 나눈다. ACP(&lt;code&gt;claude.toml&lt;/code&gt;에 선언된 &lt;code&gt;claude-agent-acp&lt;/code&gt; 어댑터 경유)라면 &lt;code&gt;session/request_permission&lt;/code&gt; 요청 자체가 권한 요청이라, 추측이 아니라 사실이 된다.&lt;/p&gt;

&lt;p&gt;재밌는 건 &lt;code&gt;signals::combine&lt;/code&gt;의 우선순위다. 화면에서 읽은 &lt;code&gt;blocked&lt;/code&gt;는 항상 이긴다. 그다음이 에이전트의 최신 보고, 그다음이 "셸이 다시 앞에 있음", 마지막이 나머지 화면 규칙이다. 보고는 오래될 수도 있고, 훅이 조용히 실패해서 아예 안 올 수도 있다. 반면 화면 위 프롬프트는 정확한 규칙이고, 절대 묻히면 안 되는 단 하나의 알림이다.&lt;/p&gt;

&lt;p&gt;여기서 작은 설계 두 개가 따라 나온다. 알림은 세션·상태별로 30초 쿨다운이 있다(&lt;code&gt;runtime/notify.rs&lt;/code&gt;). 다시 그릴 때마다 &lt;code&gt;blocked → working → blocked&lt;/code&gt;로 깜빡일 수 있어서다. 그리고 도구가 pane에 글자를 치기 전에는 &lt;code&gt;runtime/tools.rs&lt;/code&gt;가 직전 주기 결과를 믿지 않고 &lt;code&gt;classify_session&lt;/code&gt;을 다시 돌린다. &lt;code&gt;blocked&lt;/code&gt;인 pane은 사용자가 확인하는 &lt;code&gt;runtime_answer&lt;/code&gt;만 받고, 일반 &lt;code&gt;runtime_send&lt;/code&gt;는 막힌다.&lt;/p&gt;

&lt;h2&gt;
  
  
  직접 붙인다면
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;바이트 끝부분이 아니라 에뮬레이트한 화면으로 판정한다. Claude Code는 단어를 커서 이동으로 배치하고 개행을 안 쓴다. 대답한 프롬프트는 바이트에 몇 분씩 남는다.&lt;/li&gt;
&lt;li&gt;"물음표가 있다", "N초 조용하다" 말고 프롬프트 고유의 문구와 푸터(&lt;code&gt;Esc to cancel · Tab to amend&lt;/code&gt;)에 매칭한다. 턴 종료도, 조용한 도구 실행도 밖에서 보면 프롬프트와 똑같다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blocked&lt;/code&gt; 규칙은 &lt;code&gt;working&lt;/code&gt;보다 위에 두고, "아무것도 안 걸림"은 대기로 처리한다. 틀린 "대기 중"이 늦은 "대기 중"보다 비싸다.&lt;/li&gt;
&lt;li&gt;규칙은 릴리스 없이 갈아 끼울 수 있는 데이터 파일로 두고, 실행할 프로그램은 거기서 못 바꾸게 한다.&lt;/li&gt;
&lt;li&gt;프로세스 테이블은 "누가 돌고 있나"와 "끝났나"에만 쓰고 "기다리나"에는 쓰지 않는다. 직접 띄운다면 훅이나 ACP를 쓰되, 화면 위 프롬프트를 보고보다 우선한다.&lt;/li&gt;
&lt;li&gt;사람이 볼 거라면 수백 ms 주기면 충분하다. pane에 입력하기 직전에만 다시 판정한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;코드는 &lt;code&gt;vt.py&lt;/code&gt;, &lt;code&gt;rules.py&lt;/code&gt;, &lt;code&gt;fake_agent.py&lt;/code&gt;, &lt;code&gt;supervisor.py&lt;/code&gt; 네 파일에, 표를 만드는 &lt;code&gt;fixtures.py&lt;/code&gt;, &lt;code&gt;analyze.py&lt;/code&gt;가 전부다. 결과가 보이는 화면은 osFoundry 데스크톱 앱의 &lt;a href="https://osfoundry.io/ko/platform/agent-sessions" rel="noopener noreferrer"&gt;에이전트 세션 보드&lt;/a&gt;다.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>terminal</category>
      <category>agents</category>
    </item>
    <item>
      <title>Codex CLI가 osFoundry 노트와 지식 베이스를 MCP로 읽게 하는 방법</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:55:21 +0000</pubDate>
      <link>https://dev.to/bbt-ko/codex-cliga-osfoundry-noteuwa-jisig-beiseureul-mcpro-ilgge-haneun-bangbeob-39a5</link>
      <guid>https://dev.to/bbt-ko/codex-cliga-osfoundry-noteuwa-jisig-beiseureul-mcpro-ilgge-haneun-bangbeob-39a5</guid>
      <description>&lt;p&gt;목표는 Codex CLI가 저장소에서 작업하다가 &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 프로젝트에 있는 스펙 노트, 지원용 지식 베이스, 버그 레코드를 직접 찾아보는 상태다. 읽기는 osFoundry MCP 서버를 거치고, 뭔가를 쓰기 전에는 꼭 확인을 받는다. Codex가 한 호출은 전부 osFoundry 쪽 연결의 Recent activity(최근 활동)에 남는다. 할 일은 세 가지다. &lt;code&gt;~/.codex/config.toml&lt;/code&gt;에 테이블 하나 추가, 브라우저에서 로그인 한 번, 클릭 한 번으로 연결을 프로젝트에 넣기.&lt;/p&gt;

&lt;h2&gt;
  
  
  누가 뭘 맡나
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex 쪽&lt;/th&gt;
&lt;th&gt;osFoundry 쪽&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인&lt;/td&gt;
&lt;td&gt;평소 쓰던 Codex 로그인 그대로. osFoundry OAuth 토큰은 Codex가 들고 있다&lt;/td&gt;
&lt;td&gt;osFoundry 계정으로 연결을 한 번 승인한다. 워크스페이스는 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델을 돌리는 쪽&lt;/td&gt;
&lt;td&gt;내 머신의 Codex, 내 Codex 요금제로 돈다&lt;/td&gt;
&lt;td&gt;없음. 이 경로에는 모델도 프로바이더 키도 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;툴이 실행되는 곳&lt;/td&gt;
&lt;td&gt;Codex가 HTTPS로 툴 호출을 보낸다&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api.osfoundry.io&lt;/code&gt;가 권한부터 확인하고 실행한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex에 보이는 툴&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;config.toml&lt;/code&gt;의 &lt;code&gt;enabled_tools&lt;/code&gt; / &lt;code&gt;disabled_tools&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;연결별 Tools 체크리스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;닿는 범위&lt;/td&gt;
&lt;td&gt;서버가 돌려준 것만&lt;/td&gt;
&lt;td&gt;연결을 추가한 프로젝트, 그리고 초대한 채널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;승인&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;default_tools_approval_mode&lt;/code&gt;로 정해지는 Codex 자체 확인 창&lt;/td&gt;
&lt;td&gt;GitNest와 Room 변경은 "Needs your approval"(승인 필요)에서 대기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터가 남는 곳&lt;/td&gt;
&lt;td&gt;답변과 diff는 Codex 세션에 남는다&lt;/td&gt;
&lt;td&gt;노트, 레코드, 문서는 osFoundry에 남는다. 로컬 전용 항목은 기기 밖으로 안 나간다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  설정
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. osFoundry에서 연결 만들기
&lt;/h3&gt;

&lt;p&gt;osFoundry 헤더의 열쇠 아이콘을 눌러 Connected 탭을 연다. 제목은 "Connected agents"고 그 밑에 지금 워크스페이스 이름이 보인다. "Connect in three steps" 아래 폼에 이렇게 넣는다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assistant: Other MCP client&lt;/li&gt;
&lt;li&gt;Display name: 알아보기 쉬운 이름, 예를 들어 &lt;code&gt;Codex CLI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add assistant(어시스턴트 추가)를 누른다&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;연결은 "Setup pending. This assistant cannot read or change anything yet." 상태로 목록에 뜬다. Server address를 복사해 둔다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvht1hpl3pi7zpgk2z0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvht1hpl3pi7zpgk2z0.jpg" alt="Codex CLI 연결을 선택한 Connected 탭" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: Codex CLI 연결을 선택한 Connected 탭&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Codex에 서버 추가하기
&lt;/h3&gt;

&lt;p&gt;OpenAI &lt;a href="https://learn.chatgpt.com/docs/extend/mcp?surface=cli" rel="noopener noreferrer"&gt;Codex MCP 문서&lt;/a&gt;에 나온 한 줄이면 된다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add osfoundry &lt;span class="nt"&gt;--url&lt;/span&gt; https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이러면 &lt;code&gt;[mcp_servers.osfoundry]&lt;/code&gt; 테이블이 생긴다. &lt;code&gt;~/.codex/config.toml&lt;/code&gt;을 열어서 좀 더 조여 두자. 아래 키는 전부 그 문서에 있는 것들이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mcp_servers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;osfoundry&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"https://api.osfoundry.io/api/v1/external-mcp"&lt;/span&gt;
&lt;span class="n"&gt;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"writes"&lt;/span&gt;
&lt;span class="n"&gt;enabled_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"list_granted_resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_project_resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_note_tabs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_note_tab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_sheet_range"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"search_knowledge_base"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_knowledge_article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_record_items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;줄마다 하는 일은 이렇다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scopes&lt;/code&gt;는 서버가 제시하는 스코프를 대신한다. 안 쓰면 Codex는 서버 챌린지나 protected resource metadata에서 스코프를 가져오는데, osFoundry 메타데이터에는 &lt;code&gt;osfoundry:run&lt;/code&gt;을 포함해 네 개가 있다. 레코드를 읽고 고치기만 하는 코딩 어시스턴트한테 세션 실행 권한까지는 필요 없다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt;는 read-only 표시가 없는 툴마다 확인 창을 띄운다. osFoundry는 툴에 &lt;code&gt;readOnlyHint&lt;/code&gt;를 달아 두기 때문에 읽기 툴 9개는 확인 없이 돌고 &lt;code&gt;edit_record_item&lt;/code&gt;만 먼저 물어본다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enabled_tools&lt;/code&gt;는 툴 목록을 짧게 유지한다. 서버가 내놓는 툴은 59개고, 이 목록은 10개다. 이름은 접두사 없는 MCP 원래 툴 이름으로 쓴다. 여기서 잘 삽질하는 게 오타다. 이름을 틀려도 에러가 안 나고, 그 툴이 그냥 없을 뿐이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;특정 저장소에서만 쓰고 싶으면 같은 테이블을 그 저장소의 &lt;code&gt;.codex/config.toml&lt;/code&gt;에 넣으면 된다. 다만 Codex는 trusted(신뢰함)로 표시한 프로젝트의 설정만 읽는다.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 로그인하기
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp login osfoundry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;브라우저에서 &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;가 열리고 "Connect an agent"라는 화면이 나온다. Assistant client와 "Returns to"가 보이는데, Codex가 콜백을 내 머신에서 받기 때문에 Returns to는 로컬 주소다. 그다음은 이렇다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace to connect: 하나를 고른다. 다른 워크스페이스는 연결을 따로 만들어야 한다.&lt;/li&gt;
&lt;li&gt;Requested capabilities: 위의 &lt;code&gt;scopes&lt;/code&gt;라면 "Read supported resources in projects you add"와 "Change supported resources where you have edit access" 두 줄이 보인다.&lt;/li&gt;
&lt;li&gt;Connection: "Use pending: Codex CLI"를 골라서 1단계에서 만든 연결에 로그인을 붙인다.&lt;/li&gt;
&lt;li&gt;"I approve connecting this assistant to &amp;lt;워크스페이스&amp;gt;"에 체크하고 Connect를 누른다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SSH로 들어간 원격 머신이라면 &lt;code&gt;codex mcp login osfoundry --no-browser&lt;/code&gt;가 URL을 대신 출력한다. 그 URL을 노트북에서 열고, 리다이렉트된 URL 전체를 터미널에 다시 붙여 넣으면 된다.&lt;/p&gt;

&lt;p&gt;osFoundry 서버에는 클라이언트 등록 엔드포인트가 있고 퍼블릭 클라이언트를 받아 주기 때문에, Codex가 알아서 등록한다. 클라이언트 ID를 붙여 넣을 일은 없다.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. 연결을 프로젝트에 넣기
&lt;/h3&gt;

&lt;p&gt;열쇠 아이콘 → Connected로 돌아오면 연결이 "Connected."로 바뀌어 있다. Projects에서 프로젝트를 고르고 Add to project(프로젝트에 추가)를 누른다. 그러면 그 프로젝트의 동기화된 노트, 지식 베이스, Record 채널, 클라우드 Room, 할당된 GitNest 저장소, 토론 채널이 열린다. 나중에 추가되는 것도 포함이다. 그 밖의 Message 채널은 하나씩 Invite(초대)해야 한다.&lt;/p&gt;

&lt;p&gt;Tools 섹션은 "All supported tools are on by default." 상태로 시작한다. 누가 나중에 &lt;code&gt;config.toml&lt;/code&gt;을 고쳐도 이 연결이 절대 부르면 안 되는 툴이 있으면 체크를 풀고 Save tools(툴 저장)를 누른다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanbasoj837a8uv5zu2e6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanbasoj837a8uv5zu2e6.jpg" alt="읽기 툴만 체크한 Tools 체크리스트와 Save tools" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: 읽기 툴만 체크한 Tools 체크리스트와 Save tools&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Codex에서 확인하기
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;codex&lt;/code&gt; TUI에서 &lt;code&gt;/mcp&lt;/code&gt;를 치면 활성화된 서버 목록이 나온다. 거기 &lt;code&gt;osfoundry&lt;/code&gt;가 있으면 된다. 그다음은 그냥 평소 말로 부탁하면 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실제로 돌려 보면
&lt;/h2&gt;

&lt;p&gt;이 글을 쓴 머신에는 Codex가 설치돼 있지 않아서, 아래 세션은 시뮬레이션한 예시다. 툴 이름은 라이브 서버가 &lt;code&gt;tools/list&lt;/code&gt;로 돌려주는 그대로다. Codex 소스는 모델에 보이는 이름에서 MCP 툴 앞에 &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&lt;/code&gt;를 붙이기 때문에 여기서도 그렇게 적었다. 프롬프트, 프로젝트, 답변은 데모용 내용이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;예시 (시뮬레이션)

&amp;gt; tests/test_refund.py에서 실패하는 테스트는 환불 기간을 14일로 보고 있어.
  고치기 전에 osFoundry 스펙이랑 지원 KB부터 확인해 줘.

• mcp__osfoundry__list_granted_resources {}
  ← 프로젝트 1개: "Billing v2"
• mcp__osfoundry__list_project_resources {"project_id": "…", "resource_type": "note", "search": "환불"}
  ← 노트 "환불 규칙 (스펙)"
• mcp__osfoundry__list_note_tabs {"note_id": "…"}
  ← 탭: Note (텍스트), Sheet "예외 케이스"
• mcp__osfoundry__read_note_tab {"note_id": "…", "tab_id": "…"}
  ← "환불은 청구일로부터 30일 이내에 접수한다…"
• mcp__osfoundry__search_knowledge_base {"knowledge_base_id": "…", "query": "환불 기간"}
  ← 문서 "환불 정책", 2일 전 업데이트: 30일
• edited src/billing/refund.py, tests/test_refund.py
• mcp__osfoundry__edit_record_item {"record_channel_id": "…", "item_id": "…",
      "values": {"status": "Fixed"}, "expected_version": "…", "idempotency_key": "…"}
  [Codex: 이 툴 호출을 허용할까요? → 예]
  ← saved

틀린 건 코드가 아니라 테스트였습니다. 스펙 노트와 KB 문서 둘 다
30일이라고 되어 있습니다. 테스트와 상수 주석을 고치고 pytest를 돌렸고
(12개 통과), "환불 기간이 16일 어긋남" 레코드를 Fixed로 바꿨습니다.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;edit_record_item&lt;/code&gt;에는 &lt;code&gt;read_record_item&lt;/code&gt;이나 &lt;code&gt;list_record_items&lt;/code&gt;가 돌려준 버전이 같이 실린다. 그사이 누가 레코드를 바꿨으면 덮어쓰지 않고 거절된다. 같은 &lt;code&gt;idempotency_key&lt;/code&gt;로 다시 보내면 첫 결과가 그대로 돌아오니까, 재시도해도 두 번 쓰이지 않는다. 중간의 승인 확인은 osFoundry가 아니라 Codex가 띄운 거고, &lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt; 덕분이다.&lt;/p&gt;

&lt;h2&gt;
  
  
  선은 어디에 있나
&lt;/h2&gt;

&lt;p&gt;Codex는 osFoundry를 읽지만, osFoundry는 Codex를 읽지 않는다. 이 연결은 Codex가 필요하다고 판단할 때 툴을 부를 수 있게 해 줄 뿐이다. osFoundry는 호출마다 툴 이름, 권한 판정, 시각을 Recent activity에 남기지만, Codex 세션이나 저장소, 터미널을 열어 볼 수는 없다. 소스 코드는 Codex가 툴 인자(예를 들어 레코드 댓글)에 넣지 않는 한 어디로도 안 나간다.&lt;/p&gt;

&lt;p&gt;필터 두 개는 하는 일이 다르다. &lt;code&gt;config.toml&lt;/code&gt;의 &lt;code&gt;enabled_tools&lt;/code&gt;는 모델에 뭘 보여 줄지 정한다. 프롬프트를 짧게, 선택지를 뻔하게 만드는 용도다. osFoundry의 Tools 체크리스트는 소유자 쪽 스위치고, 로컬 파일에 뭐라고 쓰여 있든 계속 효력이 있다. 둘 다 어떤 것에 대한 접근 권한을 주지는 않는다. 그건 프로젝트 참여가 정한다.&lt;/p&gt;

&lt;p&gt;권한은 호출 단위로 판정된다. 호출할 때마다 연결 상태, 워크스페이스, 현재 프로젝트 참여, 그 항목이 지금 속한 프로젝트, 그리고 그 프로젝트에서의 내 역할을 확인한다. 노트를 다른 프로젝트로 옮기거나, 프로젝트를 빼거나, Revoke connection(연결 해제)을 누르면 토큰이 아직 만료되지 않았어도 Codex의 다음 호출은 거절된다.&lt;/p&gt;

&lt;p&gt;어떤 쓰기는 제안으로 처리된다. GitNest 작업, change set, 파일 가져오기, 풀 리퀘스트는 &lt;code&gt;propose_…&lt;/code&gt; 툴을 거쳐서, 대상과 파일 목록이 붙은 채로 Connected 탭의 "Needs your approval"에서 Approve나 Decline을 누를 때까지 기다린다. Codex가 Git 프로바이더 토큰을 받는 일은 없다. 어차피 Codex 손에는 로컬 체크아웃이 있으니까, 위 설정 예시에서는 이 툴들을 아예 뺐다.&lt;/p&gt;

&lt;p&gt;로컬 전용 데이터는 로컬에 머문다. 서버의 &lt;code&gt;initialize&lt;/code&gt; 응답에도 "Local-only data is unavailable to cloud clients."라고 나온다. 로컬 Room, 로컬 Graph 세션, 아직 동기화되지 않은 것은 이 연결로는 안 보인다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다음에 해 볼 것
&lt;/h2&gt;

&lt;p&gt;평소 질문에 답할 때 쓰는 지식 베이스를 Codex에 물려 주고, docstring이나 에러 메시지를 거기에 대 보게 하자. 예를 들면 "이 에러 문구, KB에 적힌 설명이랑 코드가 출력하는 거랑 맞아?" 같은 식이다. &lt;code&gt;search_knowledge_base&lt;/code&gt;는 문서 ID와 마지막 업데이트 시점도 돌려주니까 출처를 확인하기 쉽다. osFoundry가 지식 베이스를 어떻게 정리하는지는 &lt;a href="https://osfoundry.io/ko/features/knowledge-bases" rel="noopener noreferrer"&gt;지식 베이스 페이지&lt;/a&gt;에 있다.&lt;/p&gt;

</description>
      <category>codex</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Claude 채팅을 커스텀 커넥터로 osFoundry 프로젝트에 연결하는 방법</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:51:41 +0000</pubDate>
      <link>https://dev.to/bbt-ko/claude-caetingeul-keoseuteom-keonegteoro-osfoundry-peurojegteue-yeongyeolhaneun-bangbeob-1mjn</link>
      <guid>https://dev.to/bbt-ko/claude-caetingeul-keoseuteom-keonegteoro-osfoundry-peurojegteue-yeongyeolhaneun-bangbeob-1mjn</guid>
      <description>&lt;p&gt;여기까지 따라 하면 claude.ai의 평소 대화에서 &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; 프로젝트의 노트, 지식 베이스, 레코드를 열어 읽고, 그걸 근거로 답하고, 그 프로젝트의 토론 채널에 글까지 올릴 수 있다. 승인을 거치면 편집도 된다. Claude 로그인은 지금 쓰는 그대로다. 그 채팅이 무엇을 건드려도 되는지는 tool 호출이 있을 때마다 osFoundry가 판단하고, 호출 하나하나가 osFoundry의 연결별 Recent activity에 남는다. ChatGPT도 같은 서버 주소로 똑같이 붙일 수 있다.&lt;/p&gt;

&lt;h2&gt;
  
  
  누가 무엇을 맡나
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude 쪽&lt;/th&gt;
&lt;th&gt;osFoundry 쪽&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인&lt;/td&gt;
&lt;td&gt;평소 쓰는 Claude 계정 그대로&lt;/td&gt;
&lt;td&gt;osFoundry 계정으로 연결을 한 번 승인한다(워크스페이스 하나)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 실행&lt;/td&gt;
&lt;td&gt;원래 쓰던 Claude 채팅&lt;/td&gt;
&lt;td&gt;아무것도 안 돌린다. 이 경로에는 모델도 프로바이더 키도 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool 실행 위치&lt;/td&gt;
&lt;td&gt;Claude가 tool을 호출한다&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api.osfoundry.io&lt;/code&gt;가 권한부터 확인하고 실행한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;닿는 범위&lt;/td&gt;
&lt;td&gt;서버가 돌려준 것만&lt;/td&gt;
&lt;td&gt;연결을 추가한 프로젝트, 그리고 초대한 채널&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;승인&lt;/td&gt;
&lt;td&gt;Claude 자체의 tool 권한 확인 창&lt;/td&gt;
&lt;td&gt;GitNest와 Room 변경은 "Needs your approval"에서 대기한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 위치&lt;/td&gt;
&lt;td&gt;답변은 Claude 채팅에 남는다&lt;/td&gt;
&lt;td&gt;노트, 레코드, 문서는 osFoundry에 남는다. 로컬 전용 항목은 기기 밖으로 안 나간다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  설정
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. osFoundry에서 연결 만들기
&lt;/h3&gt;

&lt;p&gt;osFoundry 헤더의 열쇠 아이콘을 누르고 Connected 탭을 연다. 패널 제목은 "Connected agents"고, 지금 어느 워크스페이스에 있는지 같이 보인다. "Connect in three steps" 아래에서 이렇게 넣는다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assistant: Claude 선택&lt;/li&gt;
&lt;li&gt;Display name: 나중에 알아볼 수 있는 이름이면 된다. 예: &lt;code&gt;Claude research&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add assistant(어시스턴트 추가) 클릭&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;새로 만든 연결에는 "Setup pending. This assistant cannot read or change anything yet."이라고 뜬다. 아직은 아무것도 못 읽는다는 뜻이다. Server address 옆의 Copy 버튼으로 주소를 복사한다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" alt="Add assistant 직후의 Connected 탭. 서버 주소와 설정 대기 중인 연결" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: Add assistant 직후의 Connected 탭. 서버 주소와 설정 대기 중인 연결&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Claude에 커스텀 커넥터로 추가하기
&lt;/h3&gt;

&lt;p&gt;Claude에서 Customize → Connectors를 열고 이 URL로 커스텀 커넥터를 추가한다. Team이나 Enterprise 플랜이면 조직 owner가 먼저 Organization settings → Connectors에서 추가하고, 그다음에 멤버가 각자 연결한다. 원격 MCP 커넥터 절차는 Anthropic의 &lt;a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp" rel="noopener noreferrer"&gt;Claude 커스텀 커넥터 가이드&lt;/a&gt;에 정리돼 있다.&lt;/p&gt;

&lt;p&gt;Connect를 누르면 Claude가 osFoundry 로그인 화면을 알아서 찾아간다. client ID나 키를 붙여 넣을 일은 없다.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 워크스페이스 하나만 승인하기
&lt;/h3&gt;

&lt;p&gt;브라우저가 &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;로 넘어가고, "Connect an agent"라는 화면이 뜬다. 접속하려는 어시스턴트 클라이언트와 돌아갈 호스트가 표시되고, 그 아래에서 다음을 고른다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace to connect: 워크스페이스 하나. 다른 워크스페이스를 쓰려면 연결을 따로 만들어야 한다.&lt;/li&gt;
&lt;li&gt;Requested capabilities: 요청 권한이 쉬운 말로 나온다. "Read supported resources in projects you add", "Change supported resources where you have edit access", "Run sessions where you grant a runner role".&lt;/li&gt;
&lt;li&gt;Connection: "Use pending: Claude research"를 고르면 1단계에서 만든 연결에 붙는다. "Create a new connection"으로 두면 새 연결이 생긴다.&lt;/li&gt;
&lt;li&gt;"I approve connecting this assistant to &amp;lt;워크스페이스&amp;gt;"에 체크하고 Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그러면 Claude로 돌아온다. 연결은 살아 있지만 아직 아무것도 안 보인다. 새 연결은 프로젝트 0개에서 시작하기 때문이다.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. 프로젝트에 추가하기
&lt;/h3&gt;

&lt;p&gt;다시 열쇠 아이콘 → Connected로 가서 연결을 고르면 이제 "Connected."로 바뀌어 있다. Projects에서 프로젝트를 고르고 Add to project를 누른다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" alt="프로젝트에 추가된 연결과 그 아래의 연결된 채팅" width="800" height="1110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: 프로젝트에 추가된 연결과 그 아래의 연결된 채팅&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;이걸로 그 프로젝트 안의 동기화된 지원 콘텐츠에 접근 권한이 생긴다. 노트, 지식 베이스, Record 채널, 클라우드 Room, 프로젝트에 할당된 GitNest 저장소, 그리고 프로젝트의 토론 채널이다. 나중에 프로젝트에 추가하는 콘텐츠도 포함된다. 일반 Message 채널은 별개다. 채팅이 읽고 답해야 하는 채널마다 Message channels → Choose a Message channel → Invite로 초대해 줘야 한다.&lt;/p&gt;

&lt;p&gt;Tools 섹션에는 "All supported tools are on by default."라고 적혀 있다. 이 연결에 주고 싶지 않은 tool이 있으면 체크를 풀고 Save tools를 누른다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" alt="Claude 연결의 Tools 체크리스트. 기본값은 전부 켜짐" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예시: Claude 연결의 Tools 체크리스트. 기본값은 전부 켜짐&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. 채팅에서 켜기
&lt;/h3&gt;

&lt;p&gt;Claude 대화에서 + 메뉴 → Connectors를 열고 osFoundry 커넥터를 켠다. 그다음엔 평소처럼 말로 부탁하면 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  실제로 돌려 보면
&lt;/h2&gt;

&lt;p&gt;아래 프롬프트와 답변은 데모 프로젝트의 예시 콘텐츠다. tool 이름과 인자 이름은 실서버가 &lt;code&gt;tools/list&lt;/code&gt;로 알려 주는 그대로다. claude.ai에서는 호출이 커넥터 이름 아래에 쭉 나온다. 같은 URL을 Claude Code에 &lt;code&gt;osfoundry&lt;/code&gt;라는 이름으로 추가하면 같은 tool이 &lt;code&gt;mcp__osfoundry__list_project_resources&lt;/code&gt; 같은 이름으로 보인다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;예시

나: osFoundry에서 Q3 런칭 체크리스트에 아직 안 끝난 게 뭐야?
    서포트 KB랑 안 맞는 내용이 있는지도 확인하고,
    "pricing page" 레코드는 Ready로 바꿔 줘.

Claude → list_granted_resources {}
       ← 연결된 프로젝트 1개: "Launch Q3" (워크스페이스)
Claude → list_project_resources {"project_id": "…", "resource_type": "note", "search": "checklist"}
       ← 노트 "Q3 launch checklist"
Claude → list_note_tabs {"note_id": "…"}
       ← 탭: Note (텍스트), Sheet "Owners"
Claude → read_note_tab {"note_id": "…", "tab_id": "…"}
       ← 항목 9개, 미체크 3개
Claude → search_knowledge_base {"knowledge_base_id": "…", "query": "refund window"}
       ← 문서 "Refund policy" (2일 전 업데이트)
Claude → read_record_item {"record_channel_id": "…", "item_id": "…"}
       ← "pricing page", status Draft, version …
Claude → edit_record_item {"record_channel_id": "…", "item_id": "…",
                           "values": {"status": "Ready"},
                           "expected_version": "…", "idempotency_key": "…"}
       [Claude가 이 tool 호출을 허용할지 묻는다 → Allow]
       ← 저장됨, 새 version …

Claude: 남은 건 세 개야. 보도 엠바고 날짜, FAQ 번역,
        그리고 pricing page의 환불 문구. 체크리스트에는
        14일인데 KB 문서 "Refund policy"에는 30일로 돼 있어.
        pricing page 레코드는 Ready로 바꿨어.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 기록에는 일부러 넣은 디테일이 두 개 있다. &lt;code&gt;edit_record_item&lt;/code&gt;은 &lt;code&gt;read_record_item&lt;/code&gt;이 돌려준 version을 요구한다. 그래서 그 사이에 누가 레코드를 바꿨다면 덮어쓰지 않고 거절된다. 그리고 같은 &lt;code&gt;idempotency_key&lt;/code&gt;로 다시 보내면 첫 번째 결과가 돌아온다. 재시도해도 두 번 써지지 않는다.&lt;/p&gt;

&lt;h2&gt;
  
  
  어디까지 닿나
&lt;/h2&gt;

&lt;p&gt;osFoundry는 Claude 채팅을 열 수 없다. MCP 연결은 Claude가 필요하다고 판단할 때 osFoundry tool을 부를 수 있게 해 줄 뿐이다. 반대 방향, 즉 osFoundry가 개인 대화를 나열하거나 읽거나 시작하는 API는 없다. 채팅을 프로젝트에 연결하는 tool(&lt;code&gt;link_current_chat&lt;/code&gt;)도 채팅이 스스로 밝힌 라벨을 받는데, osFoundry는 이걸 확인된 신원이 아니라 라우팅 힌트로만 다룬다. 같은 이유로 Claude는 osFoundry에서 백그라운드 알림을 받지 않는다. 대화를 열고 부탁할 때만 움직인다.&lt;/p&gt;

&lt;p&gt;로컬 전용 데이터는 로컬에 남는다. 서버도 &lt;code&gt;initialize&lt;/code&gt; 응답에서 직접 "Local-only data is unavailable to cloud clients."라고 밝힌다. 로컬 Room, 로컬 Graph 세션, 한 번도 동기화되지 않은 콘텐츠는 연결에서 안 보인다. 데스크톱 Graph 세션 공유는 같은 탭에 있는 별도 스위치고, 데스크톱 앱이 열려 있고 로그인돼 있어야 한다.&lt;/p&gt;

&lt;p&gt;권한은 로그인할 때가 아니라 호출마다 확인한다. 호출 하나하나를 연결 상태, 워크스페이스, 지금 시점의 프로젝트 멤버십, 그 아이템이 지금 어느 프로젝트에 있는지, 그리고 그 아이템에 대한 내 역할과 대조한다. 노트를 다른 프로젝트로 옮기거나, 프로젝트를 빼거나, Revoke connection을 누르면 바로 다음 호출부터 거절된다. 편집은 연결을 승인한 사람, 즉 내가 아직 편집 권한을 갖고 있어야 된다.&lt;/p&gt;

&lt;p&gt;일부 쓰기는 제안이 된다. GitNest 작업, change set, 파일 가져오기, 풀 리퀘스트, Room 앱 설치는 &lt;code&gt;propose_…&lt;/code&gt; tool을 거친다. 이건 Connected 탭의 "Needs your approval"에 쌓이고, 대상 저장소나 Room, 파일을 확인한 다음 Approve나 Decline을 고르면 된다. Git 프로바이더 토큰이나 시크릿 값이 Claude에 넘어가는 일은 없다.&lt;/p&gt;

&lt;p&gt;Message 채널은 초대가 필요하다. 프로젝트에 들어 있다고 열리는 건 그 프로젝트의 토론 채널뿐이다. 다른 채널은 전부 따로 Invite해야 하고, 비공개 채널은 owner가 초대해야 한다.&lt;/p&gt;

&lt;h2&gt;
  
  
  다음 단계
&lt;/h2&gt;

&lt;p&gt;같은 프로젝트에 지식 베이스를 추가하고, 평소라면 거기서 찾아 답했을 질문을 Claude에게 던져 보자. 예를 들면 "환불에 대해 KB에 뭐라고 돼 있어? 문서 이름도 같이 알려 줘." &lt;code&gt;search_knowledge_base&lt;/code&gt;는 문서 ID와 마지막 업데이트 시점을 돌려주니까 출처를 바로 확인할 수 있다. osFoundry 지식 베이스가 어떻게 구성되는지는 &lt;a href="https://osfoundry.io/ko/features/knowledge-bases" rel="noopener noreferrer"&gt;지식 베이스 소개 페이지&lt;/a&gt;에 있다.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Claude 커스텀 커넥터는 osFoundry의 OAuth를 어떻게 찾고, 왜 권한을 매번 확인할까</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:52:54 +0000</pubDate>
      <link>https://dev.to/bbt-ko/claude-keoseuteom-keonegteoneun-osfoundryyi-oauthreul-eoddeohge-cajgo-wae-gweonhaneul-maebeon-hwaginhalgga-4dcl</link>
      <guid>https://dev.to/bbt-ko/claude-keoseuteom-keonegteoneun-osfoundryyi-oauthreul-eoddeohge-cajgo-wae-gweonhaneul-maebeon-hwaginhalgga-4dcl</guid>
      <description>&lt;p&gt;토큰 없이 &lt;a href="https://osfoundry.io/ko" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt;의 원격 MCP 서버에 tool을 호출하면 이런 응답이 온다. 2026-11-06에 도쿄에 있는 Mac에서 curl로 직접 찔러 본 결과다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \
    -H 'content-type: application/json' \
    -H 'accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
         "params":{"name":"list_project_resources","arguments":{}}}'

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp", error="invalid_token", error_description="Connect osFoundry to continue"

{"jsonrpc":"2.0","id":3,"result":{"isError":true,
  "content":[{"type":"text","text":"Authentication required"}],
  "_meta":{"mcp/www_authenticate":["Bearer resource_metadata=\"https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp\", ..."]}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude 커스텀 커넥터든 ChatGPT 개발자 모드의 MCP 연결이든, 로그인 화면까지 가는 단서는 이 헤더 한 줄이다. 사용자가 붙여 넣는 건 URL 하나뿐이다. 앞부분은 실서버에 인증 없이 보낸 요청만으로 정리했다. 뒷부분에서는 curl로는 안 보이는 부분, 즉 로그인한 뒤 권한이 어떻게 다뤄지는지를 Python으로 작게 다시 만들어서 측정했다.&lt;/p&gt;

&lt;h2&gt;
  
  
  토큰이 없을 때 클라이언트에 보이는 것
&lt;/h2&gt;

&lt;p&gt;메서드마다 응답이 다르다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요청&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;프로토콜 &lt;code&gt;2025-06-18&lt;/code&gt;, 서버 &lt;code&gt;osfoundry 0.1.0&lt;/code&gt;, instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;tool 59개, 45,368바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;리소스 메타데이터를 가리키는 &lt;code&gt;WWW-Authenticate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;엔드포인트에 &lt;code&gt;GET&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;GET으로 여는 SSE 스트림은 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;핸드셰이크와 tool 목록은 공개돼 있다. 토큰이 필요한 건 tool 실행뿐이다. 덕분에 호스트는 로그인 전에도 어떤 tool이 있는지 사용자에게 보여 줄 수 있다. 목록에 사용자 데이터는 없고, &lt;code&gt;initialize&lt;/code&gt;의 instructions도 일반적인 안내문이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use list_granted_resources first. Every Note edit must use the exact tab ID.
Local-only data is unavailable to cloud clients.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt;에 나오는 tool마다 필요한 OAuth scope와 읽기 전용 여부를 스스로 선언한다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"openWorldHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;59개를 세 보면 &lt;code&gt;osfoundry:read&lt;/code&gt; 30개(전부 &lt;code&gt;readOnlyHint: true&lt;/code&gt;), &lt;code&gt;osfoundry:write&lt;/code&gt; 26개, &lt;code&gt;osfoundry:run&lt;/code&gt; 3개(&lt;code&gt;request_session_run&lt;/code&gt;, &lt;code&gt;run_graph_session&lt;/code&gt;, &lt;code&gt;cancel_graph_run&lt;/code&gt;)다. 호스트는 &lt;code&gt;readOnlyHint&lt;/code&gt;를 보고 어떤 호출에서 승인을 받을지 정할 수 있다.&lt;/p&gt;

&lt;h2&gt;
  
  
  401은 어디를 가리키나
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resource_metadata&lt;/code&gt;는 RFC 9728(OAuth Protected Resource Metadata) 형식이다. well-known은 호스트 바로 아래에 두고, 그 뒤에 리소스 경로를 이어 붙인다. 리소스 URL 끝에 붙이는 게 아니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /.well-known/oauth-protected-resource/api/v1/external-mcp   200
GET /.well-known/oauth-protected-resource                       200 (같은 문서)
GET /api/v1/external-mcp/.well-known/oauth-protected-resource   404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:models"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;클라이언트를 직접 짜면 제일 먼저 이 404에서 삽질하게 된다. URL은 조립하지 말고 헤더에 적힌 걸 그대로 쓰면 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  인가 서버 메타데이터
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;(RFC 8414)는 이렇다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"registration_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"grant_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_response_iss_parameter_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;필드 하나하나가 설계 결정이다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;registration_endpoint&lt;/code&gt;가 있다. 호스트가 동적 클라이언트 등록(DCR)으로 스스로 등록하니까 사용자가 client ID를 만질 일이 없다.&lt;/li&gt;
&lt;li&gt;토큰 엔드포인트 인증이 &lt;code&gt;none&lt;/code&gt;이다. 호스트는 시크릿이 없는 public client고, 인가 코드를 요청한 쪽과 묶어 주는 건 PKCE(S256)다.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt; 파라미터를 지원한다. 인가 서버 여러 개와 붙는 호스트에서 서로 헷갈리지 않게 막아 준다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;authorize 단계에서 브라우저는 osFoundry 동의 화면 &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;로 넘어간다. 여기서 사용자는 워크스페이스를 딱 하나 고르고 "I approve connecting this assistant to …"에 체크한다. 이 승인으로 정해지는 건 워크스페이스뿐이다. 새 연결에는 프로젝트가 하나도 없다. 프로젝트는 나중에 앱(열쇠 아이콘 → Connected → Add to project)에서 소유자가 붙인다.&lt;/p&gt;

&lt;h2&gt;
  
  
  scope만으로는 부족한 이유
&lt;/h2&gt;

&lt;p&gt;scope는 토큰을 발급할 때 정해진다. &lt;code&gt;osfoundry:write&lt;/code&gt;는 쓰기 계열 동작을 해도 된다는 뜻일 뿐이다. 어느 노트인지는 모르고, 그 노트가 10분 전에 다른 프로젝트로 옮겨졌다는 것도 모른다. osFoundry 설계 문서에는 호출할 때마다 다시 읽는 항목이 정해져 있다. 연결 상태, 워크스페이스, 프로젝트 소속, 리소스가 지금 속한 프로젝트와 ACL, 채널 소속이나 세션 역할, tool, OAuth scope다. 연결을 승인한 사람이 지금도 그 항목의 편집 권한을 갖고 있어야 한다는 조건도 붙는다. 빼거나 옮긴 변경은 즉시 반영된다고 적혀 있다.&lt;/p&gt;

&lt;p&gt;반대 방식도 있다. 프로젝트 목록을 토큰(또는 토큰을 키로 쓰는 캐시)에 넣어 두고 만료될 때까지 믿는 방식이다. 구현은 이쪽이 편하다. 동작이 얼마나 달라지는지 보려고 둘 다 만들어 봤다.&lt;/p&gt;

&lt;h2&gt;
  
  
  최소 구성으로 다시 만들기
&lt;/h2&gt;

&lt;p&gt;Claude 바이너리는 쓰지 않았다. &lt;code&gt;client.py&lt;/code&gt;가 호스트를, &lt;code&gt;server.py&lt;/code&gt;가 MCP 서버를 흉내 낸다. 둘 다 표준 라이브러리만 썼고 합쳐서 약 280줄이다. Apple M1, Python 3.9.6에서 돌렸다. 서버가 구현한 건 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;일반 HTTP JSON-RPC 위의 &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;well-known 메타데이터 두 개&lt;/li&gt;
&lt;li&gt;동의 화면 대신 워크스페이스 하나로 자동 승인하는 &lt;code&gt;/authorize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PKCE S256을 검증하는 &lt;code&gt;/token&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connected 탭에서 소유자가 누르는 동작은 &lt;code&gt;/admin/*&lt;/code&gt;로 대신했다.&lt;/p&gt;

&lt;p&gt;tool 실행 전에 매번 거치는 체크는 이게 전부다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TOOL_SCOPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scopes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insufficient_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# design B: trust issue-time state
&lt;/span&gt;        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects_at_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROJECT_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;클라이언트는 토큰 없이 출발한다. 먼저 tool을 호출해 401을 받고, &lt;code&gt;WWW-Authenticate&lt;/code&gt;에서 &lt;code&gt;resource_metadata&lt;/code&gt;를 꺼낸다. 메타데이터 두 개를 가져온 뒤 새 PKCE verifier로 authorize와 token을 통과하고, 시나리오를 차례로 돌린다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ CHECK=per_call python3 server.py &amp;amp;  python3 client.py 8767
discovery: first call -&amp;gt; 401, metadata /.well-known/oauth-protected-resource/mcp, 5 round trips, 11.3 ms
1 fresh connection, no project yet           200 OK   []
2 after owner adds project A                 200 OK   ["note-a1"]
3 read note in A                             200 OK   "Q3 launch checklist"
4 read note in B (never added)               200 DENY not_in_project
5 write with a read-only token               403 DENY insufficient_scope
6 write with read+write token                200 OK   "saved"
7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 DENY not_in_project
9 same read after revoke                     200 DENY connection_revoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;같은 클라이언트를 &lt;code&gt;CHECK=token_only&lt;/code&gt; 서버에 붙이면 이렇게 나온다. 이쪽 토큰은 마지막으로 갱신할 때의 프로젝트 목록을 들고 있다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 OK   "Q3 launch checklist v2"
9 same read after revoke                     200 OK   "Q3 launch checklist v2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1~7은 똑같다. token_only에서도 7이 막히는 건 노트의 소속 프로젝트만은 그때그때 조회하기 때문이다. 낡는 건 토큰에 박아 둔 사실뿐이다. 차이는 8과 9에서 났고, 둘 다 소유자에게 중요한 동작이다. 프로젝트에서 빼고 연결까지 취소했는데 채팅은 계속 읽는다. 이 상태는 토큰이 만료될 때까지 이어지고, 이번 재구현에서는 &lt;code&gt;expires_in: 3600&lt;/code&gt;이었다.&lt;/p&gt;

&lt;p&gt;거부는 두 층으로 나눠서 돌려준다. scope가 모자라면 프로토콜 문제로 보고 403과 &lt;code&gt;error="insufficient_scope"&lt;/code&gt;에 필요한 scope를 붙여 보낸다. 그러면 호스트가 권한을 더 요청할 수 있다. 어느 항목이냐에 걸린 거부는 &lt;code&gt;isError: true&lt;/code&gt;가 붙은 평범한 tool 결과로 보낸다. 이건 재인가해도 해결되지 않으니 호스트에게 다시 해 보라고 할 이유가 없다.&lt;/p&gt;

&lt;h2&gt;
  
  
  매번 체크하는 비용
&lt;/h2&gt;

&lt;p&gt;서버 안에서 &lt;code&gt;access()&lt;/code&gt; 앞뒤를 &lt;code&gt;time.perf_counter_ns()&lt;/code&gt;로 감싸 실행마다 1,000번(허용 500, 거부 500) 쟀다. 측정하는 동안 Mac에서는 다른 작업도 돌고 있었다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;access() 중앙값&lt;/th&gt;
&lt;th&gt;access() p99&lt;/th&gt;
&lt;th&gt;루프백 tool 호출 중앙값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;매번 체크 (6회 실행)&lt;/td&gt;
&lt;td&gt;1.3–1.6 µs&lt;/td&gt;
&lt;td&gt;6.1–7.0 µs&lt;/td&gt;
&lt;td&gt;0.43–0.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token_only (3회 실행)&lt;/td&gt;
&lt;td&gt;0.7–1.6 µs&lt;/td&gt;
&lt;td&gt;3.5–7.7 µs&lt;/td&gt;
&lt;td&gt;0.35–1.36 ms (2회분)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;메모리 안에서 끝나는 구성에서는 매번 하는 재확인이 루프백 HTTP의 편차에 묻혔다. 실제 서버는 집합 조회 대신 DB를 보니까 더 무겁다. 다만 그 수치는 이 재구현으로는 잴 수 없어서 적지 않는다. 비교용으로 실서버를 보면, 도쿄에서 왕복 한 번에 약 500~800ms가 걸렸다. urllib으로 매번 TLS를 새로 맺고 각 10회 잰 값이고, 중앙값은 &lt;code&gt;initialize&lt;/code&gt; 790ms, &lt;code&gt;tools/list&lt;/code&gt; 816ms, 401이 나는 &lt;code&gt;tools/call&lt;/code&gt; 565ms, 메타데이터 각 520~530ms였다. 커넥터의 시간을 잡아먹는 건 권한 체크가 아니다.&lt;/p&gt;

&lt;p&gt;디스커버리는 로컬에서 5번 왕복했다(401, 리소스 메타데이터, 인가 서버 메타데이터, authorize, token). 7번은 7.5~13.4ms 안에 끝났고, Mac이 바빴던 한 번만 34ms였다. 실서버에 인증 없이 보낼 수 있는 세 구간을 더하면 콜드 스타트에서 약 1.6초다. authorize는 어차피 사람의 클릭을 기다리니까 여기서는 문제가 안 된다.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude는 호출하는데 osFoundry는 채팅을 못 여는 이유
&lt;/h2&gt;

&lt;p&gt;이 구성에서 호출하는 쪽은 호스트다. 연결을 열고, tool 목록을 받고, 언제 호출할지 정하는 건 호스트고, 서버는 답만 한다. 서버가 호스트의 대화를 붙잡을 방법은 없다. osFoundry 계획 문서의 기능 표에도 개인 ChatGPT/Claude 채팅을 열거나 목록으로 보는 칸은 "No"이고, 이유는 "No general host API is provided for this"라고 적혀 있다.&lt;/p&gt;

&lt;p&gt;tool 설계도 여기에 맞춰져 있다. &lt;code&gt;link_current_chat&lt;/code&gt;은 채팅 안에서 그 채팅이 스스로 링크를 만들게 하는 tool이다. 라벨은 자기 신고고, osFoundry는 이걸 신원 확인이 아니라 라우팅용 메모로 쓴다. 링크된 Claude 채팅에 배정한 작업은 사용자가 그 채팅을 열고 확인해 달라고 할 때까지 기다린다. 구독한 채팅에 전달되는 MCP Events는 ChatGPT 쪽 기능이고, 계획 문서에서도 호스트별 기능으로 다룬다.&lt;/p&gt;

&lt;p&gt;데이터에 관한 판단은 전부 서버가 한다. 프로젝트의 지금 상태를 아는 쪽이 서버뿐이기 때문이다. 언제 움직이고 사용자에게 물어볼지는 호스트가 정한다. 두 쪽의 인식이 어긋나도 매번 체크하면 서버의 판단은 계속 맞는 상태로 남는다.&lt;/p&gt;

&lt;h2&gt;
  
  
  직접 붙인다면 정해 둘 것
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;401의 &lt;code&gt;WWW-Authenticate&lt;/code&gt;에 &lt;code&gt;resource_metadata&lt;/code&gt;를 넣고, RFC 9728 메타데이터를 &lt;code&gt;/.well-known/oauth-protected-resource/&amp;lt;리소스 경로&amp;gt;&lt;/code&gt;에서 내준다. URL을 직접 조립하는 클라이언트는 틀리기 쉬운데, 헤더가 있으면 어떤 클라이언트도 헤매지 않는다.&lt;/li&gt;
&lt;li&gt;로그인 전에 뭘 공개할지 정한다. osFoundry는 &lt;code&gt;initialize&lt;/code&gt;와 &lt;code&gt;tools/list&lt;/code&gt;를 열어 두고 &lt;code&gt;tools/call&lt;/code&gt;만 막았다. tool 설명에서 테넌트 정보가 새어 나간다면 &lt;code&gt;tools/list&lt;/code&gt;도 막는다.&lt;/li&gt;
&lt;li&gt;tool마다 scope와 &lt;code&gt;readOnlyHint&lt;/code&gt;를 선언한다. 호스트는 이걸 보고 승인 절차를 넣을 수 있고, 403 &lt;code&gt;insufficient_scope&lt;/code&gt;를 받으면 무엇을 더 요청해야 할지 안다.&lt;/li&gt;
&lt;li&gt;토큰에는 누구인지, 어느 연결인지, 어떤 scope인지만 넣는다. 워크스페이스, 프로젝트 소속, 리소스의 소속 프로젝트, 승인한 사람의 현재 권한은 매번 다시 읽는다. 그래야 취소도, 다른 프로젝트로 옮기기도 다음 호출부터 바로 반영된다.&lt;/li&gt;
&lt;li&gt;리소스 단위 거부는 401/403이 아니라 tool 결과로 돌려준다. "이 노트는 프로젝트 B에 있다"는 토큰을 바꿔도 그대로다.&lt;/li&gt;
&lt;li&gt;쓰기에는 &lt;code&gt;expected_version&lt;/code&gt;과 멱등 키를 필수로 받는다. 호스트는 재시도하고, 모델은 같은 호출을 다시 보낸다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;스펙의 MCP 쪽 내용은 &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;MCP 인가 문서&lt;/a&gt;에 있다. osFoundry가 정리한 짧은 정의는 &lt;a href="https://osfoundry.io/ko/glossary/mcp" rel="noopener noreferrer"&gt;용어집의 MCP 항목&lt;/a&gt;에 있다.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>claude</category>
      <category>security</category>
    </item>
  </channel>
</rss>
