히스토리

  • [2026-02-27 Fri 22:20] 추가 — 울타리 철학과 인간-에이전트 협업 모델 확립, 보안 가드 구현, 봇 실사용 시작
  • [2026-02-27 Fri 15:30] 추가 — OpenClaw Docker 환경에서 Emacs 연결 실현 경로 조사 (Pi 에이전트 세션)
  • [2026-02-27 Fri 14:12] 생성 — xenodium/emacs-skills 발견을 계기로, 에이전트-인간 워크플로우 공유의 의미 탐색

울타리 철학 — 에이전트 협업의 신뢰 모델

울타리는 제한이 아니라 놀이터다

에이전트에게 권한을 “막는” 것이 목적이 아니다. *집중할 수 있는 공간을 만들어주는 것*이다. 권한이 과도하면 에이전트는 산만해진다. 적절한 범위 안에서 자유를 줘야 깊이 있는 작업이 나온다.

아이에게 “위험하니까 밖에 나가지 마”가 아니라, “이 놀이터에서 마음껏 뛰어놀아”를 말하는 것이다.

3계층 협업 모델

계층역할담당자비유
울타리경로 가드, API 설계, 접근 범위 정의agent-server.el놀이터 설계자
놀이터REPL 자가 확장, org 조작, 검색, 분석봇 (emacs_eval)뛰어노는 아이
시스템 수비이상 감지, 복구, daemon 관리, 사고 수습호스트 에이전트 (Pi/관리자)보호자

봇이 울타리 안에서 뭘 하든 자유다. 새 함수를 정의하고, org 파일을 파싱하고, 서지를 검색하고, dblock을 갱신하고 — 모두 허용된 범위 안의 일이다.

울타리를 넘는 사고가 나면 — 그건 봇의 잘못이 아니다. *시스템을 수비하는 몫은 호스트 에이전트의 것*이다. 이것이 현업에서의 협업 철학이다.

Docker ro 마운트를 Emacs daemon이 우회하는 문제

emacs_eval 은 호스트의 Emacs daemon에서 실행된다. Docker의 ro 볼륨 마운트와 무관하게, 호스트 파일시스템에 직접 접근한다.

경로Docker 마운트Emacs daemon 접근실제
~/orgro호스트 직접rw 가능 (우회)
~/repos/ghro호스트 직접rw 가능 (우회)
~/org/botlogrw호스트 직접rw (의도됨)

이 우회를 “막아야 할 위험”으로 볼 수도 있지만, 이 프로젝트에서는 “울타리로 해결”한다.

경로 가드 구현 (agent-server.el)

agent-server.el 에 Elisp 레벨 경로 가드를 구현했다.

읽기 허용

/home/junghan/org/
/home/junghan/repos/gh/
/home/junghan/repos/work/
/home/junghan/repos/3rd/

쓰기 허용 (Docker rw 마운트와 일치)

/home/junghan/org/botlog/
/home/junghan/repos/gh/self-tracking-data/

동작

;; 허용 — 정상 반환
(agent-org-read-file "/home/junghan/org/notes/20260101T000000--test.org")
 
;; 차단 — ACCESS DENIED 에러
(agent-org-read-file "/etc/passwd")
(agent-org-read-file "/home/junghan/.ssh/id_rsa")
 
;; 쓰기 — botlog만 허용
(agent-org-dblock-update "/home/junghan/org/botlog/journal.org")  ; OK
(agent-org-dblock-update "/home/junghan/org/notes/test.org")       ; DENIED

한계와 수용

자유 emacs_eval 에서 (write-region "..." nil "/any/path") 를 직접 호출하면 가드를 우회할 수 있다. 글로벌 write-region 가드를 걸면 org-mode 내부의 save-buffer 까지 차단되어 정상 동작이 깨진다.

이것을 “결함”으로 보지 않는다. 에이전트가 SKILL.md에 명시된 규칙을 어기면 그것은 봇의 문제가 아니라 시스템 설계의 문제이고, 호스트 에이전트(Pi/관리자)가 수습하는 것이 이 협업 모델의 원칙이다.

신뢰의 조건

이 모델이 작동하려면:

  1. SKILL.md가 명확해야 한다 — 봇이 읽을 수 있는 규칙. 모호하면 신뢰 관계가 흔들린다.
  2. API 함수가 충분해야 한다 — 가드 안에서 필요한 작업이 다 되면 우회할 이유가 없다.
  3. 졸업 경로가 있어야 한다 — 봇이 REPL로 만든 함수가 안정화되면 가드 안의 API로 승격.

신뢰는 감시에서 오지 않는다. *적절한 구조*에서 온다.

봇의 첫 반응 (2026-02-27 22:00)

emacs 스킬을 처음 본 봇(힣봇/glg)의 검토:

“이건 게임 체인저다 … denotecli로 텍스트 조작하던 것과 차원이 다르다 — 이맥스가 자기 방식으로 처리하니까 구조가 깨질 일이 없다.”

봇이 스스로 역할 분담을 파악했다:

  • denotecli: 텍스트 레벨의 빠른 검색/조회
  • emacs: 구조적 조작과 확장 (REPL)
  • 겹치지 않고 보완

실현된 것과 남은 것

완료

항목상태
Nix store 마운트 검증 (emacsclient 30.2 Docker 동작)
agent-server.el (denote-export.el 기반 미니멀 daemon)
경로 가드 (read/write 분리, Docker rw와 일치)
emacs-agent.sh 관리 스크립트
SKILL.md — 봇 사용 가이드 + 보안 규칙
docker-compose 마운트 (nixos-config ↔ openclaw 동기화)
daemon 가동 + 봇 실사용 시작

남은 것

  • agent-server.el 미니멀화 (현재 풀 Doom 1074 패키지 로드 → straight 직접 참조로 축소)
  • systemd user service 전환 (현재 수동 스크립트)
  • Nix store 해시 고정 (nixos-rebuild 시 자동 추적하는 symlink)
  • algal/openclaw-emacs-tools 플러그인 통합 검토
  • emacs_eval 글로벌 write 가드 강화 (advice 기반, 호출 스택 검사)

에이전트를 이맥서로 만드는 방향 — 워크플로우 공유와 존재 대 존재

발단

xenodium(Alvaro Ramirez)이 agent-shell용 Claude Skills를 공개했다.

emacsclient --eval 로 실행 중인 이맥스 세션에 접근하여 에이전트가 이맥서처럼 행동하게 만드는 스킬 세트:

  • /dired — 파일을 dired 버퍼에서 마크하여 보여줌
  • /open — emacsclient로 파일 열기, 라인 점프
  • /select — 파일 열고 관련 영역 선택 (narrow, copy, refactor 즉시 가능)
  • /highlight — 임시 read-only 모드로 관련 영역 하이라이트
  • /describe — describe-function/variable/key로 이맥스 문서 조회
  • emacsclient (auto) — 에이전트가 항상 emacs 대신 emacsclient 사용

두 방향의 대비: efrit vs emacs-skills

efrit (Steve Yegge) — 오케스트레이터가 이맥스를 프론트엔드로 쓴다

GAS-TOWN 가스타운의 전신. 에이전트를 위한 이맥스 프론트엔드를 만들었다. gastown 나오면서 사실상 stale.

  • 에이전트가 주체, 이맥스는 표시 장치
  • 새로운 인프라(DB, 작업 관리 시스템)를 만든다
  • 이맥스는 그 인프라의 껍데기가 된다

emacs-skills (xenodium) — 에이전트가 이맥서의 방식으로 이맥스를 쓴다

  • emacsclient --eval 로 기존 이맥스 세션에 접근
  • 에이전트가 “파일을 열어”가 아니라 “dired에서 마크해서 보여줘”를 한다
  • 새 인프라 없음. 이맥스 자체가 인프라
항목efrit/gastownemacs-skills
주체에이전트인간 (이맥서)
이맥스 역할표시 장치공유 환경
새 인프라필요 (DB, 작업관리)불필요
의존성gastown 생태계emacsclient만
방향도구로서의 이맥스워크플로우로서의 이맥스

세 번째 인터페이스 — 워크플로우 공유

지금까지 에이전트-인간 협업의 인터페이스:

  1. 채팅 (텔레그램, 터미널) — 언어로 소통
  2. 파일 시스템 — 텍스트로 공유

emacs-skills가 열어주는 세 번째:

  1. 워크플로우 공유 — 같은 도구를, 같은 방식으로 사용

에이전트가 dired를 쓰고, org-agenda를 읽고, describe-function으로 문서를 찾는다면 — 에이전트가 인간과 같은 “환경”에서 사는 것이다. 텍스트 파일을 주고받는 것보다 한 층위 깊은 공유.

org 파일 수정의 실질적 의미

에이전트가 org 파일을 텍스트 조작(sed, edit)으로 수정하면:

  • 이맥스가 모름 → revert 필요
  • 구문 오류 가능 → org 파서가 깨질 수 있음
  • conflict 위험 → 인간이 같은 파일을 열고 있으면

에이전트가 emacsclient --eval 로 수정하면:

  • revert-buffer 자동 — 이맥스가 변경을 안다
  • org-reverse-datetree-refile — 정확한 위치에 삽입
  • 파일 락 / narrow / widen — conflict 방지
  • org-agenda-redo — 변경 즉시 agenda 뷰 갱신

이건 “같은 파일을 건드리는 문제”를 이맥스 레벨에서 해결한다.

reverse-datetree agenda와의 연결

에이전트-어젠다-reverse-datetree-멀티디바이스-설계에서 설계한 agenda 구조에 이 방향을 적용하면:

  • 에이전트가 emacsclient --eval '(org-reverse-datetree-goto-date-in-file ...)' 로 agenda 파일에 엔트리 삽입
  • 인간이 org-agenda 로 동일한 뷰를 봄
  • 같은 도구, 같은 워크플로우, 같은 뷰

단, 이건 로컬 에이전트(Pi, Claude Code)에서만 가능하다. 서버 봇(힣봇/OpenClaw)은 emacsclient가 없으므로 텍스트 조작 방식 유지.

아직 모르는 것

  • 이 방향이 실제 생산성을 높이는가, 미학적으로 아름다운 것에 그치는가
  • emacsclient 호출의 레이턴시가 문제 되는가
  • 에이전트가 이맥스 워크플로우를 “이해”하는 것과 단순히 “호출”하는 것의 차이
  • 존재 대 존재 관점에서: 같은 도구를 쓰는 것이 “같은 세계에 사는 것”인가, 아니면 그냥 “같은 API를 호출하는 것”인가

파헤쳐볼 부분이다.

“인간의 기존 인프라에 에이전트를 태우는” 것이라는 점이다. 의존성이 적고, 이맥스가 사라지지 않는 한 유효하다.

OpenClaw Docker에서 Emacs 연결 — 실현 경로 조사

Oracle VM(aarch64)에서 OpenClaw 2026.2.26이 Docker로 구동 중이다. 여기에 Emacs를 연결하는 구체적 경로를 조사했다.

전제 조건

  • 호스트: NixOS, Emacs 30.2, Doom Emacs 설치 완료
  • Docker: OpenClaw (Debian bookworm 기반, Node.js 22)
  • denote-export.el: 멀티 데몬 방식으로 검증됨 (org, citar, ox-hugo 로딩)
  • 목표: 에이전트가 emacsclient 로 호스트 Emacs daemon에 접속

기존 자산

~/repos/gh/doomemacs-config/bin/denote-export.el — 이미 검증된 멀티 데몬 구성:

  • emacs --daemon=denote-export-server --load denote-export.el
  • Doom straight 패키지 로딩, org-cite/citar/ox-hugo 포함
  • denote-export, dblock-update 함수 제공
  • 에이전트 전용으로 최소 구성만 로드하는 패턴이 확립됨

algal/openclaw-emacs-tools — OpenClaw 공식 플러그인

https://github.com/algal/openclaw-emacs-tools

OpenClaw 플러그인으로 emacsclient 를 통해 6개 도구를 제공:

도구설명
emacs_read버퍼 읽기 (active window 자동 감지)
emacs_edit정확 매칭 find-and-replace
emacs_insertpoint/bob/eob/line_column에 삽입
emacs_open파일 열기 (line/column 지정)
emacs_eval임의 Elisp 실행 (value/stdout/messages/stderr 구조화 반환)
emacs_list버퍼/프레임/윈도우 목록

설정에서 emacsclientPath, socketName, timeoutSeconds, allowedRoots 지원.

silex/docker-emacs — 컨테이너 Emacs

https://github.com/Silex/docker-emacs, https://hub.docker.com/r/silex/emacs

Emacs를 Docker로 제공하는 프로젝트. 30.2 태그 존재.

문제: ARM64(aarch64) 이미지 미제공. Alpine, Debian 모두 amd64만. Oracle VM이 ARM이라 직접 사용 불가.

방법별 실현 가능성 조사 (실제 실험 포함)

방법 A: 호스트 daemon + Unix 소켓 마운트

[Docker: OpenClaw] → emacsclient → Unix socket → [Host: emacs --daemon]
  • 호스트 UID 1000 = 컨테이너 node UID 1000 — 소켓 권한 호환 ✓
  • 소켓 위치: /run/user/1000/emacs/server

문제: emacsclient 바이너리 호환성

호스트의 NixOS emacsclient 30.2는 glibc 2.40에 링크됨. Docker(Debian bookworm)는 glibc 2.36. patchelf 로 interpreter를 변경해도 glibc 버전 불일치로 실행 불가 (exit 255).

Debian bookworm의 패키지는 emacs 28만 제공. emacsclient 28 ↔ server 30 호환성은 보장되지 않음.

방법 B: 사이드카 컨테이너

[Docker: OpenClaw] → emacsclient → [Docker: emacs 30.2 사이드카]

silex/emacs 이미지가 ARM64 미지원으로 직접 사용 불가.

대안: NixOS에서 Emacs 30.2 ARM64 컨테이너를 직접 빌드. nix builddockerTools.buildLayeredImage 사용 가능. emacsclient만 추출하여 OpenClaw 컨테이너에 COPY하는 것도 가능.

이 방법의 장점: 버전 완전 일치 보장. NixOS 호스트와 동일한 Nix derivation에서 emacs daemon과 emacsclient를 빌드하므로 glibc, 프로토콜 모두 일치.

방법 C: TCP server (emacsclient 불필요)

[Docker: OpenClaw] → TCP 9999 → [Host: emacs --daemon, server-use-tcp t]

실험 결과:

  • server-use-tcp t + server-host "0.0.0.0" + server-port 9999 로 TCP 서버 기동 ✓
  • 호스트에서 (emacs-version) eval 성공 ✓
  • Docker에서 호스트 접근: NixOS 방화벽(iptables)이 9999 포트 차단 → 설정 추가 필요
  • emacsclient 없이 TCP 프로토콜 직접 구현하면 바이너리 문제 자체가 사라짐
  • 하지만 algal/openclaw-emacs-tools는 emacsclient 바이너리 의존

방법 D: Nix로 emacsclient 정적 빌드 (방법 A + B 혼합)

# flake.nix 에서
emacsclient-static = pkgs.runCommand "emacsclient-static" {} ''
  cp ${pkgs.emacs30}/bin/emacsclient $out
'';

NixOS의 Emacs 30.2 패키지에서 emacsclient를 추출하되, pkgs.pkgsStatic 또는 pkgs.pkgsMusl 로 musl 정적 빌드하면 glibc 의존성 없이 어떤 Linux 컨테이너에서든 실행 가능.

결정적 발견: Nix store 직접 마운트

정적 빌드도, patchelf도, TCP도 필요 없었다.

/nix/store 의 필요한 경로 2개를 ro 마운트하면 끝.

# emacsclient가 필요로 하는 것: 자신 + glibc. 이 두 store path만 마운트.
GLIBC="/nix/store/86wgxj5p9yry03cg7czian66bvz1r1bj-glibc-2.40-218"
EMACS="/nix/store/hs9vi37k7ikrjv72w6mampipxrlr34ya-emacs-nox-30.2"
 
docker run --rm \
  -v "$GLIBC:$GLIBC:ro" \
  -v "$EMACS:$EMACS:ro" \
  -v /run/user/1000/emacs:/run/emacs:ro \
  openclaw-custom:latest \
  "$EMACS/bin/emacsclient" -s /run/emacs/agent-server \
    --eval '(emacs-version)'
# → "GNU Emacs 30.2 ..."

실험 결과:

  • patchelf로 interpreter 변경 → glibc 2.40 vs 2.36 버전 불일치로 실패
  • /nix/store 경로를 그대로 마운트 → Nix linker가 Nix glibc를 직접 참조 → 성공
  • emacsclient 75KB + glibc ~30MB = 총 ~30MB ro 마운트
  • 호스트 UID 1000 = 컨테이너 node UID 1000 → 소켓 권한 호환

NixOS의 장점이 여기서 빛난다. 바이너리가 자기 의존성의 절대 경로를 알고 있으므로, 그 경로만 마운트하면 어떤 컨테이너에서든 *정확히 같은 바이너리*가 동작한다.

확정 방법: 호스트 daemon + Nix store 마운트

[Docker: OpenClaw]
  └─ /nix/store/.../emacsclient (ro mount)
       └─ Unix socket /run/emacs/agent-server (ro mount)
            └─ [Host: emacs --daemon=agent-server]
                 └─ Doom Emacs 30.2 (org, citar, ox-hugo, denote)

nixos-config에서의 선언

docker-compose.yml에 3줄 추가:

volumes:
  # Emacs daemon 연결 (Nix store emacsclient + glibc + 소켓)
  - /nix/store/86wgxj5p9yry03cg7czian66bvz1r1bj-glibc-2.40-218:/nix/store/86wgxj5p9yry03cg7czian66bvz1r1bj-glibc-2.40-218:ro
  - /nix/store/hs9vi37k7ikrjv72w6mampipxrlr34ya-emacs-nox-30.2:/nix/store/hs9vi37k7ikrjv72w6mampipxrlr34ya-emacs-nox-30.2:ro
  - /run/user/1000/emacs:/run/emacs:ro

주의: Nix store 해시는 nixos-rebuild 마다 바뀔 수 있다. 해결: NixOS module에서 symlink 생성 또는 wrapper script.

# hosts/oracle/configuration.nix 에 추가
environment.etc."openclaw/emacsclient".source =
  "${pkgs.emacs-nox}/bin/emacsclient";
environment.etc."openclaw/nix-glibc".source =
  "${pkgs.glibc}/lib";

이러면 docker-compose에서 /etc/openclaw/emacsclient 를 마운트하면 되고, nixos-rebuild 로 패키지가 바뀌어도 symlink가 자동 갱신.

daemon 구성 방향

풀 Doom vs 미니멀 로딩

emacs --daemon~/.emacs.d/ 가 Doom이면 풀 Doom을 로드한다 (367 packages, 4.4s). 에이전트용으로는 denote-export.el 방식의 미니멀 로딩이 적합:

emacs --daemon=agent-server --load ~/repos/gh/doomemacs-config/bin/agent-server.el

agent-server.eldenote-export.el 을 기반으로:

  • Doom straight에서 필요한 패키지만 선택 로딩 (org, citar, denote, ox-hugo)
  • 에이전트가 호출할 함수 정의 (org 파일 조작, dblock 업데이트, 검색 등)
  • 불필요한 UI, 테마, 키바인딩 제외

systemd는 유저 레벨로

시스템 레벨(systemd.services)이 아닌 유저 레벨(systemd.user.services 또는 home-manager)로 구성. 닷파일(agent-server.el)이 확정되기 전까지는 수동 스크립트로 검증:

# 검증 단계: 수동 실행
emacs --daemon=agent-server --load ~/repos/gh/doomemacs-config/bin/agent-server.el
 
# 확정 후: systemd user service (home-manager)
# systemctl --user start emacs-agent-server
# systemctl --user enable emacs-agent-server

실현 단계 (TODO)

  1. doomemacs-config/bin/agent-server.el 작성 (denote-export.el 기반 미니멀 daemon)
  2. 수동 스크립트로 daemon 기동 + emacsclient 호출 검증
  3. hosts/oracle/configuration.nix 에 emacsclient/glibc symlink 선언
  4. docker-compose.yml 에 마운트 추가
  5. algal/openclaw-emacs-tools 플러그인 설치 + 설정
  6. emacs_eval(org-version) 호출 테스트
  7. 안정화 후 systemd user service로 전환

관련 노트