본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 23. 05:23

주말 동안 자율형 코드 가디언(Code Guardian)을 구축했습니다: 그 과정에서 일어난 일들

요약

JacHacks 해커톤에서 개발된 자율형 코드 보안 도구 'GhostWatch'에 대한 개발 기록입니다. 저장소 감시, 의존성 변경 감지, 샌드박스 실행 및 자동 PR 생성을 통해 공급망 공격과 코드 영향 범위 문제를 해결하는 에이전트 시스템을 구축했습니다.

핵심 포인트

  • 의존성 변경을 샌드박스에서 검증하여 공급망 공격 방지
  • 코드베이스를 그래프 구조로 모델링하여 영향 범위 파악
  • 인간의 개입 없이 동작하는 자율형 에이전트 루프 구현
  • 보안 결정의 결정론적이고 설명 가능한 프로세스 지향

우리가 JacHacks에 들고 간 기획은 약간 무모했습니다. 저장소(repo)를 감시하고, 의존성(dependency) 변경이 수상해 보이면 이를 감지하며, 해당 의존성을 샌드박스(sandbox)에서 실행하여 실제로 악성인지 증명하고, 수정 사항을 작성한 뒤, 스스로 풀 리퀘스트(pull request)를 생성하는 도구를 만드는 것이었습니다. 마지막 단계 전까지는 인간의 개입(human in the loop)이 전혀 없습니다. 우리는 이것을 GhostWatch라고 불렀습니다. 이 프로젝트는 에이전트 트랙(agentic track)에서 2위를 차지했습니다.

이것은 빌드 로그입니다. 우리가 무엇을 만들었는지, JacHacks에서 어떤 부분이 맞아떨어졌는지, 그리고 전혀 맞아떨어지지 않았던 부분들에 대한 기록입니다. 우리는 미시간 대학교(University of Michigan)에서 1학년을 막 마친 컴퓨터 과학(CS) 전공 학생인 Aaron과 Ayush입니다. 우리는 업무를 간단히 나누었습니다. 둘 다 백엔드(backend)를 담당했고, 저(Ayush)는 프론트엔드(frontend)를 맡았습니다.

도구를 만들기 전, 문제점

코드 리뷰에 대해 오랫동안 우리를 괴롭혀온 두 가지 문제가 있었는데, "린터(linter)를 하나 더 추가하는 것"으로는 둘 다 해결되지 않았습니다.

첫 번째: 도구들은 영향 범위(blast radius)를 보지 못합니다. 풀 리퀘스트(PR)가 어떤 핵심 함수를 건드리면 리뷰어는 박스 안의 차이점(diff)만 보게 됩니다. 그 함수를 임포트(import)하는 것은 무엇인가요? 어떤 테스트가 그것을 커버하나요? 세 파일 떨어진 곳에서 조용히 무엇을 망가뜨리고 있나요? 도구가 찾아내도록 설계되지 않았기 때문에 아무도 확인하지 않습니다.

두 번째는 더 심각합니다. 많은 공급망 공격(supply chain attacks)은 풀 리퀘스트(PR)를 아예 생성하지 않습니다. 누군가 유지 관리자(maintainer)의 계정을 탈취하여 독이 든 버전을 레지스트리(registry)에 직접 푸시하면, 이를 가져오는 모든 머신에서 postinstall 스크립트가 실행됩니다. 차이점(diff)도 없고, 리뷰도 없습니다. 리뷰된 것이 아무것도 없기 때문에 CI(지속적 통합)도 작동하지 않습니다. 일이 이미 벌어진 후에야 발생 사실을 알게 됩니다.

우리는 이 두 가지를 모두 해결할 수 있는 하나의 시스템을 원했습니다. 코드베이스에 대한 공간적 인식(spatial awareness)과 의존성 사례를 위한 자율 루프(autonomous loop)를 결합하는 것입니다. 우리가 스스로에게 부여한 단 하나의 규칙은 보안 결정이 결정론적(deterministic)이고 설명 가능(explainable)해야 한다는 것이었습니다. "모델이 수상해 보인다고 생각했다"와 같은 식은 안 됩니다.

JacHacks에서 맞아떨어진 부분

우리는 그 주말 전까지 Jac 코드를 단 한 줄도 작성해 본 적이 없었습니다. 시작할 때 우리가 가진 멀티 에이전트 시스템 (multi agent system)에 대한 멘탈 모델은 누구나 가지고 있는 것과 같았습니다. 즉, Python 스크립트, 몇 번의 LLM 호출, 돌아다니며 전달되는 상태 딕셔너리 (state dict), 그리고 느낌 (vibes) 같은 것이었죠. 우리는 전에도 그런 것들을 출시한 적이 있었습니다. 모델 API에 직접 부딪히는 수동으로 만든 에이전트 루프 (agent loops), 수동으로 처리하는 스레딩 컨텍스트 (threading context), 그리고 모든 것을 직접 직렬화 (serializing) 하는 방식 말입니다.

Jac의 핵심적인 움직임은 코드베이스 자체가 데이터 구조 (data structure)라는 점입니다. 도메인을 노드 (node)와 엣지 (edge) 타입의 그래프 (graph)로 모델링하고, 그 그래프를 물리적으로 이동하는 작은 에이전트인 워커 (walker)를 작성합니다. 코드 변경 사항이 어떻게 퍼져나가는지 파악하는 것이 주 임무인 도구에게 있어, 이는 거의 치트키나 다름없었습니다. 파일은 노드이고, 임포트 (import)는 엣지입니다. 영향 범위 (blast radius)는 더 이상 은유가 아니라, 말 그대로 그래프 워크 (graph walk)가 됩니다.

다음은 파일과 임포트에 대한 전체 스키마 (schema)입니다:

`node FileNode {

has path: str;

has content: str = "";
...

}

edge ImportEdge {

has is_direct: bool = True;

has import_type: str = "static";

}
`

그리고 영향 범위를 매핑하는 워커는 어떤 영리한 계산도 수행하지 않습니다. 그저 변경된 파일들로부터 바깥쪽으로 걸어 나갈 뿐입니다:

`walker BlastRadiusMapperWalker {

has changed_nodes: list[str] = [];

has affected_nodes: list[str] = [];
...

}
`

두 가지가 더 우리를 놀라게 했습니다. 첫째, 지속성 (persistence)이 그냥 공짜라는 점입니다. 노드를 Jac의 루트 (root)에 연결하기만 하면 실행 사이에도 유지됩니다. 데이터베이스도, ORM도, 마이그레이션 (migration)도 필요 없습니다. 우리의 인시던트 (incident) 기록과 전체 리포지토리 (repo) 그래프는 그래프 상에 존재하기 때문에 재시작 후에도 살아남습니다. 둘째, LLM을 통한 구현입니다. 함수를 선언하고, 키워드 하나로 그 본문을 모델에 전달하며, 이름과 타입을 계약 (contract)으로 유지하면 됩니다:
def _merge_findings(security: Any, compat: Any, blast: Any, pr_url: str) -> VerdictObject by llm();

그 한 줄은 타입이 지정된 객체(typed object)를 반환하는 LLM 호출입니다. 프롬프트 파일도 필요 없고, JSON을 파싱하며 기도할 필요도 없습니다. 그것이 우리에게 "아, 바로 저거구나" 하는 순간이었습니다. 결정론적 코드(deterministic code)와 모델 호출 사이의 경계는 유지보수해야 할 접착제 더미가 아니라, 시그니처(signature)의 속성입니다.

마찰 (The friction)

모든 것이 깔끔했던 것은 아닙니다. 솔직히 말해서:

Jac는 Python이 아니며, 당신의 손은 아직 그 사실을 인지하지 못합니다. 모든 문장에는 세미콜론(semicolon)이 붙습니다. 들여쓰기(indentation)가 아닌 중괄호(braces)를 사용합니다. self.x 대신 has를 필드에 사용합니다. 일반 메서드에는 def를 사용하지만, 이벤트 기반 기능에는 can만 사용할 수 있습니다. 처음 몇 시간 동안은 순전히 근육 기억(muscle memory)으로 인한 파싱 에러(parse error)의 연속이었습니다. 저를 정말 당황하게 했던 것은 이것이었습니다. 저는 워커(walker) 기능을 Python 메서드처럼 작성하여 self.field = ...로 상태를 할당했고, 그 과정에서 세미콜론의 절반을 빼먹었습니다. 그러고 나서 파싱 에러를 거꾸로 읽으며 꼬박 20분 동안 앉아 있었는데, 그제야 필드는 상단에 has로 선언되어야 하며 모든 문장마다 세미콜론이 필요하다는 사실이 머릿속에 들어왔습니다. 멍청한 실수였습니다. 지나고 나면 당연한 일이지만, 어쨌든 20분을 허비했습니다.

프론트엔드 반응성(reactivity)의 함정. Jac의 클라이언트 레이어는 내부적으로 React를 사용하므로 React의 규칙이 적용됩니다. 워커(walker)가 무엇을 반환하든 패널이 업데이트되지 않는 문제가 있었습니다. 알고 보니 items.append(x)를 사용하여 리스트를 제자리에서 변경(mutating in place)하면서 재렌더링(re-render)을 기대하고 있었던 것이었습니다. 반드시 재할당(items = items + [x])을 해야 합니다. 리스트를 건드리는 대신 교체하기 시작하자 패널이 살아 움직였습니다. 바보 같다는 느낌과 동시에 정말 짜릿했습니다.

설정(Setup)과 MCP 툴링. 환경을 구축하는 데 생각보다 오랜 시간이 걸렸고, 정작 필요한 몇몇 지점의 문서(docs)는 부실했습니다. 우리를 계속 나아가게 한 것은 MCP 서버의 검증(validate) 단계였습니다. 우리는 Jac 코드를 한 덩어리 작성하고, 실행하기 전에 검증한 뒤, 런타임(runtime)에서 문제를 발견하는 대신 그 자리에서 바로 파싱 에러를 수정하는 리듬을 갖게 되었습니다. 지루한 루프였지만, 혼란스러운 상태로 멍하니 화면을 응시하는 시간을 많이 줄여주었습니다.

우리가 이 모든 것을 굳이 나열하는 이유는 다음과 같습니다. 각각이 실제 문서상의 공백(doc gap)이기 때문입니다. 이름을 붙여두는 것이 주말이 마찰 없이 매끄러웠던 척하는 것보다 다음 사람에게 훨씬 더 큰 도움이 됩니다.

이전 방식과의 비교

솔직하게 비교하자면, 우리의 이전 에이전트 작업물은 가공되지 않은 Python 코드와 LLM API 호출의 조합이었습니다. 오케스트레이션(orchestration)을 직접 작성해야 했고, 상태 사전(state dict)을 직접 관리해야 했으며, 모든 것을 직렬화(serialize)해야 했습니다. 시스템의 그래프(graph)는 오직 머릿속에만 존재했습니다. Jac는 그 많은 부분들을 가져갔습니다. 그래프는 딕셔너리(dictionary)로 흉내 내는 것이 아니라 실제 언어 구조물(language construct)로 존재합니다. 상태 지속성(state persistence)은 나중에 덧붙이는 데이터베이스가 아니라 기본값(default)으로 제공됩니다. 모델 경계(model boundary)는 40줄에 달하는 요청 생성 및 응답 파싱 대신 by llm을 통해 이루어집니다. 이것이 수월했다고 말씀드리지는 않겠습니다. 위로 올려서 발생했던 마찰(friction)들을 확인해 보세요. 하지만 결과적으로 이 프로그램은 우리가 만든 그 어떤 Python 프로토타입보다 문제의 형태를 훨씬 더 닮아 있게 되었습니다.

실제로 출시한 것들

  • 결정론적(deterministically)으로 구축된 리포지토리(파일, 임포트, 의존성, 테스트, 문서)의 지속 가능한 Jac 그래프.
  • 시스템 1: 보안, 호환성, 영향 범위(blast radius)를 검토하는 워커(walker)들을 하나의 리스크 판결(risk verdict)로 병합.
  • 시스템 2: 자율적인 의존성 파이프라인. 매니페스트 차이(manifest diff) 분석, 규칙 기반 리스크 분류, 샌드박스 실행, 결정론적 수정, 자동 수정 PR, Discord 알림. 지속적인 인시던트 상태(incident state) 및 재시도가 중복 PR을 생성하지 않도록 하는 멱등성(idempotency) 보장.
  • 테스트를 거친 결정론적 헬퍼(manifest parsing, name normalization, risk rules, fix inversion).
    jac test tests/test_system2.jac, 10개 통과.
  • 동일한 프로젝트 내의 Jac로 작성된 메인테이너 및 컨트리뷰터용 프론트엔드.

솔직한 부분은 이렇습니다. 샌드박스는 현재 완전히 연결된 클라우드 마이크로VM(microVM)이 아니라 npm 및 pip를 위한 로컬 서브프로세스 인스트루멘테이션(subprocess instrumentation) 단계이며, 대시보드의 일부는 여전히 데모 데이터로 실행됩니다. 우리는 의도적으로 이러한 이음새(seams)를 드러내어 두었습니다. 해커톤의 결과물이 조용히 거짓이 되어서는 안 되기 때문입니다.

데모: https://www.youtube.com/watch?v=ZN0UVnNUpRs

Repo: https://github.com/ayushmk7/GhostWatch

우리가 다시 Jac을 사용할까요?

구조화된 데이터 (Structured data)를 다루는 에이전트 시스템 (Agentic system)을 구축한다면, 네, 그리고 다음번에는 더 빠르게 할 수 있을 것입니다. 그래프 상의 워커 (Walker over a graph) 방식이 코드 방어 문제에 너무나 잘 들어맞아서, 우리가 논쟁했던 대부분의 내용은 인프라 (Plumbing)가 아니라 실제 보안 로직 (Security logic)에 관한 것이었습니다. 마찰은 분명히 존재했지만, 새로운 언어를 배우는 초기 몇 시간 동안 집중되었습니다. 그 이후로는 대부분 방해가 되지 않았습니다. 그리고 그래프 백엔드 (Graph backend), 자율형 파이프라인 (Autonomous pipeline), 프론트엔드 (Frontend)를 모두 아울러야 하는 주말 프로젝트로서, 이 모든 것을 하나의 스택 (Stack)으로 유지한 것이 멋진 아이디어와 실제로 세워놓고 데모를 할 수 있는 결과물 사이의 차이를 만들어냈습니다.

The two of us (left) with one of the co founders of Jaseci Labs Jason Mars!

코드를 살펴보고 싶다면 공개되어 있습니다: https://github.com/ayushmk7/GhostWatch. 무엇이든 물어보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0