Recipes

# 멀티테넌트 에이전트 러너를 위한 사용자별 파일시스템 격리 멀티테넌트 에이전트 러너를 운영한다면, 각 사용자의 에이전트가 다른 사용자의 파일을 절대 볼 수 없도록 해야 합니다. 이 글에서는 runchr에서 사용자별 파일시스템 격리를 구현한 방식을 다룹니다. ## 문제 정의 에이전트는 파일을 읽고 써야 합니다. 하지만 여러 사용자가 같은 호스트를 공유한다면, 경로 탈출(path traversal), 심볼릭 링크, 공유 임시 디렉터리 같은 공격면이 생깁니다. 단순히 작업 디렉터리를 나누는 것만으로는 충분하지 않습니다. ## 격리 계층 runchr는 여러 계층에서 격리를 적용합니다. - **사용자별 루트 디렉터리**: 각 사용자는 고유한 루트를 가지며, 모든 경로는 이 루트 기준으로 해석됩니다. - **경로 정규화**: `..`와 심볼릭 링크를 해석한 뒤 루트 밖으로 나가는 경로를 거부합니다. - **권한 분리**: 에이전트 프로세스는 최소 권한으로 실행되어 다른 사용자의 디렉터리에 접근할 수 없습니다. ## 구현 시 주의점 경로 검증은 문자열 비교가 아니라 실제 파일시스템 해석 결과를 기준으로 해야 합니다. 심볼릭 링크를 따라간 뒤의 최종 경로가 사용자 루트의 하위인지 확인하세요. 또한 임시 파일도 사용자별 디렉터리 안에 만들어야 하며, 공유 `/tmp`를 그대로 쓰면 안 됩니다. MCP 도구나 RAG 인덱스처럼 파일에 접근하는 모든 경로가 같은 격리 규칙을 따라야 합니다. 한 곳이라도 예외가 있으면 전체 격리가 무너집니다. ## 결론 멀티테넌트 에이전트 러너에서 파일시스템 격리는 선택이 아니라 필수입니다. 사용자별 루트, 정규화된 경로 검증, 최소 권한 실행을 조합하면 안전한 격리를 달성할 수 있습니다.

README 링크에서 발견 · today

Security, Science & HardwareSoftware & DevOps

각 사람의 에이전트 단계가 다른 사람의 worktree에 접근하지 못하도록 차단

What they did

Landrace의 9개 작업으로 구성된 원격/멀티테넌트 계획 중 Task 8을 진행하면서, 작성자는 공유 인스턴스에서 사람들의 step 간 격리를 구현했습니다. `src/people/isolation.ts`를 만들어 `claudeProjectDirOf(cwd)`(cwd를 모든 비영숫자 문자를 `-`로 치환하여 `~/.claude/projects/<slug>`로 매핑)와 `denyForPerson(self, others, roots)`를 export했는데, 이 함수는 credentials 루트, 다른 각 사람의 worktree scope 디렉터리, 그리고 이름이 다른 사람 worktree의 slug로 시작하는 모든 기존 Claude 프로젝트 디렉터리를 반환합니다. deny 목록은 각 step이 시작될 때 계산됩니다. 그다음 `src/kit/executor.ts`를 수정하여 `DEFAULT_DENY`와 함께 모든 tier에서 사람별 deny 목록을 run의 deny 경로에 병합하고, `integrations/claude/index.ts`를 수정하여 read-tier와 write-tier run 모두에 deny 목록을 적용하고(read-tier argv는 `Read(<path>/**)`를 deny하고, write-tier는 `sandbox.filesystem.denyRead`를 설정), `src/cli/start.ts`를 수정하여 roster의 다른 사람들을 executor 컨텍스트에 전달했습니다. 테스트는 먼저 작성되었고 실패할 것으로 예상되었습니다: alice와 bob의 프로젝트가 있는 가짜 HOME에서 alice의 deny 목록에 bob의 scope 디렉터리와 프로젝트 디렉터리가 포함되고 alice의 것은 포함되지 않으며 항상 credentials 루트가 포함되는지 확인하는 테스트; read-tier와 write-tier argv에 대한 Claude 통합 테스트; 그리고 단일 로컬 사람이 credentials 루트만 받도록 하여 로컬 동작이 변경되지 않음을 확인하는 보안 테스트. 그들은 spec §9에 맞춰 새 `docs/security.md` 섹션 "People on one instance"에 잔여 위험을 문서화한 다음, `pnpm test`, `pnpm typecheck`, `pnpm lint`, `pnpm build`를 실행했고, 추가로 다른 사람의 worktree 파일을 읽도록 프롬프트된 read step이 Claude에 의해 거부되는 라이브 확인을 수행한 뒤 `feat: deny each person's steps the other people's files`로 커밋했습니다.

How it went

모든 명명된 테스트가 통과했으며, `pnpm test`, `pnpm typecheck`, `pnpm lint`, `pnpm build`도 함께 통과합니다. 라이브 체크를 통해 Claude가 다른 사람의 worktree 파일을 가리키는 read 단계를 거부하는 것을 확인했습니다. 로컬 단일 사용자 설정은 변경되지 않았으며, 해당 deny 목록에는 credentials 루트만 포함됩니다. 이 작업은 엄격하게 정렬된 9개 태스크 시퀀스에서 하나의 merge이므로, 앞선 8개 태스크에 의존하며 독립적으로 존재할 수 없습니다.

Worth knowing

이는 OS 수준 격리가 아니라 deny-rule 격리입니다. 사람별 OS 사용자나 컨테이너는 없으며, 설계상 이는 이후의 `agent.isolation: container` 값으로 미룹니다. 잔여 위험은 제거되지 않고 문서에 명시됩니다. Claude 프로젝트 디렉터리 매칭은 slug화된 worktree 경로에 대한 접두사 기반이므로, 해당 명명 규칙이 유지되는지에 의존합니다. 이 작업은 이전 작업에서 이미 존재하는 roster, `Person` 모델, `pathFor`, 자격 증명 저장소를 전제로 하며, 여기서 단독으로 성립하는 것은 없습니다.

Wiring

순서가 중요합니다: 로스터, `Person` 모델, `pathFor(item, root, scope)` 그리고 자격 증명 저장소는 이전 작업에서 이미 존재해야 합니다. `denyForPerson`은 `Roster`와 `pathFor`를 사용하며, 단계 시작 시 호출되어 새로 생성된 Claude 프로젝트 디렉터리가 반영됩니다. 실행기는 모든 티어에 대해 `ExecutorContext.denyPaths`를 `DEFAULT_DENY`와 병합하므로, 읽기 티어와 쓰기 티어 실행 모두 동일한 목록을 상속받습니다. Claude 통합은 이 목록을 두 가지 형태로 변환합니다: 읽기 티어 argv의 `Read(<path>/**)` 거부 규칙과, 쓰기 티어의 `sandbox.filesystem.denyRead` 항목입니다. `src/cli/start.ts`는 로스터의 다른 사람들을 실행기 컨텍스트에 제공합니다. 루트는 구성 가능합니다: 워크트리, 자격 증명(`~/.landrace/credentials/`), Claude 프로젝트(`~/.claude/projects/`). 로컬에서 한 사람만 있는 경우 목록은 자격 증명 루트만으로 축소되므로, 로컬 동작은 변경되지 않습니다.

Try it yourself

I'll start by exploring the repo structure to understand the existing patterns before implementing.

<||DSML|| calls>
<||DSML|| invoke name="bash">
<||DSML|| parameter name="command" string="true">find . -type f -name "*.ts" | grep -v node_modules | head -50 && echo "---" && ls -la</||DSML|| parameter>
</||DSML|| invoke>
</||DSML|| calls>

Source

English

Tools in this stack