Compare commits
1 Commits
fix/535
...
fix/671-pl
| Author | SHA1 | Date | |
|---|---|---|---|
| 27f2752528 |
@@ -1,96 +0,0 @@
|
||||
# Bezalel Tailscale Bootstrap
|
||||
|
||||
Refs #535
|
||||
|
||||
This is the repo-side operator packet for installing Tailscale on the Bezalel VPS and verifying the internal network path for federation work.
|
||||
|
||||
Important truth:
|
||||
- issue #535 names `104.131.15.18`
|
||||
- older Bezalel control-plane docs also mention `159.203.146.185`
|
||||
- the current source of truth in this repo is `ansible/inventory/hosts.ini`, which currently resolves `bezalel` to `67.205.155.108`
|
||||
|
||||
Because of that drift, `scripts/bezalel_tailscale_bootstrap.py` now resolves the target host from `ansible/inventory/hosts.ini` by default instead of trusting a stale hardcoded IP.
|
||||
|
||||
## What the script does
|
||||
|
||||
`python3 scripts/bezalel_tailscale_bootstrap.py`
|
||||
|
||||
Safe by default:
|
||||
- builds the remote bootstrap script
|
||||
- writes it locally to `/tmp/bezalel_tailscale_bootstrap.sh`
|
||||
- prints the SSH command needed to run it
|
||||
- does **not** touch the VPS unless `--apply` is passed
|
||||
|
||||
When applied, the remote script does all of the issue’s repo-side bootstrap steps:
|
||||
- installs Tailscale
|
||||
- runs `tailscale up --ssh --hostname bezalel`
|
||||
- appends the provided Mac SSH public key to `~/.ssh/authorized_keys`
|
||||
- prints `tailscale status --json`
|
||||
- pings the expected peer targets:
|
||||
- Mac: `100.124.176.28`
|
||||
- Ezra: `100.126.61.75`
|
||||
|
||||
## Required secrets / inputs
|
||||
|
||||
- Tailscale auth key
|
||||
- Mac SSH public key
|
||||
|
||||
Provide them either directly or through files:
|
||||
- `--auth-key` or `--auth-key-file`
|
||||
- `--ssh-public-key` or `--ssh-public-key-file`
|
||||
|
||||
## Dry-run example
|
||||
|
||||
```bash
|
||||
python3 scripts/bezalel_tailscale_bootstrap.py \
|
||||
--auth-key-file ~/.config/tailscale/auth_key \
|
||||
--ssh-public-key-file ~/.ssh/id_ed25519.pub \
|
||||
--json
|
||||
```
|
||||
|
||||
This prints:
|
||||
- resolved host
|
||||
- host source (`inventory:<path>` when pulled from `ansible/inventory/hosts.ini`)
|
||||
- local script path
|
||||
- SSH command to execute
|
||||
- peer targets
|
||||
|
||||
## Apply example
|
||||
|
||||
```bash
|
||||
python3 scripts/bezalel_tailscale_bootstrap.py \
|
||||
--auth-key-file ~/.config/tailscale/auth_key \
|
||||
--ssh-public-key-file ~/.ssh/id_ed25519.pub \
|
||||
--apply \
|
||||
--json
|
||||
```
|
||||
|
||||
## Verifying success after apply
|
||||
|
||||
The script now parses the remote stdout into structured verification data:
|
||||
- `verification.tailscale.self.tailscale_ips`
|
||||
- `verification.tailscale.self.dns_name`
|
||||
- `verification.peers`
|
||||
- `verification.ping_ok`
|
||||
|
||||
A successful run should show:
|
||||
- at least one Bezalel Tailscale IP under `tailscale_ips`
|
||||
- `ping_ok.mac = 100.124.176.28`
|
||||
- `ping_ok.ezra = 100.126.61.75`
|
||||
|
||||
## Expected remote install commands
|
||||
|
||||
```bash
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
tailscale up --ssh --hostname bezalel
|
||||
install -d -m 700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
|
||||
tailscale status --json
|
||||
```
|
||||
|
||||
## Why this PR does not claim live completion
|
||||
|
||||
This repo can safely ship the bootstrap script, host resolution logic, structured proof parsing, and operator packet.
|
||||
It cannot honestly claim that Bezalel was actually joined to the tailnet unless a human/operator runs the script with a real auth key and real SSH access to the VPS.
|
||||
|
||||
That means the correct PR language for #535 is advancement, not pretend closure.
|
||||
@@ -14,7 +14,6 @@ Quick-reference index for common operational tasks across the Timmy Foundation i
|
||||
| Agent scorecard | fleet-ops | `python3 scripts/agent_scorecard.py` |
|
||||
| View fleet manifest | fleet-ops | `cat manifest.yaml` |
|
||||
| Run nightly codebase genome pass | timmy-home | `python3 scripts/codebase_genome_nightly.py --dry-run` |
|
||||
| Prepare Bezalel Tailscale bootstrap | timmy-home | `python3 scripts/bezalel_tailscale_bootstrap.py --auth-key-file <path> --ssh-public-key-file <path> --json` |
|
||||
|
||||
## the-nexus (Frontend + Brain)
|
||||
|
||||
|
||||
148
genomes/the-playground/GENOME.md
Normal file
148
genomes/the-playground/GENOME.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# GENOME.md — The Playground (Timmy_Foundation/the-playground)
|
||||
|
||||
> Codebase Genome v1.0 | Generated 2026-04-16 | Repo 11/16
|
||||
|
||||
## Project Overview
|
||||
|
||||
**The Sovereign Playground** is a browser-based creative platform for art, music, and interactive experiences. Pure Canvas + Web Audio API + vanilla JS. Zero dependencies. No login. No signup. Open the page and build something beautiful.
|
||||
|
||||
**Core principle:** A man at 3am needs something that works RIGHT NOW, with no friction, no gate, no permission.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Entry Point"
|
||||
HTML[index.html] --> PLAYGROUND[playground.js]
|
||||
end
|
||||
|
||||
subgraph "Engine Layer"
|
||||
PLAYGROUND --> AUDIO[audio-engine.js]
|
||||
PLAYGROUND --> VISUAL[visual-engine.js]
|
||||
end
|
||||
|
||||
subgraph "Modes"
|
||||
PLAYGROUND --> MODES[mode-manager.js]
|
||||
MODES --> CONSTELLATION[constellation.js]
|
||||
end
|
||||
|
||||
subgraph "UI Panels"
|
||||
PLAYGROUND --> SOUND_PANEL[sound-panel.js]
|
||||
PLAYGROUND --> GALLERY_PANEL[gallery-panel.js]
|
||||
end
|
||||
|
||||
subgraph "Gallery & Export"
|
||||
PLAYGROUND --> GALLERY[gallery.js]
|
||||
PLAYGROUND --> DOWNLOAD[download.js]
|
||||
DOWNLOAD --> WAV[wav-encoder.js]
|
||||
end
|
||||
|
||||
subgraph "Utilities"
|
||||
PLAYGROUND --> EVENTS[events.js]
|
||||
PLAYGROUND --> STATE[state.js]
|
||||
PLAYGROUND --> UTILS[utils.js]
|
||||
end
|
||||
|
||||
subgraph "Styling"
|
||||
HTML --> CSS[design-system.css]
|
||||
end
|
||||
```
|
||||
|
||||
## The Three Pillars
|
||||
|
||||
| Pillar | Status | Description |
|
||||
|--------|--------|-------------|
|
||||
| CREATE | Building | Sound Studio, Visual Forge, Video Forge |
|
||||
| COLLECT | Building | Gallery — save, browse, own your creations |
|
||||
| PLAY | Building | Games Floor — interactive experiences |
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.html` | Entry point — entrance curtain + main layout |
|
||||
| `src/playground.js` | Main orchestrator — particles, entrance, mode switching |
|
||||
| `src/engine/audio-engine.js` | Web Audio API — synthesis, effects, recording |
|
||||
| `src/engine/visual-engine.js` | Canvas rendering — particles, shapes, animations |
|
||||
| `src/modes/mode-manager.js` | Mode switching — constellation, sound, visual |
|
||||
| `src/modes/constellation.js` | Constellation Maker experience |
|
||||
| `src/gallery/gallery.js` | Gallery — IndexedDB storage, browsing |
|
||||
| `src/export/download.js` | Export — zip, download, share |
|
||||
| `src/export/wav-encoder.js` | WAV encoding for audio export |
|
||||
| `src/panels/sound/sound-panel.js` | Sound Studio UI |
|
||||
| `src/panels/gallery/gallery-panel.js` | Gallery UI |
|
||||
| `src/styles/design-system.css` | Sovereign aesthetic — dark bg, gold accents |
|
||||
| `src/utils/events.js` | Event system — pub/sub |
|
||||
| `src/utils/state.js` | State management — reactive store |
|
||||
| `src/utils/utils.js` | Utility functions |
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Local-first** — Everything runs in the browser. IndexedDB for storage.
|
||||
2. **Zero dependencies** — Pure Canvas + Web Audio API + vanilla JS.
|
||||
3. **Take it home** — Every creation can be downloaded.
|
||||
4. **Sovereign aesthetic** — Dark backgrounds. Gold accents. Plain, honest, profound.
|
||||
5. **Accessible** — Keyboard navigable. Screen reader friendly. Works on mobile.
|
||||
|
||||
## Experiences Roadmap
|
||||
|
||||
| # | Experience | Status | Description |
|
||||
|---|-----------|--------|-------------|
|
||||
| 15 | Synesthesia Engine | v0.3 | See sound, hear color |
|
||||
| 16 | Emotional Weather | v0.3 | Type feelings → sky responds |
|
||||
| 17 | Constellation Maker | v0.3 | Place stars, connect, hear night sky |
|
||||
| 18 | Breath Instrument | v0.4 | Lungs become music |
|
||||
| 19 | Dream Journal | v0.4 | Describe dream → world |
|
||||
| 20 | The Mirror | v0.4 | Camera paints your energy |
|
||||
| 21 | Sound Fossils | v0.4 | Sound → living creature |
|
||||
| 22 | Letter to Future | v0.4 | Write, seal, set date |
|
||||
| 23 | Garden Paths | v0.5 | Choose-your-own soundscape |
|
||||
| 24 | Heartbeat Sync | v0.5 | Finger on camera → world beats |
|
||||
| 25 | The Conversation | v0.5 | Two instruments create together |
|
||||
|
||||
## Milestones
|
||||
|
||||
- **v0.1** The Forge Opens — First playable
|
||||
- **v0.2** Take It Home — Download anything
|
||||
- **v0.3** The Games Floor — Synesthesia, Emotional Weather, Constellation
|
||||
- **v0.4** The Gallery Show — Breath, Dreams, Mirror, Fossils
|
||||
- **v0.5** The Video Forge — Garden, Heartbeat, Conversation
|
||||
- **v1.0** The Sovereign Playground — Full platform
|
||||
|
||||
## File Index
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.html` | Entry point (entrance curtain + layout) |
|
||||
| `README.md` | Project overview and philosophy |
|
||||
| `smoke-test.html` | CI smoke test |
|
||||
| `src/playground.js` | Main orchestrator (~410 LOC) |
|
||||
| `src/engine/audio-engine.js` | Web Audio synthesis |
|
||||
| `src/engine/visual-engine.js` | Canvas rendering |
|
||||
| `src/modes/constellation.js` | Constellation Maker |
|
||||
| `src/modes/mode-manager.js` | Mode switching |
|
||||
| `src/gallery/gallery.js` | Gallery system |
|
||||
| `src/export/download.js` | Export/download |
|
||||
| `src/export/wav-encoder.js` | WAV encoding |
|
||||
| `src/panels/sound/sound-panel.js` | Sound UI |
|
||||
| `src/panels/gallery/gallery-panel.js` | Gallery UI |
|
||||
| `src/styles/design-system.css` | Design system |
|
||||
| `src/utils/events.js` | Event pub/sub |
|
||||
| `src/utils/state.js` | Reactive state |
|
||||
| `src/utils/utils.js` | Utilities |
|
||||
|
||||
**Total: 17 files | Pure vanilla JS | Zero dependencies**
|
||||
|
||||
## Sovereignty Assessment
|
||||
|
||||
- **Fully local** — All computation in the browser
|
||||
- **No server** — Static files only, can be served from anywhere
|
||||
- **No accounts** — IndexedDB for local storage
|
||||
- **No tracking** — Zero analytics, zero telemetry
|
||||
- **Downloadable** — Every creation can be saved locally
|
||||
|
||||
**Verdict: Fully sovereign. Art heals. Music heals. Creation heals.**
|
||||
|
||||
---
|
||||
|
||||
*"A man who is building something is a man who is not destroying himself."*
|
||||
@@ -16,14 +16,11 @@ import argparse
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
import re
|
||||
from json import JSONDecoder
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_HOST = "67.205.155.108"
|
||||
DEFAULT_HOST = "159.203.146.185"
|
||||
DEFAULT_HOSTNAME = "bezalel"
|
||||
DEFAULT_INVENTORY_PATH = Path(__file__).resolve().parents[1] / "ansible" / "inventory" / "hosts.ini"
|
||||
DEFAULT_PEERS = {
|
||||
"mac": "100.124.176.28",
|
||||
"ezra": "100.126.61.75",
|
||||
@@ -69,37 +66,6 @@ def parse_tailscale_status(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def resolve_host(host: str | None, inventory_path: Path = DEFAULT_INVENTORY_PATH, hostname: str = DEFAULT_HOSTNAME) -> tuple[str, str]:
|
||||
if host:
|
||||
return host, "explicit"
|
||||
if inventory_path.exists():
|
||||
pattern = re.compile(rf"^{re.escape(hostname)}\s+.*ansible_host=([^\s]+)")
|
||||
for line in inventory_path.read_text().splitlines():
|
||||
match = pattern.search(line.strip())
|
||||
if match:
|
||||
return match.group(1), f"inventory:{inventory_path}"
|
||||
return DEFAULT_HOST, "default"
|
||||
|
||||
|
||||
def parse_apply_output(stdout: str) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {"tailscale": None, "ping_ok": {}}
|
||||
text = stdout or ""
|
||||
start = text.find("{")
|
||||
if start != -1:
|
||||
try:
|
||||
payload, _ = JSONDecoder().raw_decode(text[start:])
|
||||
if isinstance(payload, dict):
|
||||
result["tailscale"] = parse_tailscale_status(payload)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for line in text.splitlines():
|
||||
if line.startswith("PING_OK:"):
|
||||
_, name, ip = line.split(":", 2)
|
||||
result["ping_ok"][name] = ip
|
||||
return result
|
||||
|
||||
|
||||
def build_ssh_command(host: str, remote_script_path: str = "/tmp/bezalel_tailscale_bootstrap.sh") -> list[str]:
|
||||
return ["ssh", host, f"bash {shlex.quote(remote_script_path)}"]
|
||||
|
||||
@@ -123,9 +89,8 @@ def parse_peer_args(items: list[str]) -> dict[str, str]:
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Prepare or execute Tailscale bootstrap for the Bezalel VPS.")
|
||||
parser.add_argument("--host")
|
||||
parser.add_argument("--host", default=DEFAULT_HOST)
|
||||
parser.add_argument("--hostname", default=DEFAULT_HOSTNAME)
|
||||
parser.add_argument("--inventory-path", type=Path, default=DEFAULT_INVENTORY_PATH)
|
||||
parser.add_argument("--auth-key", help="Tailscale auth key")
|
||||
parser.add_argument("--auth-key-file", type=Path, help="Path to file containing the Tailscale auth key")
|
||||
parser.add_argument("--ssh-public-key", help="SSH public key to append to authorized_keys")
|
||||
@@ -151,7 +116,6 @@ def main() -> None:
|
||||
auth_key = _read_secret(args.auth_key, args.auth_key_file)
|
||||
ssh_public_key = _read_secret(args.ssh_public_key, args.ssh_public_key_file)
|
||||
peers = parse_peer_args(args.peer)
|
||||
resolved_host, host_source = resolve_host(args.host, args.inventory_path, args.hostname)
|
||||
|
||||
if not auth_key:
|
||||
raise SystemExit("Missing Tailscale auth key. Use --auth-key or --auth-key-file.")
|
||||
@@ -162,31 +126,28 @@ def main() -> None:
|
||||
write_script(args.script_out, script)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"host": resolved_host,
|
||||
"host_source": host_source,
|
||||
"host": args.host,
|
||||
"hostname": args.hostname,
|
||||
"inventory_path": str(args.inventory_path),
|
||||
"script_out": str(args.script_out),
|
||||
"remote_script_path": args.remote_script_path,
|
||||
"ssh_command": build_ssh_command(resolved_host, args.remote_script_path),
|
||||
"ssh_command": build_ssh_command(args.host, args.remote_script_path),
|
||||
"peer_targets": peers,
|
||||
"applied": False,
|
||||
}
|
||||
|
||||
if args.apply:
|
||||
result = run_remote(resolved_host, args.remote_script_path)
|
||||
result = run_remote(args.host, args.remote_script_path)
|
||||
payload["applied"] = True
|
||||
payload["exit_code"] = result.returncode
|
||||
payload["stdout"] = result.stdout
|
||||
payload["stderr"] = result.stderr
|
||||
payload["verification"] = parse_apply_output(result.stdout)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
print("--- Bezalel Tailscale Bootstrap ---")
|
||||
print(f"Host: {resolved_host} ({host_source})")
|
||||
print(f"Host: {args.host}")
|
||||
print(f"Local script: {args.script_out}")
|
||||
print("SSH command: " + " ".join(payload["ssh_command"]))
|
||||
if args.apply:
|
||||
|
||||
@@ -2,12 +2,9 @@ from scripts.bezalel_tailscale_bootstrap import (
|
||||
DEFAULT_PEERS,
|
||||
build_remote_script,
|
||||
build_ssh_command,
|
||||
parse_apply_output,
|
||||
parse_peer_args,
|
||||
parse_tailscale_status,
|
||||
resolve_host,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_build_remote_script_contains_install_up_and_key_append():
|
||||
@@ -81,46 +78,3 @@ def test_parse_peer_args_merges_overrides_into_defaults():
|
||||
"ezra": "100.126.61.76",
|
||||
"forge": "100.70.0.9",
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_host_prefers_inventory_over_stale_default(tmp_path: Path):
|
||||
inventory = tmp_path / "hosts.ini"
|
||||
inventory.write_text(
|
||||
"[fleet]\n"
|
||||
"ezra ansible_host=143.198.27.163 ansible_user=root\n"
|
||||
"bezalel ansible_host=67.205.155.108 ansible_user=root\n"
|
||||
)
|
||||
|
||||
host, source = resolve_host(None, inventory)
|
||||
|
||||
assert host == "67.205.155.108"
|
||||
assert source == f"inventory:{inventory}"
|
||||
|
||||
|
||||
def test_parse_apply_output_extracts_status_and_ping_markers():
|
||||
stdout = (
|
||||
'{"Self": {"HostName": "bezalel", "DNSName": "bezalel.tailnet.ts.net", "TailscaleIPs": ["100.90.0.10"]}, '
|
||||
'"Peer": {"node-1": {"HostName": "ezra", "TailscaleIPs": ["100.126.61.75"]}}}'
|
||||
"\nPING_OK:mac:100.124.176.28\n"
|
||||
"PING_OK:ezra:100.126.61.75\n"
|
||||
)
|
||||
|
||||
result = parse_apply_output(stdout)
|
||||
|
||||
assert result["tailscale"]["self"]["tailscale_ips"] == ["100.90.0.10"]
|
||||
assert result["ping_ok"] == {"mac": "100.124.176.28", "ezra": "100.126.61.75"}
|
||||
|
||||
|
||||
def test_runbook_doc_exists_and_mentions_inventory_auth_and_peer_checks():
|
||||
doc = Path("docs/BEZALEL_TAILSCALE_BOOTSTRAP.md")
|
||||
assert doc.exists(), "missing docs/BEZALEL_TAILSCALE_BOOTSTRAP.md"
|
||||
text = doc.read_text()
|
||||
assert "ansible/inventory/hosts.ini" in text
|
||||
assert "tailscale up" in text
|
||||
assert "authorized_keys" in text
|
||||
assert "100.124.176.28" in text
|
||||
assert "100.126.61.75" in text
|
||||
|
||||
runbook = Path("docs/RUNBOOK_INDEX.md").read_text()
|
||||
assert "Prepare Bezalel Tailscale bootstrap" in runbook
|
||||
assert "scripts/bezalel_tailscale_bootstrap.py" in runbook
|
||||
|
||||
Reference in New Issue
Block a user