feat(tunnel): add cloudflare tunnel toggle tool (#34)

* feat(tunnel): add toggle tool and bash script

* feat(tunnel): add cloudflared to Dockerfile

* docs(tunnel): add ADR and handoff

* docs(handoff): set PR number

* docs: update project map + handoff + ADR

---------

Co-authored-by: opencode-agent <agent@slaid098.dev>
This commit is contained in:
Sergey 2026-07-24 02:11:29 +03:00 committed by GitHub
parent 954b5b71af
commit efc0827226
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 4 deletions

View file

@ -21,6 +21,7 @@ REDIS_PASSWORD=your-redis-password
# Cloudflare Tunnel (optional — see separate tunnel repo) # Cloudflare Tunnel (optional — see separate tunnel repo)
CLOUDFLARE_TUNNEL_TOKEN= CLOUDFLARE_TUNNEL_TOKEN=
TUNNEL_DOMAIN=
# Antidetect Browser MCP # Antidetect Browser MCP
# Local: http://localhost:8765/mcp # Local: http://localhost:8765/mcp

41
.opencode/scripts/tunnel.sh Executable file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="/tmp/tunnel.pid"
LOG_FILE="/tmp/tunnel.log"
if [[ -z "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then
echo "❌ CLOUDFLARE_TUNNEL_TOKEN is not set"
exit 1
fi
# Toggle: if PID file exists and process is alive → stop
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill "$PID"
rm -f "$PID_FILE"
echo "stopped"
exit 0
else
rm -f "$PID_FILE"
fi
fi
# Start
nohup cloudflared tunnel run --token "$CLOUDFLARE_TUNNEL_TOKEN" > "$LOG_FILE" 2>&1 &
PID=$!
echo "$PID" > "$PID_FILE"
sleep 2
if ! kill -0 "$PID" 2>/dev/null; then
rm -f "$PID_FILE"
echo "❌ tunnel failed to start — check $LOG_FILE"
exit 1
fi
if [[ -n "${TUNNEL_DOMAIN:-}" ]]; then
echo "started (PID: $PID, domain: $TUNNEL_DOMAIN)"
else
echo "started (PID: $PID)"
fi

19
.opencode/tools/tunnel.ts Normal file
View file

@ -0,0 +1,19 @@
import { spawnSync } from "child_process"
import path from "path"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Toggle cloudflare tunnel. First call starts, second call stops. No arguments needed.",
args: {},
async execute(_args, context) {
const script = path.join(import.meta.dir, "..", "scripts", "tunnel.sh")
const r = spawnSync("bash", [script], {
encoding: "utf-8",
cwd: context.worktree,
})
if (r.status !== 0) {
return `⚠️ tunnel failed (exit ${r.status}): ${r.stderr}`
}
return r.stdout.trim()
},
})

View file

@ -26,6 +26,8 @@ RUN mkdir -p -m 0755 /etc/apt/keyrings \
&& apt-get install -y gh \ && apt-get install -y gh \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

View file

@ -0,0 +1,15 @@
# ADR-013: Cloudflare tunnel toggle tool
## Статус
Accepted
## Контекст
Tunnel skill в старом приватном репо (slaid098/opencode, PR#52/54) содержал приватные данные — IPs, домены, UUIDs. Для публичного репо opencode-config нужен tool без хардкода. Toggle mode (без аргументов) максимально прост для агента: первый вызов = start, второй = stop.
## Решение
TS tool `tunnel.ts` (wrapper) + bash script `tunnel.sh` (toggle logic). Named tunnel через env var `CLOUDFLARE_TUNNEL_TOKEN` (dashboard-managed, без локального config.yml). PID-файл `/tmp/tunnel.pid` отслеживает состояние. `TUNNEL_DOMAIN` — опциональный, только для display в output.
## Альтернативы
- Quick mode (random `*.trycloudflare.com`) — отклонено (random URL меняется при рестарте, не подходит для стабильного доступа)
- localtunnel (npx) — отклонено (лишняя Node.js зависимость, less reliable чем cloudflared)
- SSH localhost.run — отклонено (ограниченный bandwidth, TCP-only)

View file

@ -0,0 +1,26 @@
---
pr_number: 34
title: Tunnel tool with toggle mode
---
# PR: Tunnel tool with toggle mode
## Что сделано
- `.opencode/tools/tunnel.ts` — TS tool wrapper (toggle без args, вызывает `bash .opencode/scripts/tunnel.sh`)
- `.opencode/scripts/tunnel.sh` — bash toggle: PID файл → stop, нет → start named tunnel (`cloudflared tunnel run --token $CLOUDFLARE_TUNNEL_TOKEN`)
- `Dockerfile` — cloudflared install (`curl -L ... -o /usr/local/bin/cloudflared`)
- `.env.example` — добавлен `TUNNEL_DOMAIN=` (опциональный, для display)
- ADR-013 + этот handoff
## Почему
Tunnel skill в старом приватном репо содержал приватные данные (IPs, домены, UUID). Для публичного репо нужен tool без хардкода. Toggle mode (без аргументов) максимально прост для агента — первый вызов = start, второй = stop.
## Pending
- Нет
## Watch out
- `CLOUDFLARE_TUNNEL_TOKEN` должен быть в `.env` (compose env_file) — без него скрипт падает exit 1
- PID-файл `/tmp/tunnel.pid` теряется при рестарте контейнера — `status` скажет not running, нужно перезапустить
- DinD: origin host в Cloudflare dashboard должен быть `docker-dind:<PORT>` (не `localhost:<PORT>`), иначе 502 Bad Gateway
- Tools auto-discovered через @opencode-ai/plugin — НЕ нужно регистрировать в opencode.json
- `cloudflared` в Dockerfile устанавливается из latest release (не pinned version)

View file

@ -41,7 +41,8 @@ opencode-config/
│ ├── tools/ │ ├── tools/
│ │ ├── merge-pr.ts # merge_pr tool wrapper (orchestrator-safe gh pr merge) — PR#30 │ │ ├── merge-pr.ts # merge_pr tool wrapper (orchestrator-safe gh pr merge) — PR#30
│ │ ├── pipeline-status.ts # pipeline_status tool wrapper │ │ ├── pipeline-status.ts # pipeline_status tool wrapper
│ │ └── spec-status.ts # spec_status tool wrapper │ │ ├── spec-status.ts # spec_status tool wrapper
│ │ └── tunnel.ts # Cloudflare tunnel toggle tool (start/stop без args) — PR#34
│ ├── scripts/ │ ├── scripts/
│ │ ├── check-adr-refs.py # ADR cross-reference validator (adr-check.yml) │ │ ├── check-adr-refs.py # ADR cross-reference validator (adr-check.yml)
│ │ ├── check-permissions.py # Permissions validator (permissions-check.yml) │ │ ├── check-permissions.py # Permissions validator (permissions-check.yml)
@ -49,7 +50,8 @@ opencode-config/
│ │ ├── pipeline-status.py # 7-phase oracle (gh PR + CI polling) │ │ ├── pipeline-status.py # 7-phase oracle (gh PR + CI polling)
│ │ ├── scaffold-handoff.sh # Scaffold handoff + ADR stubs │ │ ├── scaffold-handoff.sh # Scaffold handoff + ADR stubs
│ │ ├── setup-memory.sh # opencode-memory bootstrap │ │ ├── setup-memory.sh # opencode-memory bootstrap
│ │ └── spec-status.py # 9-phase spec oracle │ │ ├── spec-status.py # 9-phase spec oracle
│ │ └── tunnel.sh # Cloudflare tunnel toggle bash (named mode via CLOUDFLARE_TUNNEL_TOKEN) — PR#34
│ ├── opencode.json # MCP servers, providers, permissions, agents, plugins │ ├── opencode.json # MCP servers, providers, permissions, agents, plugins
│ ├── package.json # npm deps for tools/*.ts │ ├── package.json # npm deps for tools/*.ts
│ └── .gitignore # Ignores node_modules, etc. │ └── .gitignore # Ignores node_modules, etc.
@ -85,8 +87,8 @@ opencode-config/
├── uv.lock # Locked deps for Python project ├── uv.lock # Locked deps for Python project
├── .pre-commit-config.yaml # ruff + UV hooks ├── .pre-commit-config.yaml # ruff + UV hooks
├── docker-compose.yml # 2 services (dind + opencode), opencode_network, 4 bind mounts — PR#24 ├── docker-compose.yml # 2 services (dind + opencode), opencode_network, 4 bind mounts — PR#24
├── Dockerfile # node:20-slim + uv + gh + chromium + docker.io + opencode-ai + repomix — PR#24 ├── Dockerfile # node:20-slim + uv + gh + chromium + docker.io + opencode-ai + repomix + cloudflared — PR#24, PR#34
├── .env.example # Placeholder-only env template (user copies to .env) — PR#24 ├── .env.example # Placeholder-only env template (user copies to .env) — PR#24, PR#34 (TUNNEL_DOMAIN)
├── app_data/ ├── app_data/
│ ├── workspaces/ # Agent working directory (.gitkeep) │ ├── workspaces/ # Agent working directory (.gitkeep)
│ └── ssh/ # SSH keys, not in git (.gitkeep) │ └── ssh/ # SSH keys, not in git (.gitkeep)