diff --git a/.gitignore b/.gitignore index d731e7c..1acbb16 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,10 @@ nohup.out .mmdb .ipdb -dist/ \ No newline at end of file +dist/ + +# Local agent / assistant notes — not for public repo +CLAUDE.md +.claude/ +.agents/ +docs/superpowers/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 77c5dc9..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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. diff --git a/docs/superpowers/specs/2026-04-23-install-script-design.md b/docs/superpowers/specs/2026-04-23-install-script-design.md deleted file mode 100644 index b2d64f3..0000000 --- a/docs/superpowers/specs/2026-04-23-install-script-design.md +++ /dev/null @@ -1,319 +0,0 @@ -# install.sh — one-line installer/updater/uninstaller - -**Status:** approved **Date:** 2026-04-23 **Owner:** flashduty-runner - -## Goal - -Replace the manual 4-line-per-platform install block in the README with a single command that handles install, update, and uninstall for the flashduty-runner binary across Linux and macOS. - -## Non-goals - -- Windows installer (`install.ps1`) — separate future project. -- macOS launchd integration — docs hint only. -- Non-systemd Linux init systems (OpenRC, runit) — binary-only install is acceptable. -- Attestation / cosign signature verification — `checksums.txt` over HTTPS is the trust anchor. -- Distribution via apt / yum / brew — out of scope. - -## User-facing surface - -**Canonical URL:** `https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh` - -```bash -# Install or update (interactive token prompt if needed) -curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh | sudo bash - -# Non-interactive install (for automation) -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/env) -curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh \ - | sudo bash -s -- --uninstall - -# Uninstall + wipe config and working dir -curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh \ - | sudo bash -s -- --purge - -# Binary-only (no systemd unit even if systemd is present) -curl -fsSL https://raw.githubusercontent.com/flashcatcloud/flashduty-runner/main/install.sh \ - | sudo bash -s -- --no-service -``` - -### Environment variables (installer-local) - -Installer-local vars are unprefixed. The installer writes prefixed `FLASHDUTY_RUNNER_*` vars into `/etc/flashduty-runner/env`, which are the names the running binary reads at runtime. - -Because `sudo` scrubs env by default, these must be passed on the sudo command line (`sudo TOKEN=… bash`), not exported in the caller's shell. - -| Var | Purpose | Default | -|---|---|---| -| `TOKEN` | Written as `FLASHDUTY_RUNNER_TOKEN=…` into env file on first install | prompt via `/dev/tty` if interactive, else exit 6 | -| `VERSION` | Pin a release tag (e.g. `v0.0.5`) | latest release | -| `URL` | Written as `FLASHDUTY_RUNNER_URL=…` into env file | `wss://api.flashcat.cloud/safari/environment/ws` | -| `INSTALL_DIR` | Binary install path | `/usr/local/bin` | -| `REPO` | GitHub owner/repo override (for forks / testing) | `flashcatcloud/flashduty-runner` | - -### Flags - -- `--uninstall` — remove binary and systemd unit, keep `/etc/flashduty-runner/`. -- `--purge` — imply `--uninstall`, also remove `/etc/flashduty-runner/`, `/var/lib/flashduty-runner/`, and the `flashduty` user. -- `--no-service` — skip systemd unit and service user; install binary only. -- `--token ` — flag equivalent of `TOKEN` env var. -- `--version ` — flag equivalent of `VERSION` env var. -- `--help` — print usage and exit 0. - -### Exit codes - -| Code | Meaning | -|---|---| -| 0 | Success or no-op | -| 1 | Generic error | -| 2 | Unsupported platform (OS or arch) | -| 3 | Must be run as root | -| 4 | Checksum mismatch | -| 5 | Download failure | -| 6 | Missing token when service setup is required | - -## Internal structure - -Single POSIX `sh`-compatible file at repo root: `install.sh`. ~250 lines. No bashisms (runs under `dash`, `ash`, `bash`). - -``` -install.sh -├── parse_args() -├── detect_platform() # OS=linux|darwin, ARCH_GR=x86_64|arm64 -├── detect_init() # INIT=systemd|none (skipped on darwin) -├── resolve_version() # $VERSION or latest via GitHub -├── download_and_verify() # tarball + checksums.txt, sha256 -├── install_binary() # atomic mv into place, keep .bak -├── ensure_user() # useradd -r flashduty (idempotent) -├── ensure_workdir() # /var/lib/flashduty-runner/workspace -├── ensure_token() # TOKEN / --token / /dev/tty prompt -├── write_env_file() # /etc/flashduty-runner/env (0600) -├── install_systemd_unit() -├── enable_and_start() -├── uninstall() -└── main() # dispatcher -``` - -### State on disk - -| Path | Owner/Mode | Purpose | -|---|---|---| -| `${INSTALL_DIR}/flashduty-runner` | root:root 0755 | Binary | -| `${INSTALL_DIR}/flashduty-runner.bak` | root:root 0755 | Previous binary (after updates) | -| `/etc/flashduty-runner/env` | root:root 0600 | Runtime env (token, URL, log level) | -| `/etc/systemd/system/flashduty-runner.service` | root:root 0644 | Unit file | -| `/var/lib/flashduty-runner/workspace` | flashduty:flashduty 0750 | Runtime workspace / sandbox root | -| `flashduty` system user | `-r -s /usr/sbin/nologin` | Runs the service | - -(Env file is `0600 root:root` because systemd reads `EnvironmentFile=` itself as PID 1 / root before spawning the service; the `flashduty` service user never reads the file directly.) - -### Logging - -All output on stderr via `info()` / `warn()` / `err()` helpers. Colored only when stderr is a TTY. stdout reserved. - -### Idempotency - -Every step checks current state first: -- User/group: skip if exists. -- Env file: never overwritten on re-install/update — load-bearing contract. -- Systemd unit: rewritten only if content differs (compared via sha). -- Binary: replaced only if version differs from resolved target. - -## Flows - -### Install (fresh machine) - -1. `parse_args` → mode=install. -2. Require root → exit 3 otherwise. -3. `detect_platform` → exit 2 on unsupported OS/arch. -4. `detect_init` → INIT=systemd|none. -5. `resolve_version` → TAG. -6. `download_and_verify`: - - `mkdir` tmp dir, `trap` cleanup. - - `curl -fsSL` tarball + `checksums.txt`. - - `grep -F` exact filename in checksums, compute `sha256sum`/`shasum -a 256`, compare → exit 4 on mismatch. - - `tar -xzf` → binary. -7. `install_binary` → `mv` to `${INSTALL_DIR}/flashduty-runner` (0755). -8. If `--no-service` or OS=darwin → print next-step hint, exit 0. -9. `ensure_user` → `useradd -r -s /usr/sbin/nologin flashduty` (idempotent). -10. `ensure_workdir` → mkdir, chown, chmod. -11. `ensure_token` → use `$TOKEN`, `--token`, or prompt via `/dev/tty` (if interactive) → exit 6 if still empty. -12. `write_env_file` → only if missing; preserve existing content on update. -13. If INIT=systemd: - - `install_systemd_unit` → render from template, write, `daemon-reload`. - - `systemctl enable --now flashduty-runner`. - - Print: *"Installed flashduty-runner vX.Y.Z. Check status: `systemctl status flashduty-runner`"*. -14. Else (INIT=none, Linux without systemd): - - Print: *"Systemd not detected. Binary installed at ${INSTALL_DIR}/flashduty-runner; start manually with `flashduty-runner run` or wire into your init system."* - -### Update (re-run, binary exists) - -1. Steps 1–5 same. -2. Run `${INSTALL_DIR}/flashduty-runner version` to read current version. -3. If current == TAG → log *"Already at vX.Y.Z"*, exit 0. -4. `download_and_verify` (same as install). -5. `systemctl stop` (if systemd and unit exists). -6. `mv ${INSTALL_DIR}/flashduty-runner ${INSTALL_DIR}/flashduty-runner.bak`. -7. `mv` new binary into place. -8. Env file untouched. Systemd unit rewritten only if content differs. -9. `systemctl start` (if service was previously enabled). -10. Verify active after 2 seconds; on failure log warning with `.bak` rollback hint (no auto-rollback). -11. Print *"Updated vA → vB"*. - -### Uninstall - -1. `parse_args` → mode=uninstall, `PURGE=true|false`. -2. Require root. -3. `systemctl stop flashduty-runner` — ignore errors. -4. `systemctl disable flashduty-runner` — ignore errors. -5. `rm -f /etc/systemd/system/flashduty-runner.service`. -6. `systemctl daemon-reload` — ignore errors. -7. `rm -f ${INSTALL_DIR}/flashduty-runner ${INSTALL_DIR}/flashduty-runner.bak`. -8. If `--purge`: - - `rm -rf /etc/flashduty-runner` - - `rm -rf /var/lib/flashduty-runner` - - `userdel flashduty` (ignore errors) -9. Print summary of what was removed. - -Every uninstall step logs and continues; a half-broken install should still clean up as much as possible. - -## Systemd unit - -`/etc/systemd/system/flashduty-runner.service`: - -```ini -[Unit] -Description=Flashduty Runner -Documentation=https://github.com/flashcatcloud/flashduty-runner -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=flashduty -Group=flashduty -EnvironmentFile=/etc/flashduty-runner/env -ExecStart=__INSTALL_DIR__/flashduty-runner run -Restart=always -RestartSec=5 -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true -ReadWritePaths=/var/lib/flashduty-runner - -[Install] -WantedBy=multi-user.target -``` - -`__INSTALL_DIR__` is substituted at install time. - -Differences from the README's hand-written example, all intentional: -- `network-online.target` — the runner opens a WSS connection at start; `network.target` is satisfied too early. -- Hardening stanza — defense-in-depth. -- `ReadWritePaths=/var/lib/flashduty-runner` — required because `ProtectSystem=strict` otherwise blocks workspace writes. - -## Env file template - -`/etc/flashduty-runner/env` — mode 0600, owner `root:root` (systemd reads `EnvironmentFile=` as PID 1, so the service user never needs read access): - -```bash -# Managed by install.sh on first install. Edit freely; will not be overwritten on updates. -FLASHDUTY_RUNNER_TOKEN=wnt_xxx -FLASHDUTY_RUNNER_URL=wss://api.flashcat.cloud/safari/environment/ws -FLASHDUTY_RUNNER_WORKSPACE=/var/lib/flashduty-runner/workspace -FLASHDUTY_RUNNER_LOG_LEVEL=info -``` - -Written only on first install (when the file does not exist). Updates must never overwrite it — users who tune permissions, URL, or log level must be able to re-run the installer without losing settings. - -## Download URL convention - -``` -https://github.com/${REPO}/releases/download/${TAG}/flashduty-runner_${OS_TITLE}_${ARCH_GR}.tar.gz -https://github.com/${REPO}/releases/download/${TAG}/checksums.txt -``` - -Where: -- `OS_TITLE` ∈ `Linux`, `Darwin` (title-case from `uname -s`). -- `ARCH_GR` ∈ `x86_64` (amd64), `arm64` — per goreleaser's asset naming. - -### Version resolution - -1. If `VERSION` / `--version` set → use verbatim. -2. Else `curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/${REPO}/releases/latest` — parse the redirect target. No GitHub API calls, no rate limits, no token needed. - -## Platform matrix - -| Condition | Behavior | -|---|---| -| Linux + systemd | Full flow: binary + user + env + unit + `enable --now` | -| Linux + no systemd (Alpine, minimal container) | Binary + user + env, skip unit, print run hint | -| Darwin | Binary only, print run hint. No user/env/unit. | -| Other OS | Exit 2 | -| Non-supported arch | Exit 2 | -| Not root | Exit 3 | -| `--no-service` on Linux | Binary only, skip user/env/unit | - -## Failure modes - -| Failure | Detection | Recovery | -|---|---|---| -| Missing `curl` | `command -v curl` | Exit 1 with install hint | -| Missing `tar` | `command -v tar` | Exit 1 with install hint | -| Missing `sha256sum` and `shasum` | combined check | Exit 1 with install hint | -| Download failure | `curl -fsSL` exit code | Exit 5, system untouched | -| Checksum mismatch | computed vs expected | Exit 4, tmp dir cleaned, no binary swap | -| Missing token, non-TTY stdin | `[ -t 0 ]` + `[ -r /dev/tty ]` | Exit 6 with hint | -| `systemctl enable --now` failure | non-zero exit | Warn; binary installed; point at `journalctl` and `.bak` | -| Update: new binary fails to start | `systemctl is-active` after 2s | Same warn; no auto-rollback | -| Crash mid-flow | `trap 'rm -rf "$TMPDIR"' EXIT` | Tmp cleaned; binary not swapped if verification hadn't completed | -| Concurrent install | `flock` on `/var/lock/flashduty-runner-install.lock` | Second invocation blocks or fails fast | - -## Security - -- `curl -fsSL` only. No fallback to http. -- Mandatory SHA256 verification against `checksums.txt` from the same release. -- Atomic binary replacement: tmp write → verify → `mv` on the same filesystem. -- Token never logged, never written outside `/etc/flashduty-runner/env`. -- Token prompt reads from `/dev/tty` with `stty -echo`, never from stdin (which is the pipe). -- No `eval`, no unquoted expansions of user-adjacent strings. `grep -F` for the exact asset filename in `checksums.txt`. -- `checksums.txt` itself is trusted because it comes from GitHub releases over HTTPS — same trust level as `rustup`, `bun`, `ollama`. **Trust-on-first-use caveat:** an attacker controlling the releases page (e.g. compromised maintainer token) could replace both binary and checksums atomically. Users needing stronger guarantees should pin `VERSION` and compare the binary sha against a known value. Attestation verification is out of scope (requires `gh`/`cosign` on host). -- Uninstall preserves `/etc/flashduty-runner/` by default — accidental uninstall doesn't destroy config. - -## Testing plan - -### Manual matrix (pre-merge) - -- Ubuntu 22.04 (systemd): install → update (from pinned older version) → uninstall → `--purge`. -- Alpine 3.19 (no systemd): install → binary-only path prints hint. -- macOS (darwin/arm64): install → binary-only path prints hint. -- `--no-service` on Ubuntu: binary only, no user/env/unit created. -- `VERSION=v0.0.5` on Ubuntu: verify pinning works. -- Re-run with current version: verify no-op path. -- Non-TTY piped install without `TOKEN`: verify exit 6 with clear message. -- Corrupted tarball (simulate by editing checksums in test fork): verify exit 4, no binary swap. - -### Automated CI - -Add `.github/workflows/install-sh.yml`: -- `shellcheck install.sh` — lint. -- `sh -n install.sh` — parse check under POSIX shell. -- Docker smoke test in `ubuntu:22.04`: - - `apt-get install -y curl ca-certificates tar` - - Run script pointed at the real release (`REPO=flashcatcloud/flashduty-runner`). - - Skip `enable --now` (via `--no-service`) so no real token / WSS endpoint is needed. - - Assert `flashduty-runner version` output matches `VERSION`. - - Run `--uninstall` and assert binary is gone. - -## Rollout - -1. Land `install.sh` + CI workflow on `main`. -2. Update `README.md` and `README_zh.md` "Quick Start → Binary Installation" sections to lead with the one-liner, keeping the manual block below for users who want it. -3. No goreleaser changes — the script lives in the repo, not in release assets (matches the decision in Q7).