Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/workflows/install-sh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: install.sh

on:
push:
branches:
- main
paths:
- install.sh
- .github/workflows/install-sh.yml
pull_request:
paths:
- install.sh
- .github/workflows/install-sh.yml

permissions:
contents: read

jobs:
lint:
name: shellcheck + parse
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: shellcheck
run: shellcheck -s sh install.sh
- name: sh parse
run: sh -n install.sh
- name: bash parse
run: bash -n install.sh

smoke:
name: smoke test (ubuntu)
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v6

- name: run in ubuntu:22.04 container
run: |
docker run --rm -v "$PWD":/w:ro ubuntu:22.04 bash -c '
set -eu
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl tar ca-certificates >/dev/null

echo "::group::--help"
bash /w/install.sh --help | head
echo "::endgroup::"

echo "::group::install --no-service @ v0.0.5"
bash /w/install.sh --no-service --version v0.0.5
test -x /usr/local/bin/flashduty-runner
/usr/local/bin/flashduty-runner version | grep -q "0.0.5"
echo "::endgroup::"

echo "::group::no-op re-run"
bash /w/install.sh --no-service --version v0.0.5 2>&1 | tee /tmp/out
grep -q "Already at v0.0.5" /tmp/out
echo "::endgroup::"

echo "::group::full install with TOKEN env var"
bash /w/install.sh --uninstall >/dev/null
TOKEN="wnt_ci_test" bash /w/install.sh --version v0.0.5
test -f /etc/flashduty-runner/env
grep -q "FLASHDUTY_RUNNER_TOKEN=wnt_ci_test" /etc/flashduty-runner/env
id flashduty
test -d /var/lib/flashduty-runner/workspace
echo "::endgroup::"

echo "::group::env file preserved across update"
echo "# CI marker" >> /etc/flashduty-runner/env
sum_before=$(sha256sum /etc/flashduty-runner/env | awk "{print \$1}")
bash /w/install.sh --version v0.0.5 >/dev/null
sum_after=$(sha256sum /etc/flashduty-runner/env | awk "{print \$1}")
[ "$sum_before" = "$sum_after" ]
echo "::endgroup::"

echo "::group::uninstall keeps config"
bash /w/install.sh --uninstall
test ! -e /usr/local/bin/flashduty-runner
test -f /etc/flashduty-runner/env
echo "::endgroup::"

echo "::group::purge"
bash /w/install.sh --purge
test ! -d /etc/flashduty-runner
test ! -d /var/lib/flashduty-runner
! id flashduty 2>/dev/null
echo "::endgroup::"

echo "::group::non-tty without TOKEN exits 6"
set +e
bash /w/install.sh --version v0.0.5 </dev/null >/tmp/err 2>&1
rc=$?
set -e
[ "$rc" = "6" ]
grep -q "Token is required" /tmp/err
echo "::endgroup::"

echo "::group::nonexistent version exits 5"
set +e
bash /w/install.sh --no-service --version v99.99.99 >/tmp/err2 2>&1
rc=$?
set -e
[ "$rc" = "5" ]
grep -q "Failed to download" /tmp/err2
echo "::endgroup::"

echo ""
echo "ALL CHECKS PASSED"
'
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# CLAUDE.md

**Flashduty Runner** — lightweight agent that connects over WebSocket (TLS) to the fc-safari AI-SRE platform, receives task requests, executes them on the host, and streams results back. Used by end-users to grant Safari access to their servers.

Not a web service — no HTTP API, no pgy registration.

## Repo-specific

| Field | Value |
|---|---|
| Language | Go (Cobra CLI) |
| Default upstream | `wss://api.flashcat.cloud/safari/worknode/ws` (override with `--url`) |
| Auth | token `wnt_…` from Safari (env `FLASHDUTY_RUNNER_TOKEN` or `--token`) |
| Build | `make build` / `make build-all` (linux+darwin × amd64+arm64) |
| Test / lint / fmt | `make test` / `make lint` / `make fmt` |
| Install tools | `make tools` |
| Docker / install target | `make install` |

## Architecture

| Dir | Role |
|---|---|
| `cmd/` | CLI — `run`, `version` |
| `ws/` | WebSocket client — reconnect, heartbeat |
| `workspace/` | Sandbox for file ops; symlink-escape protected |
| `permission/` | Glob-based command whitelist/blacklist |
| `protocol/` | Message types (`task.request`, `task.output`, `task.result`, `mcp.call`, `mcp.result`, heartbeat) |
| `mcp/` | MCP protocol layer for tool calls routed from Safari |

## Permission modes

Controlled via flags / YAML in `/etc/flashduty-runner/`:

- **Strict** (default) — whitelist-only.
- **Trust** — allow everything, block only catastrophic patterns (e.g. `rm -rf /`).
- **Read-only** — `cat` / `head` / `ls` / `grep` / `ps` / `df` / …

Last-match-wins glob ordering. Treat the permission layer as a security boundary — never bypass it to "make a test pass".

## Environment variables

| Var | Purpose |
|---|---|
| `FLASHDUTY_RUNNER_TOKEN` | Required. Auth token issued by Safari. |
| `FLASHDUTY_RUNNER_URL` | Override upstream WebSocket URL |
| `FLASHDUTY_RUNNER_WORKSPACE` | Sandbox root |
| `FLASHDUTY_RUNNER_LOG_LEVEL` | `debug` / `info` / `warn` / `error` |

## Relationship with fc-safari

Every change to the protocol (new message types, auth handshake, capability negotiation) needs matching changes in fc-safari's worknode handler. Search `fc-safari` for `worknode` / `protocol` usage before modifying.

## Shared doc

`@~/.claude/flashcat-dev.md` covers Go env + code style. DB / pgy sections do not apply.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,28 @@ permission:

## Quick Start

### Binary Installation
### One-line Install / Update (Linux + macOS)

```bash
# Install or update (prompts for token if not already set)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash

# Non-interactive (pass token on the sudo line)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo TOKEN=wnt_xxx bash

# Pin a specific version
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo VERSION=v0.0.5 bash

# Uninstall (keeps /etc/flashduty-runner/ config)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash -s -- --uninstall

# Uninstall and wipe everything (binary, config, workspace, service user)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash -s -- --purge
```

On Linux with systemd the script also creates a `flashduty` service user, writes `/etc/flashduty-runner/env`, installs a hardened unit, and runs `systemctl enable --now`. On macOS and non-systemd Linux it installs the binary only. Run with `--help` for all flags.

### Manual Binary Installation

```bash
# Linux (amd64)
Expand Down
23 changes: 22 additions & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,28 @@ permission:

## 快速开始

### 二进制安装
### 一键安装 / 升级(Linux + macOS)

```bash
# 安装或升级(未配置 token 时会交互式提示输入)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash

# 非交互式(在 sudo 行传入 token)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo TOKEN=wnt_xxx bash

# 指定版本
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo VERSION=v0.0.5 bash

# 卸载(保留 /etc/flashduty-runner/ 配置)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash -s -- --uninstall

# 彻底卸载(二进制、配置、工作区、服务用户一并删除)
curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash -s -- --purge
```

在带 systemd 的 Linux 上,脚本会创建 `flashduty` 系统用户、写入 `/etc/flashduty-runner/env`、安装加固过的 systemd 单元并执行 `systemctl enable --now`。macOS 和无 systemd 的 Linux 仅安装二进制。使用 `--help` 查看全部参数。

### 手动二进制安装

```bash
# Linux (amd64)
Expand Down
27 changes: 22 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ var (

// Command line flags
var (
flagToken string
flagURL string
flagWorkspace string
flagLogLevel string
flagToken string
flagURL string
flagWorkspace string
flagLogLevel string
flagMaxAttempts int
)

// Default values
Expand Down Expand Up @@ -89,6 +90,7 @@ Environment variables:
cmd.Flags().StringVar(&flagURL, "url", "", "WebSocket endpoint URL (env: FLASHDUTY_RUNNER_URL)")
cmd.Flags().StringVar(&flagWorkspace, "workspace", "", "Workspace root directory (env: FLASHDUTY_RUNNER_WORKSPACE)")
cmd.Flags().StringVar(&flagLogLevel, "log-level", "", "Log level: debug, info, warn, error (env: FLASHDUTY_RUNNER_LOG_LEVEL)")
cmd.Flags().IntVar(&flagMaxAttempts, "max-attempts", -1, "Max reconnect attempts (0=unlimited, default=30, env: FLASHDUTY_RUNNER_MAX_ATTEMPTS)")

return cmd
}
Expand All @@ -111,6 +113,7 @@ type Config struct {
URL string
WorkspaceRoot string
LogLevel string
MaxAttempts int
}

func loadConfig() (*Config, error) {
Expand Down Expand Up @@ -156,6 +159,19 @@ func loadConfig() (*Config, error) {
cfg.LogLevel = defaultLogLevel
}

// Max attempts: flag > env > default (30)
// -1 means flag wasn't set, so check env; 0 = unlimited
cfg.MaxAttempts = flagMaxAttempts
if cfg.MaxAttempts == -1 {
if envVal := os.Getenv("FLASHDUTY_RUNNER_MAX_ATTEMPTS"); envVal != "" {
if v, err := fmt.Sscanf(envVal, "%d", &cfg.MaxAttempts); v != 1 || err != nil {
return nil, fmt.Errorf("invalid FLASHDUTY_RUNNER_MAX_ATTEMPTS: %s", envVal)
}
} else {
cfg.MaxAttempts = ws.DefaultMaxReconnectAttempts
}
}

return cfg, nil
}

Expand All @@ -172,6 +188,7 @@ func runRunner() error {
slog.Info("starting flashduty-runner",
"version", Version,
"workspace", cfg.WorkspaceRoot,
"max_attempts", cfg.MaxAttempts,
)

checker := permission.NewChecker(map[string]string{"*": "allow"})
Expand All @@ -190,7 +207,7 @@ func runRunner() error {
handler := ws.NewHandler(wspace)

// Create WebSocket client
client := ws.NewClient(cfg.Token, cfg.URL, cfg.WorkspaceRoot, handler.Handle, Version)
client := ws.NewClient(cfg.Token, cfg.URL, cfg.WorkspaceRoot, handler.Handle, Version, cfg.MaxAttempts)
handler.SetClient(client)

// Setup signal handling
Expand Down
Loading
Loading