Skip to content

Commit 45a275b

Browse files
author
박성모
committed
feat(surf): sandboxed web → markdown brief command
Add an end-to-end '명령 → 검색 → 문서' pipeline that runs entirely inside a throwaway Docker container, fully isolated from the OpenClaw main container. surf "오늘 코스피 종가와 거래대금" surf "이번 주 IT 빅뉴스 5건" --max 8 surf "S&P 500 weekly recap" --lang en surf "이 논문 요약" --sources url://https://arxiv.org/abs/2310.06825 Pipeline: 1) host CLI scripts/surf parses args, exports env, calls 'docker compose -f openclaw-mgr/compose.surf.yml run --rm surf' 2) container (mcr.microsoft.com/playwright/python:v1.46.0-jammy) runs scripts/surf-lib/surf.py: - RSS pull (Yonhap, Hani, Google News KO; Reuters, BBC, Google News EN) with simple keyword filtering - Naver search + finance.naver.com (auto-included for KOSPI/ KOSDAQ keywords) - Wikipedia (ko/en) - Direct URL via 'url://https://...' 3) bodies are sent to host Ollama via host.docker.internal:11434 (qwen2.5-coder:7b by default) which writes a structured Markdown brief: title, TL;DR, key bullets, detail, sources. 4) container exits, output lives at ~/openclaw-surf/out/<slug>.md. Hardening on the surf container: - read_only: true, tmpfs:/tmp - cap_drop: ALL, no-new-privileges - only ~/openclaw-surf/out mounted rw; lib mounted ro - --rm: every run is a fresh container, no persistent state - no access to OpenClaw .env, ~/.ssh, or other host paths - OpenClaw main container stays in isolated mode throughout Files: + openclaw-mgr/compose.surf.yml + scripts/surf (host CLI) + scripts/surf-setup.sh (idempotent setup, ~/bin/surf symlink, image prefetch) + scripts/surf-lib/surf.py (collector + Ollama brief) ~ docs/GUIDE-WEB-FETCH.md (new section 8 KO+EN, TOC) ~ README.md / README.en.md (doc map mentions surf)
1 parent aa21fe3 commit 45a275b

7 files changed

Lines changed: 557 additions & 2 deletions

File tree

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ You may see system dialogs for Docker Desktop / Xcode CLT — just accept them.
8383
| 🆕 **First time / never used a terminal** | [docs/QUICKSTART-en.md](docs/QUICKSTART-en.md) | Step-by-step from "open Terminal", with sample output |
8484
| 🇰🇷 **한국어 처음 사용자** | [docs/QUICKSTART-ko.md](docs/QUICKSTART-ko.md) | 한국어 버전 |
8585
| 📖 **Unfamiliar with the terms (Ollama · Docker · OpenClaw)** | [docs/GUIDE-OLLAMA.md](docs/GUIDE-OLLAMA.md) · [docs/GUIDE-DOCKER.md](docs/GUIDE-DOCKER.md) · [docs/GUIDE-OPENCLAW.md](docs/GUIDE-OPENCLAW.md) | Three 3-minute primers (concepts, vocabulary, philosophy) — KO+EN |
86-
| 🌐 **Pulling stocks · news · FX from the web** | [docs/GUIDE-WEB-FETCH.md](docs/GUIDE-WEB-FETCH.md) | Network toggle cycle, real-world prompts, official APIs, automation, troubleshooting. KO+EN |
86+
| 🌐 **Pulling stocks · news · FX from the web** | [docs/GUIDE-WEB-FETCH.md](docs/GUIDE-WEB-FETCH.md) | Network toggle cycle, real-world prompts, official APIs, automation, troubleshooting. **Includes `surf` command — sandboxed Docker fetch → Markdown brief**. KO+EN |
8787
| 🎨 **Designer workflow automation (Pinterest → nano-banana → Figma)** | [docs/GUIDE-CREATIVE-PIPELINE.md](docs/GUIDE-CREATIVE-PIPELINE.md) | 4-step manual → one command. 4 parallel nano-banana windows for ~3.7× speedup. KO+EN |
8888
| 👤 **General user** | [README.en.md](README.en.md) (this file) | Command catalog · `.env` · network isolation · FAQ |
8989
| 🇰🇷 **일반 사용자 (KO)** | [README.md](README.md) | Korean main README |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ cd openclaw-workspace/openclaw-mgr
9696
| 🆕 **처음 보는 사람 / 터미널 처음** | [docs/QUICKSTART-ko.md](docs/QUICKSTART-ko.md) | 터미널 여는 법부터 단계별로, 예시 출력 포함 |
9797
| 🇬🇧 **English first-timer** | [docs/QUICKSTART-en.md](docs/QUICKSTART-en.md) | Same as above, in English |
9898
| 📖 **단어가 낯설면 (Ollama · Docker · OpenClaw)** | [docs/GUIDE-OLLAMA.md](docs/GUIDE-OLLAMA.md) · [docs/GUIDE-DOCKER.md](docs/GUIDE-DOCKER.md) · [docs/GUIDE-OPENCLAW.md](docs/GUIDE-OPENCLAW.md) | 3분용 기초 가이드 3편 (구조·용어·철학) — KO+EN 병기 |
99-
| 🌐 **웹에서 코스피·뉴스·환율 가져오기** | [docs/GUIDE-WEB-FETCH.md](docs/GUIDE-WEB-FETCH.md) | 네트워크 토글 사이클·실전 프롬프트·공식 API 키·자동화·트러블슈팅. KO+EN 병기 |
99+
| 🌐 **웹에서 코스피·뉴스·환율 가져오기** | [docs/GUIDE-WEB-FETCH.md](docs/GUIDE-WEB-FETCH.md) | 네트워크 토글 사이클·실전 프롬프트·공식 API 키·자동화·트러블슈팅. **`surf` 명령으로 샌드박스 도커 안에서 검색 → 마크다운 도구 포함**. KO+EN 병기 |
100100
| 🎨 **디자이너 워크플로우 자동화 (Pinterest → 나노바나나 → Figma)** | [docs/GUIDE-CREATIVE-PIPELINE.md](docs/GUIDE-CREATIVE-PIPELINE.md) | 4단계 수작업 → 1명령. 나노바나나 4창 병렬로 속도 ~3.7×. KO+EN 병기 |
101101
| 👤 **일반 사용자** | [README.md](README.md) (이 문서) | 명령 카탈로그·`.env`·네트워크 격리·FAQ |
102102
| 🇬🇧 **General user (EN)** | [README.en.md](README.en.md) | Full English equivalent of this README |

docs/GUIDE-WEB-FETCH.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [5. 안전 패턴 (꼭 읽기)](#5-안전-패턴-꼭-읽기)
1717
- [6. 자동화 — 매일 아침 9시 코스피 요약](#6-자동화--매일-아침-9시-코스피-요약)
1818
- [7. 트러블슈팅](#7-트러블슈팅)
19+
- [8. 🧪 샌드박스 자동 브리프 — `surf` 명령](#8--샌드박스-자동-브리프--surf-명령)
1920
- [🇬🇧 English](#-english)
2021
- [TL;DR](#tldr)
2122
- [1. Open the network → work → lock back (recommended cycle)](#1-open-the-network--work--lock-back-recommended-cycle)
@@ -231,6 +232,105 @@ crontab -e
231232
| `network online` 한 채로 깜빡 잊음 | 사람의 망각 |[1회용 세션 스니펫](#권장-1회용-인터넷-세션-스니펫) 으로 자동 잠금 |
232233
| 사이트가 봇 차단(403/429) | UA 누락 / rate limit | RSS 로 전환. 정 안 되면 `Mozilla/5.0` UA 명시. 그래도 안 되면 그 사이트는 포기. |
233234

235+
### 8. 🧪 샌드박스 자동 브리프 — `surf` 명령
236+
237+
> **OpenClaw 메인 컨테이너를 건드리지 않고**, 1회용 격리 컨테이너에서 검색·수집·요약·문서화까지 한 번에 끝냅니다. 코스피 종가, 뉴스 5선, 환율 등 **명령 → 검색 → 마크다운** 시나리오에 최적화.
238+
239+
#### 한 줄로:
240+
241+
```bash
242+
surf "오늘 코스피 종가와 거래대금"
243+
# → ~/openclaw-surf/out/오늘-코스피-종가와-거래대금-20260426-0930.md
244+
245+
surf "이번 주 IT 빅뉴스 5건" --max 8
246+
surf "S&P 500 weekly recap" --lang en --sources rss,wikipedia
247+
surf "이 논문 요약" --sources url://https://arxiv.org/abs/2310.06825
248+
surf "삼성전자 외국인 매매 추이" --out samsung-foreign.md
249+
```
250+
251+
#### 어떻게 동작하나
252+
253+
```
254+
호스트(맥북) 인터넷
255+
└ surf "오늘 코스피 ..." ─► Docker compose run --rm
256+
257+
┌──────▼──────────────────┐
258+
│ 1회용 컨테이너 │ ⇄ 일반 인터넷
259+
│ Playwright + Python │
260+
│ read_only: true │
261+
│ cap_drop: ALL │
262+
│ tmpfs:/tmp │
263+
│ 마운트: out/ 만 read-write │
264+
└──────┬──────────────────┘
265+
│ host.docker.internal:11434
266+
┌──────▼─────────────────┐
267+
│ 호스트 Ollama │
268+
│ (qwen2.5-coder:7b) │
269+
└──────┬─────────────────┘
270+
271+
~/openclaw-surf/out/*.md ◄┘ (호스트에 저장, 컨테이너는 사라짐)
272+
```
273+
274+
#### OpenClaw 메인 컨테이너와의 격리
275+
276+
| 항목 | 영향? |
277+
|---|---|
278+
| OpenClaw 컨테이너 `isolated` 모드 유지? | ✅ 그대로 유지 (별개 compose 프로젝트, 별개 네트워크) |
279+
| 호스트 `~/.ssh`, `~/Documents` 접근 | ❌ 마운트 없음 |
280+
| OpenClaw `.env` / `compose.security.yml` 접근 | ❌ 마운트 없음 |
281+
| 컨테이너의 영속 변경 |`read_only: true` + `--rm` (실행 후 자동 삭제) |
282+
| 권한 상승 (sudo) |`cap_drop: ALL` + `no-new-privileges` |
283+
| 호스트 Ollama 호출 |`host.docker.internal:11434`|
284+
285+
즉, **명령마다 새 컨테이너** 가 떴다 사라지고, 호스트로 빠져나오는 건 출력 마크다운 한 개뿐입니다.
286+
287+
#### 1회 세팅
288+
289+
```bash
290+
~/DEV/openclaw-workspace/scripts/surf-setup.sh
291+
# - SURF_HOME (~/openclaw-surf) 생성
292+
# - Playwright Docker 이미지 사전 pull (~700MB)
293+
# - ~/bin/surf 런처
294+
```
295+
296+
#### 옵션
297+
298+
| 옵션 | 효과 | 기본 |
299+
|---|---|---|
300+
| `--sources rss,naver,wikipedia` | 출처 선택. `url://https://...` 도 가능 | `rss,naver,wikipedia` |
301+
| `--lang ko` / `--lang en` | RSS 피드와 위키 언어 | `ko` |
302+
| `--max N` | 컨테이너가 모을 본문 수 상한 | `6` |
303+
| `--out NAME.md` | 출력 파일명 (생략 시 자동 슬러그) | 자동 |
304+
| `SURF_OPEN=1 surf ...` | 완료 후 `open` 으로 자동 열기 | `0` |
305+
306+
#### 환경변수 (compose.surf.yml 가 읽음)
307+
308+
| 변수 | 의미 |
309+
|---|---|
310+
| `SURF_HOME` | 결과 저장 위치 (기본 `~/openclaw-surf`) |
311+
| `OLLAMA_TEXT_MODEL` | 요약 모델 (기본 `qwen2.5-coder:7b`) |
312+
313+
#### 일정 자동화 (cron / launchd)
314+
315+
```bash
316+
# 평일 오전 9시 5분에 코스피·코스닥 브리프
317+
crontab -e
318+
5 9 * * 1-5 ~/bin/surf "오늘 코스피·코스닥 종가·거래대금" --out kospi-$(date +\%F).md
319+
```
320+
321+
> 결과는 항상 `~/openclaw-surf/out/` 에 마크다운으로 떨어지므로 [Obsidian / Bear / Notes 자동 import](https://help.obsidian.md/Files+and+folders/Folder+structure) 에 그대로 연결됩니다.
322+
323+
#### `surf` 트러블슈팅
324+
325+
| 증상 | 대응 |
326+
|---|---|
327+
| `Ollama 데몬이 응답하지 않습니다` | Ollama 앱 실행 후 다시 |
328+
| 첫 실행이 느림 | Playwright 이미지(~700MB) 첫 pull. 다음부턴 ~5초 |
329+
| 출처 본문이 비어있음 | RSS 가 가장 안정적. `--sources rss` 만으로도 시도 |
330+
| 결과가 너무 짧음 | `--max 8` 로 본문 더 모으기, 또는 `OLLAMA_TEXT_MODEL=qwen2.5:14b-instruct-q4_K_M` |
331+
| Bot 차단(403/429) | `--sources rss,wikipedia` 또는 `url://` 로 직접 URL 지정 |
332+
333+
234334
---
235335

236336
## 🇬🇧 English
@@ -369,3 +469,31 @@ crontab -e
369469
| Result truncated | Context length exceeded | Use a larger-context model, or pre-cut HTML with `htmlq`/`pup` |
370470
| Forgot to lock back to isolated | Human forgetfulness | Use the [one-shot session snippet](#one-shot-internet-session-snippet) |
371471
| 403 / 429 from a site | Bot detection / rate limit | Switch to RSS; add `Mozilla/5.0` UA; otherwise abandon that source |
472+
473+
### 8. 🧪 Sandboxed auto-brief — the `surf` command
474+
475+
> One command → throwaway Docker container fetches and summarizes → Markdown brief on disk. The OpenClaw main container stays in `isolated` the whole time.
476+
477+
```bash
478+
surf "today's KOSPI close and turnover"
479+
surf "S&P 500 weekly recap" --lang en
480+
surf "this week's IT headlines" --max 8
481+
surf "summarize this paper" --sources url://https://arxiv.org/abs/2310.06825
482+
```
483+
484+
**Sandbox guarantees** — every run spawns a fresh `mcr.microsoft.com/playwright/python` container with `read_only: true`, `cap_drop: ALL`, `no-new-privileges`, `tmpfs:/tmp`. Only `~/openclaw-surf/out` is mounted (read-write); `~/.ssh`, the OpenClaw `.env`, and the OpenClaw container are **not** touched. The container hits `host.docker.internal:11434` to reach the host Ollama, then is `--rm`'d.
485+
486+
**One-time setup:**
487+
488+
```bash
489+
~/DEV/openclaw-workspace/scripts/surf-setup.sh # pulls Playwright image, creates ~/bin/surf
490+
```
491+
492+
**Options:** `--sources rss,naver,wikipedia,url://https://...` · `--lang ko|en` · `--max N` · `--out file.md` · env `SURF_OPEN=1` to auto-open.
493+
494+
**Cron example:**
495+
496+
```bash
497+
5 9 * * 1-5 ~/bin/surf "today's KOSPI/KOSDAQ close and turnover" --out kospi-$(date +\%F).md
498+
```
499+

openclaw-mgr/compose.surf.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# =============================================================================
2+
# compose.surf.yml — 웹서핑 샌드박스 (Playwright + Python)
3+
# -----------------------------------------------------------------------------
4+
# 1회용 컨테이너로 인터넷에서 정보를 수집하고 마크다운 브리프를 생성합니다.
5+
# OpenClaw 메인 컨테이너와는 별개의 네트워크에서 동작합니다.
6+
#
7+
# 격리 규칙:
8+
# • 호스트의 Ollama(127.0.0.1:11434) 만 host.docker.internal 로 접근.
9+
# • 그 외 인터넷은 일반 인터넷 (DNS 정상). 작업 끝나면 컨테이너 자체가 사라짐.
10+
# • 호스트 파일시스템은 출력 디렉터리(~/openclaw-surf) 만 read-write 마운트.
11+
# • OPENCLAW_DIR / .env / ~/.ssh 등은 일절 마운트 안 함.
12+
# • 매 실행마다 새 컨테이너 (--rm). 안에서 영속 변경 금지.
13+
# =============================================================================
14+
name: openclaw-surf
15+
16+
services:
17+
surf:
18+
image: mcr.microsoft.com/playwright/python:v1.46.0-jammy
19+
# 출력 디렉터리만 마운트
20+
volumes:
21+
- ${SURF_HOME:-${HOME}/openclaw-surf}/out:/work/out:rw
22+
- ${SURF_REPO}/scripts/surf-lib:/work/lib:ro
23+
working_dir: /work
24+
# 호스트의 Ollama 데몬을 host.docker.internal 로 호출
25+
extra_hosts:
26+
- "host.docker.internal:host-gateway"
27+
# 보안 하드닝
28+
read_only: true
29+
tmpfs:
30+
- /tmp:size=512m
31+
- /work/.cache:size=128m
32+
cap_drop: ["ALL"]
33+
security_opt:
34+
- "no-new-privileges:true"
35+
# 환경변수: 컨테이너 안 surf.py 가 읽음
36+
environment:
37+
OLLAMA_HOST: "http://host.docker.internal:11434"
38+
OLLAMA_TEXT_MODEL: "${OLLAMA_TEXT_MODEL:-qwen2.5-coder:7b}"
39+
QUERY: "${QUERY}"
40+
SOURCES: "${SOURCES:-rss,naver,wikipedia}"
41+
MAX_PAGES: "${MAX_PAGES:-6}"
42+
LANG: "${LANG:-ko}"
43+
OUT_FILE: "/work/out/${OUT_FILE}"
44+
# 컨테이너에 1개의 명령만: lib/surf.py
45+
command: ["python", "/work/lib/surf.py"]

scripts/surf

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# scripts/surf — 명령 → 웹 검색 → 마크다운 브리프 (샌드박스 컨테이너)
4+
# -----------------------------------------------------------------------------
5+
# 사용:
6+
# surf "오늘 코스피 종가와 거래대금"
7+
# surf "한겨레 1면 5개 요약" --lang ko
8+
# surf "S&P 500 weekly recap" --lang en --sources rss,wikipedia
9+
# surf "이 논문 요약: https://arxiv.org/abs/2310.06825" --sources url://https://arxiv.org/abs/2310.06825
10+
# surf "오늘 IT 빅뉴스" --out today-tech.md --max 8
11+
#
12+
# 동작:
13+
# 1) 임시 Docker 컨테이너(Playwright/Python) 를 띄움
14+
# 2) 컨테이너 안에서 RSS·네이버·위키 등에서 본문 수집
15+
# 3) 호스트 Ollama 에 마크다운 브리프 요청
16+
# 4) ~/openclaw-surf/out/<file>.md 로 저장 후 컨테이너 자동 삭제
17+
#
18+
# OpenClaw 메인 컨테이너의 isolated 모드와 무관 — 전혀 영향 없음.
19+
# Copyright 2026 박성모 Park Sungmo — MIT License
20+
# =============================================================================
21+
set -euo pipefail
22+
23+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24+
SURF_HOME="${SURF_HOME:-$HOME/openclaw-surf}"
25+
mkdir -p "$SURF_HOME/out"
26+
chmod 700 "$SURF_HOME"
27+
28+
# Ollama 데몬 확인
29+
if ! curl -s --max-time 2 http://127.0.0.1:11434/api/tags >/dev/null; then
30+
echo "✗ Ollama 데몬이 응답하지 않습니다 (127.0.0.1:11434)" >&2
31+
echo " Ollama 앱을 실행한 뒤 다시 시도하세요." >&2
32+
exit 2
33+
fi
34+
35+
# 인자 파싱
36+
query=""
37+
sources="rss,naver,wikipedia"
38+
lang="ko"
39+
max=6
40+
out=""
41+
while [ $# -gt 0 ]; do
42+
case "$1" in
43+
--sources) sources="$2"; shift 2 ;;
44+
--lang) lang="$2"; shift 2 ;;
45+
--max) max="$2"; shift 2 ;;
46+
--out) out="$2"; shift 2 ;;
47+
-h|--help)
48+
sed -n '4,18p' "$0" | sed 's/^# //; s/^#//'
49+
exit 0 ;;
50+
*)
51+
if [ -z "$query" ]; then query="$1"; else query="$query $1"; fi
52+
shift ;;
53+
esac
54+
done
55+
56+
[ -n "$query" ] || { echo "검색어 필요. 사용: surf \"...\"" >&2; exit 2; }
57+
58+
# 출력 파일명: --out 없으면 슬러그 + 날짜
59+
if [ -z "$out" ]; then
60+
slug="$(echo "$query" | tr '[:upper:]' '[:lower:]' \
61+
| sed -E 's/[^a-z0-9가-힣]+/-/g; s/^-+|-+$//g' | cut -c1-40)"
62+
out="${slug:-brief}-$(date +%Y%m%d-%H%M).md"
63+
fi
64+
65+
echo "▸ surf '$query' → $SURF_HOME/out/$out"
66+
67+
# Docker 사용
68+
command -v docker >/dev/null || { echo "✗ docker 가 필요합니다" >&2; exit 2; }
69+
docker info >/dev/null 2>&1 || { echo "✗ Docker 가 실행 중이 아닙니다" >&2; exit 2; }
70+
71+
export SURF_HOME SURF_REPO="$REPO_DIR" QUERY="$query" SOURCES="$sources" \
72+
LANG="$lang" MAX_PAGES="$max" OUT_FILE="$out"
73+
74+
# 컨테이너 1회 실행 (--rm), 결과는 호스트의 out 디렉터리에 남음
75+
docker compose -f "$REPO_DIR/openclaw-mgr/compose.surf.yml" \
76+
run --rm \
77+
--no-deps \
78+
surf
79+
80+
echo "✔ 완료: $SURF_HOME/out/$out"
81+
[ "${SURF_OPEN:-0}" = "1" ] && open "$SURF_HOME/out/$out" || true

0 commit comments

Comments
 (0)