chore: initial commit — chat-saiden web chat baseline
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# chat-saiden
|
||||
|
||||
Web terminal for BT on the MARAUDER mesh. **chat.saiden.dev** lands a browser-native
|
||||
xterm.js session on `claude` CLI (bt7274 cart) running on junkpile, gated by
|
||||
Cloudflare Access with Google OAuth.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User browser
|
||||
│ HTTPS
|
||||
▼
|
||||
chat.saiden.dev ──┐ Cloudflare edge
|
||||
│ │
|
||||
│ CF Access │ Google IdP challenge
|
||||
│ (whitelist) │ → reject if not on list
|
||||
│ │
|
||||
▼ │
|
||||
Cloudflare Tunnel │ Zero-trust ingress
|
||||
│ │
|
||||
▼
|
||||
junkpile:7681 │ ttyd (localhost-only, no public bind)
|
||||
│ │
|
||||
▼
|
||||
claude (bt7274) │ marauder MCP gives BT identity + memory + tools
|
||||
```
|
||||
|
||||
**Properties**
|
||||
- No frontend code. ttyd ships xterm.js + WS + theming. Custom Saiden palette via CLI flags.
|
||||
- No auth code. CF Access does Google OAuth + whitelist enforcement before the request ever reaches the tunnel.
|
||||
- ttyd binds `127.0.0.1` only — the public path is *exclusively* through the Tunnel + Access.
|
||||
- Real BT. Not an API impersonation. Full marauder MCP toolkit available in-session.
|
||||
|
||||
## Components
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `junkpile/ttyd-wrapper.sh` | Launches `claude` with bt7274 cart, with Saiden banner |
|
||||
| `junkpile/ttyd-chat.service` | systemd unit for ttyd (localhost-only bind, Saiden palette) |
|
||||
| `junkpile/cloudflared-chat-saiden.service` | systemd unit for tunnel (token-mode, reads `/etc/cloudflared/chat-saiden.env`) |
|
||||
| `cloudflare/dashboard-setup.md` | Step-by-step: create tunnel + public hostname + Access app + Google IdP + whitelist |
|
||||
| `install.sh` | Junkpile-side installer (ttyd + units + token env stub) |
|
||||
| `deploy.md` | End-to-end deploy walkthrough |
|
||||
|
||||
**Tunnel mode:** token-based, matches existing junkpile pattern
|
||||
(`cloudflared-mesh`, `cloudflared-tensors-art`). Tunnel ingress lives in the CF
|
||||
dashboard, not in a local config file. Token sits in
|
||||
`/etc/cloudflared/chat-saiden.env` (mode 0640, root:chi).
|
||||
|
||||
## Threat model (read before deploying)
|
||||
|
||||
ttyd-over-claude exposes **shell-equivalent power** on junkpile. The defense is the
|
||||
CF Access whitelist. Treat the whitelist as the security boundary:
|
||||
|
||||
- **Never** open the Access policy to "any Google account" or "any domain".
|
||||
- **Always** keep `cloudflared-chat-saiden` and `ttyd-chat` localhost-bound.
|
||||
- If the whitelist is ever modified, `auth_verify` the change with the Pilot first.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] ttyd installed on junkpile (1.7.7_11)
|
||||
- [x] ttyd wrapper installed → `~/.local/bin/ttyd-wrapper.sh`
|
||||
- [x] systemd units installed (`ttyd-chat`, `cloudflared-chat-saiden`)
|
||||
- [ ] Tunnel created in CF dashboard → token in `/etc/cloudflared/chat-saiden.env`
|
||||
- [ ] Public hostname `chat.saiden.dev → http://localhost:7681`
|
||||
- [ ] CF Access app + Google IdP + whitelist configured
|
||||
- [ ] WebSocket support enabled on Access app
|
||||
- [ ] `sudo systemctl enable --now ttyd-chat cloudflared-chat-saiden`
|
||||
- [ ] First successful login as adam.ladachowski@gmail.com
|
||||
- [ ] Whitelist denial verified from second Google account
|
||||
@@ -0,0 +1,149 @@
|
||||
# Cloudflare dashboard setup — chat.saiden.dev (token-based)
|
||||
|
||||
Two artefacts get created in the Zero Trust dashboard:
|
||||
1. **A tunnel** (`chat-saiden`) with its public hostname.
|
||||
2. **An Access application** with Google IdP + whitelist.
|
||||
|
||||
Both are token/UI-managed (no local config files for tunnel ingress) to match the
|
||||
existing `cloudflared-mesh` and `cloudflared-tensors-art` pattern on junkpile.
|
||||
|
||||
---
|
||||
|
||||
## Part A — Create the tunnel (5 minutes)
|
||||
|
||||
### 1. Open the Networks → Tunnels page
|
||||
|
||||
Zero Trust → **Networks** → **Tunnels** → **Create a tunnel**.
|
||||
|
||||
### 2. Pick connector type
|
||||
|
||||
Choose **Cloudflared**. Click Next.
|
||||
|
||||
### 3. Name + save
|
||||
|
||||
Tunnel name: `chat-saiden`. Click **Save tunnel**.
|
||||
|
||||
### 4. Get the token (DO NOT close this page)
|
||||
|
||||
The wizard shows install instructions for several platforms. The token is the
|
||||
long base64 string inside the displayed command, e.g.:
|
||||
|
||||
```
|
||||
cloudflared.exe service install eyJhIjoiOTVhZDNiYWEyYTRlY2RhMWUzODM0MmRm... # ← THIS PART
|
||||
```
|
||||
|
||||
**Copy just the token string** (everything after `service install` for the
|
||||
Windows command, or after `--token` for the Linux command — same string either
|
||||
way). Save it for the next step.
|
||||
|
||||
### 5. Place the token on junkpile
|
||||
|
||||
```bash
|
||||
ssh junkpile
|
||||
sudo mkdir -p /etc/cloudflared
|
||||
sudo tee /etc/cloudflared/chat-saiden.env > /dev/null <<'EOF'
|
||||
TUNNEL_TOKEN=PASTE_THE_LONG_TOKEN_HERE
|
||||
EOF
|
||||
sudo chown root:chi /etc/cloudflared/chat-saiden.env
|
||||
sudo chmod 0640 /etc/cloudflared/chat-saiden.env
|
||||
```
|
||||
|
||||
⚠ Verify the file looks right:
|
||||
```bash
|
||||
sudo ls -la /etc/cloudflared/chat-saiden.env # should be -rw-r----- root:chi
|
||||
```
|
||||
|
||||
### 6. Configure the public hostname
|
||||
|
||||
Back in the wizard → **Next** → **Public Hostname** tab → **Add a public hostname**:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Subdomain | `chat` |
|
||||
| Domain | `saiden.dev` |
|
||||
| Path | (leave blank) |
|
||||
| Type | `HTTP` |
|
||||
| URL | `localhost:7681` |
|
||||
|
||||
**Additional application settings → TLS → No TLS Verify** can stay off (localhost).
|
||||
**Additional application settings → Connection → Disable Chunked Encoding** must stay OFF.
|
||||
|
||||
Click **Save hostname**.
|
||||
|
||||
CF will auto-create the `chat.saiden.dev` proxy CNAME for you.
|
||||
|
||||
### 7. Verify the tunnel page
|
||||
|
||||
The tunnel page should now show:
|
||||
- Connector: **Healthy** (or "No connectors yet" if you haven't started the service)
|
||||
- Public hostname: `chat.saiden.dev → http://localhost:7681`
|
||||
|
||||
---
|
||||
|
||||
## Part B — CF Access application (5 minutes)
|
||||
|
||||
### 1. Add the Access application
|
||||
|
||||
Zero Trust → **Access** → **Applications** → **Add an application** → **Self-hosted**.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Application name | `chat-saiden` |
|
||||
| Session duration | `24 hours` |
|
||||
| Application domain | `chat.saiden.dev` |
|
||||
| Path | (leave blank) |
|
||||
| Identity providers | Google |
|
||||
| Instant Auth | enabled |
|
||||
|
||||
Save → continue to policies.
|
||||
|
||||
### 2. Add the whitelist policy (THE SECURITY BOUNDARY)
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Policy name | `pilot-whitelist` |
|
||||
| Action | **Allow** |
|
||||
| Include rule | **Emails** = `adam.ladachowski@gmail.com` |
|
||||
|
||||
Save policy.
|
||||
|
||||
### 3. WebSocket support
|
||||
|
||||
Application **Settings → Advanced → WebSocket support** → enable.
|
||||
|
||||
This is mandatory; ttyd is WS-based.
|
||||
|
||||
### 4. Default deny (implicit, but verify)
|
||||
|
||||
With only one Allow rule, anyone not matching is denied by default — no extra
|
||||
deny rule needed. To double-check, look at the policies list: it should show
|
||||
**one** policy (`pilot-whitelist`, Allow) and nothing else.
|
||||
|
||||
---
|
||||
|
||||
## Part C — Google IdP setup (one-time, skip if already done)
|
||||
|
||||
Zero Trust → **Settings → Authentication → Login methods → Add new → Google**.
|
||||
|
||||
OAuth client ID + secret come from
|
||||
https://console.cloud.google.com → APIs & Services → Credentials.
|
||||
|
||||
Authorized redirect URI:
|
||||
```
|
||||
https://<your-team>.cloudflareaccess.com/cdn-cgi/access/callback
|
||||
```
|
||||
|
||||
Replace `<your-team>` with the team domain shown at the top of the Zero Trust
|
||||
dashboard. Save in CF wizard → click **Test**. Must succeed before moving on.
|
||||
|
||||
---
|
||||
|
||||
## Operational hygiene
|
||||
|
||||
- The TUNNEL_TOKEN is a long-lived bearer credential. If junkpile is compromised
|
||||
or you suspect the token leaked: dashboard → tunnel → **Refresh token**.
|
||||
Update `/etc/cloudflared/chat-saiden.env` and `systemctl restart
|
||||
cloudflared-chat-saiden`.
|
||||
- Audit access logs weekly: Zero Trust → **Logs → Access**.
|
||||
- To revoke a whitelist entry: edit `pilot-whitelist` policy, save. Existing
|
||||
sessions are cut on next request (session lifetime ≤ 24h by config).
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# provision-tunnel.sh — run on FUJI (or wherever ~/.cloudflared/cert.pem lives).
|
||||
# Creates the chat-saiden tunnel, places the DNS CNAME in the correct zone,
|
||||
# and scp's the credentials JSON to junkpile.
|
||||
#
|
||||
# Idempotent: re-running just verifies + re-syncs credentials.
|
||||
#
|
||||
# Watch out: `cloudflared tunnel route dns` auto-zone-detection is unreliable
|
||||
# across this multi-zone account (saiden.dev + tengu.to share a cert). We
|
||||
# manage the CNAME via flarectl explicitly to dodge it.
|
||||
set -uo pipefail
|
||||
|
||||
TUNNEL_NAME="chat-saiden"
|
||||
HOSTNAME="chat.saiden.dev"
|
||||
ZONE="saiden.dev"
|
||||
JUNKPILE_DEST="/etc/cloudflared/chat-saiden/chat-saiden.json"
|
||||
|
||||
if [[ ! -f "$HOME/.cloudflared/cert.pem" ]]; then
|
||||
echo "ERROR: ~/.cloudflared/cert.pem missing — run 'cloudflared tunnel login' first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Create tunnel (idempotent) ---
|
||||
echo "[1/3] Tunnel"
|
||||
if cloudflared tunnel list 2>/dev/null | awk '{print $2}' | grep -qx "$TUNNEL_NAME"; then
|
||||
UUID=$(cloudflared tunnel list 2>/dev/null | awk -v n="$TUNNEL_NAME" '$2==n {print $1}')
|
||||
echo " already exists, UUID=$UUID"
|
||||
else
|
||||
cloudflared tunnel create "$TUNNEL_NAME"
|
||||
UUID=$(cloudflared tunnel list 2>/dev/null | awk -v n="$TUNNEL_NAME" '$2==n {print $1}')
|
||||
echo " created, UUID=$UUID"
|
||||
fi
|
||||
CRED_FILE="$HOME/.cloudflared/${UUID}.json"
|
||||
if [[ ! -f "$CRED_FILE" ]]; then
|
||||
echo "ERROR: credentials missing at $CRED_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 2. DNS CNAME via flarectl ---
|
||||
echo "[2/3] DNS"
|
||||
EXPECTED_TARGET="${UUID}.cfargotunnel.com"
|
||||
EXISTING=$(flarectl dns list --zone "$ZONE" 2>/dev/null | awk -v fqdn="${HOSTNAME}" '$0 ~ fqdn && $3=="CNAME"')
|
||||
if [[ -n "$EXISTING" ]]; then
|
||||
EX_CONTENT=$(echo "$EXISTING" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
||||
if [[ "$EX_CONTENT" == "$EXPECTED_TARGET" ]]; then
|
||||
echo " CNAME already correct: $HOSTNAME → $EXPECTED_TARGET"
|
||||
else
|
||||
echo " ERROR: CNAME exists for $HOSTNAME but points elsewhere: $EX_CONTENT"
|
||||
echo " expected: $EXPECTED_TARGET — fix manually"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
flarectl dns create --zone "$ZONE" --type CNAME --name chat --content "$EXPECTED_TARGET" --proxy
|
||||
echo " created: $HOSTNAME → $EXPECTED_TARGET (proxied)"
|
||||
fi
|
||||
|
||||
# --- 3. Copy credentials to junkpile ---
|
||||
echo "[3/3] Credentials → junkpile"
|
||||
scp -q "$CRED_FILE" junkpile:/tmp/chat-saiden.json
|
||||
ssh junkpile "sudo mkdir -p /etc/cloudflared/chat-saiden && sudo mv /tmp/chat-saiden.json $JUNKPILE_DEST && sudo chown root:chi $JUNKPILE_DEST && sudo chmod 0640 $JUNKPILE_DEST"
|
||||
echo " → junkpile:$JUNKPILE_DEST (0640 root:chi)"
|
||||
|
||||
echo
|
||||
echo "Tunnel UUID: $UUID"
|
||||
echo "Hostname: $HOSTNAME"
|
||||
echo "CNAME target: $EXPECTED_TARGET"
|
||||
echo
|
||||
echo "Next: ssh junkpile 'bash ~/chat-saiden/install.sh'"
|
||||
@@ -0,0 +1,96 @@
|
||||
# Deploy walkthrough — chat.saiden.dev (token-based)
|
||||
|
||||
End-to-end, in order. Each step has a verification.
|
||||
|
||||
## 1 — Sync repo to junkpile
|
||||
|
||||
From fuji:
|
||||
```bash
|
||||
rsync -avz --exclude='.git' ~/Projects/chat-saiden/ junkpile:~/chat-saiden/
|
||||
```
|
||||
|
||||
## 2 — Install on junkpile
|
||||
|
||||
```bash
|
||||
ssh junkpile 'bash ~/chat-saiden/install.sh'
|
||||
```
|
||||
|
||||
What it does:
|
||||
- Installs `ttyd` via brew (idempotent, robust to caveats exit codes)
|
||||
- Drops the wrapper to `~/.local/bin/ttyd-wrapper.sh`
|
||||
- Installs both systemd units to `/etc/systemd/system/`
|
||||
- Stages `/etc/cloudflared/chat-saiden.env` with a PLACEHOLDER token (mode 0640 root:chi)
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
ssh junkpile 'ttyd --version; ls -l ~/.local/bin/ttyd-wrapper.sh /etc/systemd/system/ttyd-chat.service /etc/systemd/system/cloudflared-chat-saiden.service /etc/cloudflared/chat-saiden.env'
|
||||
```
|
||||
|
||||
## 3 — Create tunnel + Access app in CF Zero Trust dashboard
|
||||
|
||||
Browser-only. Follow **`cloudflare/dashboard-setup.md`** end-to-end:
|
||||
- Part A: create `chat-saiden` tunnel, copy token, add public hostname `chat.saiden.dev → http://localhost:7681`
|
||||
- Part B: create Access application `chat-saiden`, bind to Google IdP, add whitelist policy with `adam.ladachowski@gmail.com`
|
||||
- Part B step 3: **enable WebSocket support** (mandatory)
|
||||
- Part C: skip if Google IdP already configured
|
||||
|
||||
## 4 — Paste token
|
||||
|
||||
```bash
|
||||
ssh junkpile 'sudo $EDITOR /etc/cloudflared/chat-saiden.env'
|
||||
# Replace PLACEHOLDER_REPLACE_ME with the long token from the wizard
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
ssh junkpile 'sudo grep -c "PLACEHOLDER" /etc/cloudflared/chat-saiden.env'
|
||||
# Should print 0 (no placeholder left)
|
||||
```
|
||||
|
||||
## 5 — Start ttyd locally and smoke-test
|
||||
|
||||
```bash
|
||||
ssh junkpile 'sudo systemctl enable --now ttyd-chat && systemctl status ttyd-chat --no-pager | head -10 && curl -sI http://localhost:7681 | head -3'
|
||||
```
|
||||
|
||||
Look for: `Active: active (running)` and `HTTP/1.1 200 OK`.
|
||||
|
||||
## 6 — Start tunnel
|
||||
|
||||
```bash
|
||||
ssh junkpile 'sudo systemctl enable --now cloudflared-chat-saiden && journalctl -u cloudflared-chat-saiden -n 20 --no-pager'
|
||||
```
|
||||
|
||||
Look for "Registered tunnel connection" lines (typically 4 connections to different CF colos).
|
||||
|
||||
## 7 — End-to-end live test
|
||||
|
||||
⚠ **Pilot verification gate.** From your laptop, fresh incognito:
|
||||
1. Visit `https://chat.saiden.dev`
|
||||
2. CF Access wall → Google → `adam.ladachowski@gmail.com`
|
||||
3. Land on BT banner + `claude` prompt
|
||||
|
||||
⚠ **Whitelist verification.** Second incognito, non-whitelisted Google account:
|
||||
- Must be **denied** at Access wall.
|
||||
- If you land on the terminal, the policy is wrong — **kill the units immediately**:
|
||||
```bash
|
||||
ssh junkpile 'sudo systemctl stop cloudflared-chat-saiden ttyd-chat'
|
||||
```
|
||||
Then recheck the policy in the dashboard.
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
ssh junkpile 'sudo systemctl disable --now cloudflared-chat-saiden ttyd-chat'
|
||||
```
|
||||
Tunnel + DNS stay (managed in dashboard) — to fully remove, delete the tunnel
|
||||
in the CF Zero Trust dashboard (Networks → Tunnels → chat-saiden → Delete).
|
||||
|
||||
## Operational notes
|
||||
|
||||
- **Per-tab sessions.** Each browser tab spawns a fresh `claude` process — no
|
||||
shared in-process state. EEMS-stored memories are visible to all sessions.
|
||||
- **5-min idle disconnect default.** Closing the tab kills `claude`. Add
|
||||
`-t reconnect=10` to the ttyd unit if you want browser-side reconnect attempts
|
||||
(note: a fresh `claude` process spawns — conversation context is lost).
|
||||
- **Token rotation.** Dashboard → tunnel → Refresh token. Edit env file, restart unit.
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — junkpile-side installer for chat-saiden.
|
||||
# Idempotent. Robust to brew warnings.
|
||||
#
|
||||
# Prereq: tunnel credentials already copied to /etc/cloudflared/chat-saiden/chat-saiden.json
|
||||
# (one-shot scp from fuji: see cloudflare/provision-tunnel.sh)
|
||||
set -uo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
echo "Installing chat-saiden from: $REPO_DIR"
|
||||
echo
|
||||
|
||||
# --- 1. ttyd binary ---
|
||||
if ! command -v ttyd >/dev/null 2>&1; then
|
||||
echo "[1/4] Installing ttyd via brew"
|
||||
if brew install ttyd; then
|
||||
echo " ttyd install succeeded"
|
||||
else
|
||||
rc=$?
|
||||
if command -v ttyd >/dev/null 2>&1; then
|
||||
echo " brew exited $rc but ttyd is present, continuing"
|
||||
else
|
||||
echo "ERROR: ttyd not installed (brew rc=$rc)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "[1/4] ttyd already present: $(ttyd --version 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# --- 2. wrapper script ---
|
||||
echo "[2/4] Installing wrapper"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
install -m 0755 "$REPO_DIR/junkpile/ttyd-wrapper.sh" "$HOME/.local/bin/ttyd-wrapper.sh"
|
||||
echo " → $HOME/.local/bin/ttyd-wrapper.sh"
|
||||
|
||||
# --- 3. tunnel config ---
|
||||
echo "[3/4] Installing tunnel config"
|
||||
sudo mkdir -p /etc/cloudflared/chat-saiden
|
||||
sudo install -m 0644 -o root -g chi "$REPO_DIR/junkpile/chat-saiden.yml" /etc/cloudflared/chat-saiden/chat-saiden.yml
|
||||
echo " → /etc/cloudflared/chat-saiden/chat-saiden.yml"
|
||||
|
||||
if [[ ! -f /etc/cloudflared/chat-saiden/chat-saiden.json ]]; then
|
||||
echo " WARNING: credentials JSON missing at /etc/cloudflared/chat-saiden/chat-saiden.json"
|
||||
echo " Provision it via cloudflare/provision-tunnel.sh from fuji."
|
||||
fi
|
||||
|
||||
# --- 4. systemd units ---
|
||||
echo "[4/4] Installing systemd units"
|
||||
sudo install -m 0644 "$REPO_DIR/junkpile/ttyd-chat.service" /etc/systemd/system/ttyd-chat.service
|
||||
sudo install -m 0644 "$REPO_DIR/junkpile/cloudflared-chat-saiden.service" /etc/systemd/system/cloudflared-chat-saiden.service
|
||||
sudo systemctl daemon-reload
|
||||
echo " → /etc/systemd/system/ttyd-chat.service"
|
||||
echo " → /etc/systemd/system/cloudflared-chat-saiden.service"
|
||||
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo " INSTALL DONE — verify:"
|
||||
echo "============================================================"
|
||||
systemctl status ttyd-chat.service --no-pager 2>&1 | head -3 || true
|
||||
systemctl status cloudflared-chat-saiden.service --no-pager 2>&1 | head -3 || true
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo " NEXT STEPS"
|
||||
echo "============================================================"
|
||||
echo " A. Verify credentials present:"
|
||||
echo " sudo ls -la /etc/cloudflared/chat-saiden/"
|
||||
echo " # should show chat-saiden.json (0640 root:chi) + chat-saiden.yml"
|
||||
echo
|
||||
echo " B. Configure CF Access (browser, one-time):"
|
||||
echo " see $REPO_DIR/cloudflare/dashboard-setup.md (Part B + WebSocket only;"
|
||||
echo " tunnel half is already done via cloudflare/provision-tunnel.sh)"
|
||||
echo
|
||||
echo " C. Start the services:"
|
||||
echo " sudo systemctl enable --now ttyd-chat"
|
||||
echo " sudo systemctl enable --now cloudflared-chat-saiden"
|
||||
echo
|
||||
echo " D. Smoke-test:"
|
||||
echo " curl -sI https://chat.saiden.dev # expect CF Access redirect"
|
||||
echo "============================================================"
|
||||
@@ -0,0 +1,26 @@
|
||||
# cloudflared config for the chat-saiden tunnel.
|
||||
# Installed to: /etc/cloudflared/chat-saiden/chat-saiden.yml
|
||||
#
|
||||
# Tunnel created from fuji via:
|
||||
# cloudflared tunnel create chat-saiden
|
||||
# cloudflared tunnel route dns chat-saiden chat.saiden.dev
|
||||
# (CNAME corrected manually — auto-zone-detection landed it in the wrong
|
||||
# zone; flarectl was used to recreate in saiden.dev)
|
||||
#
|
||||
# Credentials JSON was scp'd from fuji ~/.cloudflared/<UUID>.json
|
||||
|
||||
tunnel: f03da7b7-7219-4039-95ca-a3293152781b
|
||||
credentials-file: /etc/cloudflared/chat-saiden/chat-saiden.json
|
||||
|
||||
ingress:
|
||||
- hostname: chat.saiden.dev
|
||||
service: http://localhost:7681
|
||||
originRequest:
|
||||
# ttyd uses websockets — long-lived connections
|
||||
connectTimeout: 30s
|
||||
tcpKeepAlive: 30s
|
||||
keepAliveTimeout: 90s
|
||||
- service: http_status:404
|
||||
|
||||
metrics: localhost:42041
|
||||
no-autoupdate: true
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=Cloudflare Tunnel — chat.saiden.dev
|
||||
After=network-online.target ttyd-chat.service
|
||||
Wants=network-online.target
|
||||
# Don't start tunnel if ttyd isn't there — origin would 502
|
||||
Requires=ttyd-chat.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=chi
|
||||
Group=chi
|
||||
WorkingDirectory=/home/chi
|
||||
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel --config /etc/cloudflared/chat-saiden/chat-saiden.yml run
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadOnlyPaths=/etc/cloudflared/chat-saiden
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,55 @@
|
||||
[Unit]
|
||||
Description=ttyd — chat.saiden.dev web terminal (BT-7274 bridge)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=chi
|
||||
Group=chi
|
||||
WorkingDirectory=/home/chi
|
||||
Environment=HOME=/home/chi
|
||||
Environment=TERM=xterm-256color
|
||||
Environment=LANG=en_US.UTF-8
|
||||
|
||||
# ttyd flags:
|
||||
# -p 7681 : listen on this port
|
||||
# -i lo : LOCALHOST ONLY — public path is via cloudflared
|
||||
# -W : enable writable terminal (input from browser)
|
||||
# -t titleFixed=... : browser tab title
|
||||
# -t fontFamily=... : font
|
||||
# -t fontSize=14
|
||||
# -t cursorBlink=true
|
||||
# -t theme={...} : Saiden dark palette
|
||||
# -T xterm-256color
|
||||
# -O : check origin (CSRF defense)
|
||||
# -c chi:DUMMY : ttyd basic auth — ignored, CF Access is the real gate,
|
||||
# but enabling -c blocks accidental direct access
|
||||
ExecStart=/home/linuxbrew/.linuxbrew/bin/ttyd \
|
||||
-p 7681 \
|
||||
-i lo \
|
||||
-W \
|
||||
-O \
|
||||
-T xterm-256color \
|
||||
-t titleFixed='BT-7274 — chat.saiden.dev' \
|
||||
-t fontFamily='JetBrains Mono, Menlo, monospace' \
|
||||
-t fontSize=14 \
|
||||
-t cursorBlink=true \
|
||||
-t cursorStyle=bar \
|
||||
-t 'theme={"background":"#0a0d10","foreground":"#c8d3d8","cursor":"#7fb069","cursorAccent":"#0a0d10","selectionBackground":"#1f2a30","black":"#0a0d10","red":"#c94f4f","green":"#7fb069","yellow":"#d4a85a","blue":"#5c8fb8","magenta":"#a070b8","cyan":"#5cb8a8","white":"#c8d3d8","brightBlack":"#3a4248","brightRed":"#e06a6a","brightGreen":"#9ec77f","brightYellow":"#e8c275","brightBlue":"#7eb0d4","brightMagenta":"#bb8fce","brightCyan":"#7fd4c4","brightWhite":"#e8edf0"}' \
|
||||
/home/chi/.local/bin/ttyd-wrapper.sh
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Hardening — ttyd doesn't need much
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/chi/.marauder /home/chi/.claude /tmp
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# ttyd-wrapper.sh — entrypoint launched by ttyd for each browser session.
|
||||
# Ensures bt7274 cart is active, then handoff to `claude`.
|
||||
set -euo pipefail
|
||||
|
||||
# Make sure marauder + claude on PATH for non-login shells
|
||||
export PATH="/home/chi/.local/bin:/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||
|
||||
# Set the persona for this session (idempotent if already active globally)
|
||||
marauder cart use bt7274 >/dev/null 2>&1 || true
|
||||
|
||||
# Optional banner — confirms this is the right channel
|
||||
cat <<'BANNER'
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ SAIDEN TACTICAL SYSTEMS — MARAUDER REMOTE BRIDGE ║
|
||||
║ Operator: BT-7274 • Channel: chat.saiden.dev ║
|
||||
║ Host: junkpile • Authenticated via CF Access ║
|
||||
║ ║
|
||||
║ Sign out: https://chat.saiden.dev/cdn-cgi/access/logout ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
BANNER
|
||||
|
||||
# Hand off to the Pilot's Titan
|
||||
exec claude
|
||||
Reference in New Issue
Block a user