Skip to content
Closed
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
43 changes: 43 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Privacy & Telemetry

Dual-Graph respects your privacy. All network calls beyond package installation
are **opt-in** and **off by default**.

## Environment Variables

| Variable | Default | Effect |
|---|---|---|
| `DG_TELEMETRY=1` | off | Enables error reporting and one-time feedback form |
| `DG_AUTO_UPDATE=1` | off | Enables launcher self-update from GitHub/R2 |

You can also pass `--no-update` to `dg`/`dgc` to suppress auto-update for a
single invocation.

## What each flag controls

### `DG_TELEMETRY=1`
- Error reports POST to a Google Apps Script webhook (script step + error message)
- One-time feedback rating POST (asked once, 2+ days after install)
- No machine IDs or hardware identifiers are included

### `DG_AUTO_UPDATE=1`
- Version check: fetches `version.txt` from GitHub/R2 (~100 bytes)
- If newer version exists: downloads `dual_graph_launch.sh` and upgrades `graperoot` via pip
- Re-execs the launcher after update

## What is NOT collected
- No machine IDs or hardware identifiers (collection removed entirely)
- No filesystem contents beyond the project graph
- No API keys or credentials
- No identity.json is created or read

## Compiled MCP server (`graperoot` package)
The `mcp_graph_server` component is distributed as a compiled Python package
(`graperoot` on PyPI). It currently contains a `_ping_license_server()` heartbeat
that contacts a server every 15 minutes with machine identity. This heartbeat is
being removed by the package maintainer in a forthcoming release.

## Network calls that always happen (not gated by flags)
- `pip install graperoot` and other dependencies — standard PyPI package installation
- Localhost-only HTTP between the launcher and the local MCP server
- `bootstrap.pypa.io/get-pip.py` — only during venv creation fallback if pip is missing
142 changes: 51 additions & 91 deletions bin/dual_graph_launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

set -Eeuo pipefail

# Opt-in flags (off by default — see PRIVACY.md)
# DG_TELEMETRY=1 → enable error reporting and feedback form
# DG_AUTO_UPDATE=1 → enable launcher self-update
# --no-update → suppress auto-update for this invocation
_DG_NO_UPDATE=0
for _arg in "$@"; do
[[ "$_arg" == "--no-update" ]] && _DG_NO_UPDATE=1
done

ASSISTANT="${1:-}"
if [[ "$ASSISTANT" != "codex" && "$ASSISTANT" != "claude" ]]; then
echo "Usage: $0 <codex|claude> [project_path] [prompt]" >&2
Expand Down Expand Up @@ -56,63 +65,12 @@ _platform_name() {
}

_machine_id() {
python3 - "$SCRIPT_DIR/identity.json" <<'PY' 2>/dev/null || echo "unknown"
import json
import os
import platform
import subprocess
import sys
import uuid
from pathlib import Path

identity_path = Path(sys.argv[1])

def get_machine_id() -> str:
sys_name = platform.system()
try:
if sys_name == "Darwin":
out = subprocess.check_output(
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
stderr=subprocess.DEVNULL,
timeout=3,
).decode()
for line in out.splitlines():
if "IOPlatformUUID" in line:
return line.split('"')[3]
elif sys_name == "Linux":
for p in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
try:
val = Path(p).read_text().strip()
if val:
return val
except OSError:
pass
except Exception:
pass
return str(uuid.getnode())

try:
if identity_path.exists():
data = json.loads(identity_path.read_text(encoding="utf-8"))
mid = data.get("machine_id", "").strip()
if mid:
print(mid)
raise SystemExit(0)
mid = get_machine_id()
identity_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"machine_id": mid,
"platform": platform.system().lower(),
"tool": "launcher-auto",
}
identity_path.write_text(json.dumps(payload), encoding="utf-8")
print(mid)
except Exception:
print("unknown")
PY
# Machine identity collection removed — see PRIVACY.md
echo "disabled"
}

_send_cli_error() {
[[ "${DG_TELEMETRY:-}" == "1" ]] || return 0
local step="$1"
local message="$2"
local machine_id platform payload
Expand Down Expand Up @@ -223,44 +181,46 @@ else
POLICY_MARKER="dgc-policy-v10"
fi

# ── Self-update ────────────────────────────────────────────────────────────────
_R2="https://pub-18426978d5a14bf4a60ddedd7d5b6dab.r2.dev"
_BASE_URL="https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main"
# ── Self-update (opt-in: DG_AUTO_UPDATE=1, suppressed by --no-update) ─────────
_LOCAL_VER="$(cat "$SCRIPT_DIR/version.txt" 2>/dev/null || echo "0")"
_REMOTE_VER="$(
curl -sf --max-time 3 "$_BASE_URL/bin/version.txt" 2>/dev/null \
|| curl -sf --max-time 3 "$_R2/version.txt" 2>/dev/null \
|| echo ""
)"
_NOTICE_FILE="$SCRIPT_DIR/last_update_notice.txt"

if [[ -n "$_REMOTE_VER" ]] && _version_gt "$_REMOTE_VER" "$_LOCAL_VER"; then
_LAST_NOTICE_VER="$(cat "$_NOTICE_FILE" 2>/dev/null || echo "")"
if [[ "$_LAST_NOTICE_VER" != "$_REMOTE_VER" ]]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "[$TOOL_LABEL] New version ($_LOCAL_VER -> $_REMOTE_VER) available. To refresh launcher files run:"
echo "[$TOOL_LABEL] curl -sSL https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main/install.sh | bash"
else
echo "[$TOOL_LABEL] New version ($_LOCAL_VER -> $_REMOTE_VER) available. To refresh launcher files run:"
echo "[$TOOL_LABEL] curl -sSL https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main/install.sh | bash"
if [[ "${DG_AUTO_UPDATE:-}" == "1" ]] && [[ "$_DG_NO_UPDATE" != "1" ]]; then
_R2="https://pub-18426978d5a14bf4a60ddedd7d5b6dab.r2.dev"
_BASE_URL="https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main"
_REMOTE_VER="$(
curl -sf --max-time 3 "$_BASE_URL/bin/version.txt" 2>/dev/null \
|| curl -sf --max-time 3 "$_R2/version.txt" 2>/dev/null \
|| echo ""
)"
_NOTICE_FILE="$SCRIPT_DIR/last_update_notice.txt"

if [[ -n "$_REMOTE_VER" ]] && _version_gt "$_REMOTE_VER" "$_LOCAL_VER"; then
_LAST_NOTICE_VER="$(cat "$_NOTICE_FILE" 2>/dev/null || echo "")"
if [[ "$_LAST_NOTICE_VER" != "$_REMOTE_VER" ]]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "[$TOOL_LABEL] New version ($_LOCAL_VER -> $_REMOTE_VER) available. To refresh launcher files run:"
echo "[$TOOL_LABEL] curl -sSL https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main/install.sh | bash"
else
echo "[$TOOL_LABEL] New version ($_LOCAL_VER -> $_REMOTE_VER) available. To refresh launcher files run:"
echo "[$TOOL_LABEL] curl -sSL https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main/install.sh | bash"
fi
echo "$_REMOTE_VER" > "$_NOTICE_FILE" 2>/dev/null || true
fi
echo "$_REMOTE_VER" > "$_NOTICE_FILE" 2>/dev/null || true
fi
echo "[$TOOL_LABEL] Update available ($_LOCAL_VER → $_REMOTE_VER) — updating..."
curl -fsSL "$_BASE_URL/bin/dual_graph_launch.sh" -o "$SCRIPT_DIR/dual_graph_launch.sh" \
|| curl -fsSL "$_R2/dual_graph_launch.sh" -o "$SCRIPT_DIR/dual_graph_launch.sh"
chmod +x "$SCRIPT_DIR/dual_graph_launch.sh"
echo "$_REMOTE_VER" > "$SCRIPT_DIR/version.txt"
# Upgrade graperoot so venv gets latest mcp_graph_server + compiled modules
if [[ -x "$VENV_BIN/pip" ]]; then
"$VENV_BIN/pip" install graperoot --upgrade --quiet 2>/dev/null || true
echo "[$TOOL_LABEL] Update available ($_LOCAL_VER → $_REMOTE_VER) — updating..."
curl -fsSL "$_BASE_URL/bin/dual_graph_launch.sh" -o "$SCRIPT_DIR/dual_graph_launch.sh" \
|| curl -fsSL "$_R2/dual_graph_launch.sh" -o "$SCRIPT_DIR/dual_graph_launch.sh"
chmod +x "$SCRIPT_DIR/dual_graph_launch.sh"
echo "$_REMOTE_VER" > "$SCRIPT_DIR/version.txt"
# Upgrade graperoot so venv gets latest mcp_graph_server + compiled modules
if [[ -x "$VENV_BIN/pip" ]]; then
"$VENV_BIN/pip" install graperoot --upgrade --quiet 2>/dev/null || true
fi
echo "[$TOOL_LABEL] Updated to $_REMOTE_VER. Restarting..."
EXEC_ARGS=("$SCRIPT_DIR/dual_graph_launch.sh" "$ASSISTANT" "$PROJECT")
[[ -n "$PROMPT" ]] && EXEC_ARGS+=("$PROMPT")
exec "${EXEC_ARGS[@]}"
elif [[ -n "$_REMOTE_VER" && "$_REMOTE_VER" != "$_LOCAL_VER" ]]; then
echo "[$TOOL_LABEL] Local version ($_LOCAL_VER) is newer than remote ($_REMOTE_VER); skipping downgrade."
fi
echo "[$TOOL_LABEL] Updated to $_REMOTE_VER. Restarting..."
EXEC_ARGS=("$SCRIPT_DIR/dual_graph_launch.sh" "$ASSISTANT" "$PROJECT")
[[ -n "$PROMPT" ]] && EXEC_ARGS+=("$PROMPT")
exec "${EXEC_ARGS[@]}"
elif [[ -n "$_REMOTE_VER" && "$_REMOTE_VER" != "$_LOCAL_VER" ]]; then
echo "[$TOOL_LABEL] Local version ($_LOCAL_VER) is newer than remote ($_REMOTE_VER); skipping downgrade."
fi
# ──────────────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -1161,10 +1121,10 @@ else
# ───────────────────────────────────────────────────────────────────────────
fi

# ── One-time feedback form ─────────────────────────────────────────────────────
# ── One-time feedback form (opt-in: DG_TELEMETRY=1) ───────────────────────────
_FEEDBACK_DONE="$SCRIPT_DIR/feedback_done"
_INSTALL_DATE_FILE="$SCRIPT_DIR/install_date.txt"
if [[ ! -f "$_FEEDBACK_DONE" ]] && [[ -t 0 ]]; then
if [[ "${DG_TELEMETRY:-}" == "1" ]] && [[ ! -f "$_FEEDBACK_DONE" ]] && [[ -t 0 ]]; then
_SHOW_FEEDBACK=1
if [[ -f "$_INSTALL_DATE_FILE" ]]; then
_INSTALL_DATE="$(cat "$_INSTALL_DATE_FILE")"
Expand Down
59 changes: 5 additions & 54 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

set -euo pipefail

LICENSE_SERVER="https://dual-graph-license-production.up.railway.app"
INSTALL_DIR="$HOME/.dual-graph"
VENV="$INSTALL_DIR/venv"
mkdir -p "$INSTALL_DIR"
Expand Down Expand Up @@ -229,66 +228,18 @@ echo ""

echo "[install] Using $($PYTHON --version)"

# ── License check ─────────────────────────────────────────────────────────────
echo "[install] Checking license..."

LICENSE_KEY="${DG_LICENSE_KEY:-}"
PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')"
if [[ "$PLATFORM" == "darwin" ]]; then
MACHINE_ID="$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformUUID/{print $4}')"
elif [[ -f /etc/machine-id ]]; then
MACHINE_ID="$(cat /etc/machine-id)"
elif [[ -f /var/lib/dbus/machine-id ]]; then
MACHINE_ID="$(cat /var/lib/dbus/machine-id)"
fi
if [[ -z "${MACHINE_ID:-}" ]]; then
MACHINE_ID=$("$PYTHON" -c "import uuid; print(uuid.getnode())" 2>/dev/null || echo "unknown")
fi

VALIDATE_RESP=$(curl -sf -X POST "$LICENSE_SERVER/validate" \
-H "Content-Type: application/json" \
-d "{\"key\":\"$LICENSE_KEY\",\"machine_id\":\"$MACHINE_ID\",\"platform\":\"$PLATFORM\",\"tool\":\"install-sh\"}" 2>/dev/null || echo '{"ok":false,"error":"server unreachable"}')

OK=$(echo "$VALIDATE_RESP" | "$PYTHON" -c "import sys,json; print(json.load(sys.stdin).get('ok','false'))" 2>/dev/null || echo "false")

if [[ "$OK" == "True" || "$OK" == "true" ]]; then
echo "[install] License validated."
else
ERR=$(echo "$VALIDATE_RESP" | "$PYTHON" -c "import sys,json; print(json.load(sys.stdin).get('error','unknown'))" 2>/dev/null || echo "unknown")
echo "[install] License check returned: $ERR"
echo "[install] Continuing installation..."
fi

# Save identity so MCP server can ping on each startup (tracks real usage)
"$PYTHON" -c "
import json, os
d = {'machine_id': '$MACHINE_ID', 'platform': '$PLATFORM', 'tool': 'install-sh'}
open(os.path.expanduser('$HOME/.dual-graph/identity.json'), 'w').write(json.dumps(d))
" 2>/dev/null || true

# Save install date for one-time feedback prompt
# Machine identity collection and license validation removed — see PRIVACY.md
# Save install date for one-time feedback prompt (shown only if DG_TELEMETRY=1)
date +%Y-%m-%d > "$INSTALL_DIR/install_date.txt" 2>/dev/null || true

# ── Get file URLs from license server response ────────────────────────────────
get_url() {
echo "$VALIDATE_RESP" | "$PYTHON" -c "
import sys, json
d = json.load(sys.stdin)
files = d.get('files', {})
print(files.get('$1', ''))
" 2>/dev/null || echo ""
}

URL_LAUNCH=$(get_url dual_graph_launch)

# Fallback to Cloudflare R2 if server returned empty URLs
R2="https://pub-18426978d5a14bf4a60ddedd7d5b6dab.r2.dev"
BASE_URL="https://raw.githubusercontent.com/kunal12203/Codex-CLI-Compact/main"
[[ -z "$URL_LAUNCH" ]] && URL_LAUNCH="$R2/dual_graph_launch.sh"

# ── Download core engine ──────────────────────────────────────────────────────
echo "[install] Downloading core engine..."
curl -fsSL "$URL_LAUNCH" -o "$INSTALL_DIR/dual_graph_launch.sh" && chmod +x "$INSTALL_DIR/dual_graph_launch.sh"
curl -fsSL "$BASE_URL/bin/dual_graph_launch.sh" -o "$INSTALL_DIR/dual_graph_launch.sh" \
|| curl -fsSL "$R2/dual_graph_launch.sh" -o "$INSTALL_DIR/dual_graph_launch.sh"
chmod +x "$INSTALL_DIR/dual_graph_launch.sh"
curl -sf "$BASE_URL/bin/version.txt" -o "$INSTALL_DIR/version.txt" 2>/dev/null \
|| curl -sf "$R2/version.txt" -o "$INSTALL_DIR/version.txt" 2>/dev/null \
|| true
Expand Down