본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 16. 08:16

n8n, OpenClaw, Hermes가 내 사용 사례에 맞지 않아 skelm을 만들었습니다

요약

작성자는 AI 에이전트 워크플로우를 로컬 환경에서, TypeScript의 타입 시스템을 활용하여, 그리고 강력한 권한 강제 기능을 갖추어 구현하는 것을 목표로 했습니다. 기존 도구들(n8n, OpenClaw, Hermes 등)은 각각 시각적 흐름 중심, 메시징 프레임워크 중심, 또는 권고 기반의 보안 모델이라는 한계가 있어 사용자의 요구사항을 충족시키지 못했습니다. 이에 대한 대안으로 'skelm'을 개발했으며, 이는 실제 TypeScript 파일로 워크플로우를 정의하고 구조적인 권한 제어를 제공하는 것이 핵심 차별점입니다.

핵심 포인트

  • 워크플로우 구현 시 로컬 환경 및 TypeScript 타입 시스템 활용의 중요성 강조
  • n8n은 시각적 노드 그래프에 강점을 가지나, 코드 기반 워크플로우와는 거리가 있음
  • OpenClaw는 메시징/대화 흐름에 최적화되어 있어 파이프라인 제어(branch, loop 등)에는 부적합함
  • 기존 LLM 오케스트레이션 프레임워크의 권한 모델은 '권고' 수준이라 보안 경계가 될 수 없음
  • skelm은 워크플로우를 실제 .ts 파일로 작성하여 타입 안전성, 테스트 용이성, 그리고 백엔드 호출 전 구조적(Structural) 권한 강제를 제공함

나는 AI 에이전트 워크플로우 (AI agent workflows)를 로컬에서, TypeScript로, 그리고 실제 권한 강제 (permission enforcement) 기능을 갖추어 실행해야 했습니다. n8n, OpenClaw, Hermes와 같은 명백한 선택지들을 검토하며 시간을 보냈지만, 그 중 어느 것도 내 요구사항에 완전히 부합하지 않았습니다. 그래서 skelm을 만들었습니다. 이 포스트는 구체적으로 무엇이 맞지 않았는지, skelm은 무엇을 다르게 하는지, 그리고 그 차이가 왜 중요한지에 대한 하나의 구체적인 사례에 대해 다룹니다.

기존 도구들이 (내 사용 사례에서) 잘못하고 있는 점

n8n
n8n은 진정으로 훌륭한 도구입니다. 하지만 시각적 흐름 (visual flows)과 노드 그래프 모델 (node-graph model)을 중심으로 구축되었습니다. 나는 실제 .ts 모듈을 작성하고, 전체 TypeScript 타입 시스템 (type system)을 사용하며, vitest를 실행하고, 다른 코드와 마찬가지로 워크플로우를 git에 체크인하고 싶습니다. 깔끔하게 diff를 낼 수 없는 JSON 설정이나, 편집을 위해 브라우저가 필요한 캔버스 (canvas)를 원하지 않습니다.

OpenClaw
OpenClaw는 개인 비서 사용 사례 — 채팅 라우팅 (chat routing), 멀티 채널 (multi-channel), 에이전트 대화 — 에 매우 탁월합니다. 하지만 그것은 근본적으로 메시징 프레임워크 (messaging framework)입니다. 나에게는 파이프라인 프리미티브 (pipeline primitives)가 필요했습니다: 분기 (branch), 병렬 (parallel), forEach, 루프 (loop), 대기 (wait). 조합 가능하고 (composable), 테스트 가능하며 (testable), 결정론적인 (deterministic) 기능 말입니다. OpenClaw는 이를 위해 구축되지 않았으며, 그런 척하지도 않습니다.

Hermes 및 대부분의 LLM 오케스트레이션 프레임워크 (LLM orchestration frameworks)
권한 모델이 권고 사항 (advisory) 수준입니다. 시스템 프롬프트 (system prompt)에서 모델에게 무엇을 해야 하는지 알려줍니다. 모델은 이를 준수하려고 노력합니다. 이것은 작동하다가 어느 순간 작동하지 않게 됩니다 — 새로운 도구 기능이 출시되거나, 모델이 해당 작업에 기능이 필요하다고 판단하거나, 정교하게 조작된 입력 (crafted input)이 지침을 넘어설 수 있도록 유도할 때 말입니다. 권고 기반의 권한은 보안 경계 (security boundary)가 아닙니다. 그것은 단지 예의 바른 요청일 뿐입니다.

skelm이 다르게 하는 점

  1. DSL이 아닌 실제 TypeScript
    워크플로우는 .ts 파일입니다. YAML도, JSON 설정도, 시각적 캔버스도 없습니다. 전체 타입 시스템, 전체 테스트 러너, 전체 IDE 지원을 제공합니다:

export default pipeline ({
id : ' triage-incident ' ,
input : z . object ({
incidentId : z . string (),
severity : z . enum ([ ' critical ' , ' high ' , ' low ' ])
}),
steps : [
branch ({
id : ' severity-gate ' ,
on : ( ctx ) => ( ctx . input as { severity : string }).

severity, cases: { critical: parallel({ id: ' triage ', steps: [ code({ id: ' search-issues ', run: searchGitHub }), code({ id: ' create-channel ', run: createSlackChannel }), ], }), high: code({ id: ' notify-oncall ', run: notifyOncall }), low: code({ id: ' acknowledge ', run: sendAck }), }, }), agent({ id: ' root-cause ', backend: ' codex ', prompt: (ctx) => buildRcaPrompt(ctx), permissions: { fsRead: [ './runbooks' ], fsWrite: [], networkEgress: ' deny ', }, }), ], }) 이것은 실제로 실행 가능한 워크플로우 (workflow)입니다. skelm run triage-incident.workflow.ts --input '{"incidentId":"INC-001","severity":"critical"}' . 로컬 실행에는 게이트웨이 (gateway)가 필요하지 않습니다. 브라우저도 필요 없습니다. 2. 권고 사항이 아닌 구조적 권한 (Structural permissions) 이 이것은 다른 모든 설계 결정의 근간이 되는 핵심 설계 결정입니다. skelm에서 권한은 컨텍스트 윈도우 (context window) 내에서가 아니라, 백엔드 (backend)가 호출되기 전 경계에서 강제됩니다. 그 모습은 다음과 같습니다:

agent({ id: ' fix-bug ', backend: ' codex ', prompt: (ctx) => Fix: ${(ctx.input as { test: string }).test} , permissions: { fsRead: [ './src', './test' ], fsWrite: [ './src' ], // src만 허용 — 테스트와 설정은 읽기 전용 유지 allowedExecutables: [ ' node ', ' npm ' ], networkEgress: ' deny ', allowedMcpServers: [], allowedSkills: [], }, })

이것들은 힌트 (hints)가 아닙니다. 이들은 세 가지 계층에서 강제됩니다:

Pre-run mapper (실행 전 매퍼) — 어떤 SDK 호출이 이루어지기 전에, skelm은 정책을 백엔드 전용 옵션으로 변환하며 안전하지 않은 조합을 거부합니다. 승인 정책이 설정된 상태에서 fsWrite: ['*']를 사용하면 즉시 CodexPermissionError를 발생시킵니다. skelm은 위험한 전체 권한 샌드박스 (sandbox) 모드로 조용히 권한을 격상시키는 것을 거부합니다.

Backend in-process enforcement (백엔드 프로세스 내 강제) — Codex의 샌드박스, Pi의 권한 강제기, 그리고 skelm 자체의 @skelm/agent 백엔드는 모두 프로세스 내에서 네이티브하게 강제합니다. skelm의 역할은 선언된 권한에서 백엔드 옵션으로의 변환이 완전하며, 어떠한 범위도 확장하지 않도록 보장하는 것입니다.

Gateway egress proxy (게이트웨이 송신 프록시) — 게이트웨이는 임베디드된 CONNECT 프록시를 실행합니다.

HTTP_PROXY는 서브프로세스 환경(subprocess environments)으로 병합되어 외부로 나가는 TCP를 가로챕니다. allowHosts는 모델의 컨텍스트 윈도우(context window)가 아닌 TCP 레벨에서 강제됩니다.

  1. 이것이 중요한 이유에 대한 구체적인 예시
    Codex 백엔드를 연결했을 때, 저는 networkEgress: 'deny'를 설정하고 이를 networkAccessEnabled: false에 매핑했습니다. 이는 샌드박스 셸(sandbox-shell)의 네트워크 호출 — curl, wget 등 — 을 차단합니다. 하지만 Codex에는 샌드박스 셸을 통하는 것이 아니라 Codex 프로세스 내부에서 실행되는 내장 web_search 도구가 있습니다. networkAccessEnabled: false는 이를 비활성화하지 못합니다. 저의 'deny' 설정은 한 영역은 방어했지만 다른 영역은 놓치고 있었던 것입니다. 권한 매퍼(permission mapper)에서의 수정 사항은 다음과 같습니다:

// Codex는 두 개의 별개 네트워크 접점(surfaces)을 가집니다:
// 1. networkAccessEnabled — 샌드박스 셸 송신 (curl, wget 등)
// 2. webSearchMode — 모델의 내장 도구, 프로세스 내부에서 실행됨,
// 게이트웨이 프록시(gateway proxy)에 의해 가로챌 수 없음

const networkAccessEnabled = policy.networkEgress !== 'deny' // web_search는 명시적인 전체 'allow' 설정 시에만 활성화됩니다. // { allowHosts: ['api.example.com'] } 또한 이를 비활성화합니다 — // 프록시는 셸 TCP에 대해 allowHosts를 강제할 수 있지만, 프로세스 내부 검색은 그렇지 못합니다.

const webSearchAllowed = policy.networkEgress === 'allow'
const webSearchEnabled = webSearchAllowed
const webSearchMode: WebSearchMode = webSearchAllowed ? 'live' : 'disabled'

이것이 바로 권한 권고(advisory permissions)가 커버할 수 없는 종류의 격차입니다. "네트워크에 접속하지 마세요"라고 말하는 시스템 프롬프트(system prompt)는 web_search가 존재한다는 사실을 알지 못합니다.

  1. 멀티 백엔드, 플러그인 방식, 종속성 없음
    저는 워크플로(workflows)를 다시 작성하지 않고도 백엔드를 교체하고 싶었습니다. skelm은 다음을 지원합니다:
백엔드설명
@skelm/agent퍼스트 파티(First-party), 모든 OpenAI 호환 엔드포인트, 프로세스 내부 강제
@skelm/codex@openai/codex-sdk를 통한 OpenAI Codex — 샌드박스 인식, MCP, 스트리밍
@skelm/piPi 코딩 에이전트
@skelm/opencodeOpencode 코딩 에이전트 (native + ACP)
@skelm/vercel-aiVercel AI SDK
Built-in ACPCopilot, Claude Code, Gemini CLI

동일한 agent() 단계가 이 모든 것에서 작동합니다.

백엔드 교체: backend: 'codex'를 backend: 'pi'로 교체해도 권한 모델 (permission model), 기술 주입 (skill injection), 스트리밍 (streaming) 기능이 모두 그대로 유지됩니다.

  1. 일급 프리미티브 (first-class primitive)로서의 Human-in-the-loop
    wait()는 호출자가 HTTP를 통해 재개할 때까지 실행을 일시 중지합니다:

wait ({
id : ' human-review ' ,
message : ' Review this expense and approve or reject it. ' ,
output : z . object ({
decision : z . enum ([ ' approve ' , ' reject ' ]),
comments : z . string (). optional (),
}),
})

어디서든 재개하기

curl -X POST http://localhost:14738/runs/<runId>/resume
-d '{"output":{"decision":"approve","comments":"Looks good"}}'

웹훅 (webhook) 통합도, 플러그인 (plugin)도 아닙니다. 일급 단계 유형 (first-class step kind)입니다.

  1. 변조 방지 감사 (Tamper-evident audit)
    모든 권한 결정, 도구 호출 (tool call), 비밀 정보 접근 (secret access), 승인 사항은 해시 체인으로 연결된 감사 저널 (audit journal)에 기록됩니다. 체인이 변경되지 않았음을 검증할 수 있습니다:

skelm audit query --verify

이는 에이전트가 단순히 무엇을 하라고 지시받았는지가 아니라, 실제로 무엇을 할 수 있도록 허용되었는지를 증명해야 하는 컴플라이언스 (compliance) 사용 사례에서 매우 중요합니다.

skelm이 아닌 것
이것은 완성된 제품이 아닙니다. v1 이전 단계이며, API가 불안정하고 다듬어지지 않은 부분이 있습니다. 권한 모델은 견고하며 핵심 단계 프리미티브 (step primitives)는 작동합니다. 하지만 이를 둘러싼 생태계 — 통합 (integrations), 문서 (docs), 도구 (tooling) — 는 여전히 성장 중입니다.

또한 시각적 워크플로 자동화 (visual workflow automation)를 위한 n8n이나, 대화형 에이전트 (conversational agents)를 위한 OpenClaw를 대체하려는 것도 아닙니다. skelm은 다른 영역을 차지합니다. 코드를 작성하고 싶고, 에이전트가 실제로 무엇을 할 수 있도록 허용되었는지에 신경을 쓰며, 그것이 구조적으로 강제되기를 원하는 경우를 위한 것입니다.

시작하기

npm install -g skelm

skelm init my-project && cd my-project && npm install skelm

run workflows/hello.workflow.ts --input '{"name":"world"}'

전체 문서 및 소스: github.com/scottgl9/skelm

만약 n8n, OpenClaw, Hermes 또는 다른 오케스트레이션 프레임워크 (orchestration framework)를 사용하면서 저와 같은 벽에 부딪혔다면, 어떤 차이가 있었는지 진심으로 듣고 싶습니다. 권한 모델이나 모든 백엔드 통합에 관한 질문은 댓글로 남겨주시면 기꺼이 답변해 드리겠습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0