Skip to content
Open
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
27 changes: 25 additions & 2 deletions skills/brainstorming/scripts/start-server.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Start the brainstorm server and output connection info
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--insecure-remote] [--foreground] [--background]
#
# Starts server on a random high port, outputs JSON with URL.
# Each session gets its own directory to avoid conflicts.
Expand All @@ -9,8 +9,10 @@
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
# instead of /tmp. Files persist after server stops.
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
# Use 0.0.0.0 in remote/containerized environments.
# Non-loopback binds require --insecure-remote.
# --url-host <host> Hostname shown in returned URL JSON.
# --insecure-remote Acknowledge that a non-loopback bind uses plaintext HTTP.
# Prefer an SSH tunnel or a TLS-terminating reverse proxy.
# --idle-timeout-minutes <n> Shut down after n minutes idle (default 240 = 4h).
# --open Auto-open the browser on the first screen (use only
# after the user approves the visual companion).
Expand All @@ -23,6 +25,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR=""
FOREGROUND="false"
FORCE_BACKGROUND="false"
INSECURE_REMOTE="false"
BIND_HOST="127.0.0.1"
URL_HOST=""
IDLE_TIMEOUT_MINUTES=""
Expand All @@ -40,6 +43,10 @@ while [[ $# -gt 0 ]]; do
URL_HOST="$2"
shift 2
;;
--insecure-remote)
INSECURE_REMOTE="true"
shift
;;
--idle-timeout-minutes)
IDLE_TIMEOUT_MINUTES="$2"
shift 2
Expand All @@ -63,6 +70,22 @@ while [[ $# -gt 0 ]]; do
esac
done

is_loopback_bind_host() {
case "$1" in
localhost|127.*|::1|'[::1]') return 0 ;;
*) return 1 ;;
esac
}

# The session key is a bearer credential carried in the companion URL and
# WebSocket connection. This server intentionally speaks HTTP only, so exposing
# it beyond loopback is unsafe unless the operator has arranged a secure
# transport (for example SSH port forwarding or TLS termination).
if ! is_loopback_bind_host "$BIND_HOST" && [[ "$INSECURE_REMOTE" != "true" ]]; then
echo '{"error": "Refusing non-loopback HTTP bind without --insecure-remote. Use SSH port forwarding or a TLS-terminating reverse proxy."}'
exit 2
fi

if [[ -z "$URL_HOST" ]]; then
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
URL_HOST="localhost"
Expand Down
18 changes: 13 additions & 5 deletions skills/brainstorming/visual-companion.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,24 @@ scripts/start-server.sh --project-dir /path/to/project --open --foreground

**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.

If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
If the URL is unreachable from your browser because the agent runs remotely or
inside a container, keep the companion bound to loopback and forward its port
through SSH. The session URL contains a bearer key, so the server must not be
exposed through plaintext HTTP on a LAN or the Internet.

```bash
# On the remote machine: start normally and note the returned port and URL.
scripts/start-server.sh \
--project-dir /path/to/project \
--host 0.0.0.0 \
--url-host localhost
--project-dir /path/to/project

# On your local machine: replace both ports with the returned remote port.
ssh -L <local-port>:127.0.0.1:<remote-port> user@remote-host
```

Use `--url-host` to control what hostname is printed in the returned URL JSON.
Open the returned URL after replacing its host and port with
`localhost:<local-port>`. If direct non-loopback exposure is unavoidable, pass
`--insecure-remote` explicitly and place the server behind TLS; this mode is
not safe over plaintext HTTP.

## The Loop

Expand Down
20 changes: 20 additions & 0 deletions tests/brainstorm-server/start-server.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ else
"expected foreground node path, got: $captured"
fi

echo ""
echo "--- start-server.sh remote bind safety ---"

captured=$(bash "$START_SCRIPT" --project-dir "$TEST_DIR/project" --host 0.0.0.0 2>&1 || true)
if echo "$captured" | grep -q 'Refusing non-loopback HTTP bind without --insecure-remote'; then
pass "rejects non-loopback HTTP binds without explicit acknowledgement"
else
fail "rejects non-loopback HTTP binds without explicit acknowledgement" \
"expected remote bind safety error, got: $captured"
fi

captured=$(PATH="$TEST_DIR/fake-bin:$PATH" \
bash "$START_SCRIPT" --project-dir "$TEST_DIR/project" --host 0.0.0.0 --insecure-remote --foreground 2>&1 || true)
if echo "$captured" | grep -q 'FOREGROUND_MODE=true'; then
pass "allows explicitly acknowledged non-loopback binds"
else
fail "allows explicitly acknowledged non-loopback binds" \
"expected fake node to run after acknowledgement, got: $captured"
fi

echo ""
echo "--- Results: $passed passed, $failed failed ---"
if [[ $failed -gt 0 ]]; then
Expand Down