From b6cc829f53f03fb2eb992df24ea489841608f710 Mon Sep 17 00:00:00 2001 From: Keysat Date: Mon, 15 Jun 2026 13:58:15 -0500 Subject: [PATCH] Land Phase 0 launch chain: SSH -> desktop Terminal -> claude -> phone Phase 0 proven by hand (N=3) across multiple rooms. - scripts/gui-launch.sh: open a desktop Terminal via osascript so claude runs in the GUI session (login Keychain + real TTY), avoiding a long-lived token (D11). - scripts/launch-claude.sh: name the session `claude -n " - "` so Remote Control's phone conversation index is readable. - .env.example: bot credential schema (real .env stays gitignored). - AGENTS.md / ROADMAP.md: D11, Phase 0 results, Phase 1 carry-overs. --- .env.example | 10 +++++++ AGENTS.md | 62 ++++++++++++++++++++++++++++++++++++---- ROADMAP.md | 9 ++++++ scripts/gui-launch.sh | 47 ++++++++++++++++++++++++++++++ scripts/launch-claude.sh | 15 +++++++++- 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 .env.example create mode 100755 scripts/gui-launch.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..775afad --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# matrix-bridge bot credentials — COPY to .env (gitignored) and fill in real values. +# Never commit real values. The bot runs on the Spark; the real .env lives next to it there. + +MATRIX_HOMESERVER=https:// +MATRIX_USER=@: +MATRIX_DEVICE_ID=matrix-bridge-bot +MATRIX_ACCESS_TOKEN= +# Optional — kept for recovery / re-minting a token. The bot authenticates with the access token, +# not the password (logging in every start would spawn a new device each time). +MATRIX_PASSWORD= diff --git a/AGENTS.md b/AGENTS.md index 136fb96..82e2454 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,14 @@ Condensed from the scoping workshop. Each: the call, why, what it beat. the bot ever handles sensitive content over untrusted transport. - **D10 — Spark Control manages the bot (Phase 3).** Status on the dashboard + one-click update/restart, the same SSH-behind-buttons pattern Spark Control uses for the Sparks today. +- **D11 — Launch into a desktop Terminal, not a headless token (Phase 0).** The SSH session + can't reach the GUI login Keychain, so a plain `ssh … claude` reports "Not logged in." Rather + than mint a long-lived `claude setup-token`, the launcher (`scripts/gui-launch.sh`) uses + `osascript` to open a Terminal.app window in the **GUI session**, where `claude` inherits the + existing Keychain login and a real TTY. *Beat:* the long-lived OAuth token (Approach A) — works + and is fully unattended, but adds a credential to manage; kept as the documented fallback if the + Mac is ever driven headless (logged out). *Cost:* requires the Mac logged in + a one-time + Terminal Automation grant. ## Sovereignty constraint @@ -128,9 +136,51 @@ once" is not done. - **Scaffolded 2026-06-15** from a prior scoping package (SPEC/DECISIONS/CLAUDE/KICKOFF), folded into this AGENTS.md (decisions + placement), `ROADMAP.md` (phases), and the wrapper + config skeleton. No bot code yet — by design. -- **Next: Phase 0 (manual chain validation, N=3)** — Matrix onboarding (Element, Space, first - room, a bot user), write + locally test `scripts/launch-claude.sh`, passwordless SSH from the - Spark to the Mac, prove the full chain (message → SSH → wrapper → Claude session → phone - notification I can drive) by hand at least 3 times, and record the first room→repo mapping. - Bot code starts only after Phase 0 is proven. The original KICKOFF prompt is the step-by-step - for Phase 0. +- **Phase 0 — SSH leg proven (2026-06-15).** Mac Remote Login is on. The Spark `spark-32d0` + (user `modelo`) reaches the Mac over `starttunnel`/WireGuard at `10.59.211.5` — *not* the + LAN (the Spark isn't on the Mac's LAN subnet). A dedicated per-machine key + (`spark-control@spark-32d0` = `~/.ssh/id_ed25519` on the Spark) is in the Mac's + `authorized_keys`. SSH alias **`mac-bridge`** in the Spark's `~/.ssh/config` selects that key + (`IdentityFile ~/.ssh/id_ed25519` + `IdentitiesOnly yes`) — required because the pre-existing + `Host * → id_ed25519_shared` rule otherwise shadows the default key. The bot's entire Mac hop + is therefore `ssh mac-bridge ''`. *Phase 1:* bake the dedicated key + an equivalent + alias/config into the bot's Docker image (modelo's `~/.ssh/config` won't exist in the + container). +- **Phase 0 — launch chain proven end-to-end (2026-06-15).** `ssh mac-bridge → gui-launch.sh + → launch-claude.sh → authenticated claude → phone via Remote Control` works against a real + repo (`premier-gunner`). Chose **Approach B (desktop Terminal)** over a headless token — see + **D11**. Two pieces it took: (1) `~/.local/bin` (where `claude` lives) had to be added to + `~/.zprofile`, because a non-interactive login shell skips `.zshrc`; (2) `scripts/gui-launch.sh` + opens a Terminal.app window via `osascript` so `claude` runs inside the GUI session (login + Keychain + real TTY) — needed a one-time "Allow ssh to control Terminal" Automation grant. + *Known caveats for the bot:* (a) a never-trusted repo stalls at Claude's first-run folder-trust + gate — unattended launches must target already-trusted repos or pass a skip flag; (b) if the + TCC Automation grant ever resets, a launch stalls until someone clicks Allow — the bot should + detect a failed launch and report it back to the room, not hang. +- **Phase 0 — Matrix bot user live (2026-06-15).** Homeserver is the StartOS Synapse exposed on + **clearnet at `https://matrix.gilliam.ai`** (`server_name` = `matrix.gilliam.ai`, Synapse + 1.154.0) — *not* the stale `@gilliam:` account found in Element. Created a dedicated + non-admin bot **`@agent:matrix.gilliam.ai`** (type `bot`) via the Synapse Admin Dashboard + (StartOS "Create Bot User" is appservice-only/greyed out). Minted a long-lived access token + (fixed `device_id` `matrix-bridge-bot`), verified via `whoami`, and stored + homeserver/user/token/device_id (+ password for recovery) in the gitignored **`.env`** (chmod + 600). `config.toml` holds homeserver+user; `.env.example` documents the schema. Bot reuses the + stored token — never re-login per start (avoids device churn); no E2EE (D9). *Note:* the + bot↔Synapse hop is now public-internet TLS, which softens D9's "transport already WireGuard- + private" rationale (still TLS to the user's own server, single-user content) — revisit if it matters. +- **Phase 0 — rooms mapped (2026-06-15).** 9 project rooms in `config.toml` (premier-gunner, + recap, recap-relay, spark-control, ten31-transcripts, ten31-signal-engine, keysat, proof-of-work, + ten31-database), each `room_id → /Users/macpro/Projects/`. `@agent` is **joined to all 9** + (via its token), so the Phase-1 bot will see messages in each. *Manual by-hand launches must keep + message text free of `'`/`"`* — the typed SSH command line breaks on them (PS2 `>` hang); the + Phase-1 bot avoids this via `shlex.quote`. +- **Phase 0 — PROVEN / DONE (2026-06-15).** N=3 by-hand runs succeeded across multiple rooms + (recap, spark-control, premier-gunner): each opened a Terminal in the right repo, started `claude` + on the message, and pushed a drivable session to the phone. The deterministic core holds. + Added session naming: `launch-claude.sh` now runs `claude -n " - "` (topic from the + message, overridable via `$MB_SESSION_NAME`) so Remote Control's phone index is readable — + confirmed `-n` drives the phone app's conversation label. +- **Next: Phase 1 — the matrix-nio bot.** Container on the Spark, logged in as `@agent` (token in + `.env`), listening in the 9 mapped rooms; on a message it runs `ssh mac-bridge gui-launch.sh + ` (built with `shlex.quote`) and reports failures back to the room. See + ROADMAP Phase 1 (also: bake key+config into the image, curated `$MB_SESSION_NAME` topic, fail-loud). diff --git a/ROADMAP.md b/ROADMAP.md index 00e70a7..5e3f0c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,15 @@ after it. - matrix-nio bot in a container on the Spark, logged in as a bot Matrix user. - One hardcoded room → one repo. Any message in it spawns a session via the Mac wrapper. +- Carry over from Phase 0's proven launch chain (`ssh mac-bridge → gui-launch.sh → launch-claude.sh`): + - **Bake the SSH key + `mac-bridge` config into the container** (modelo's `~/.ssh` won't exist there). + - **Named sessions for the phone app.** Pass `claude -n ""` so the Remote Control + conversation index is readable (project + topic). Bot derives `` from the message; confirm + whether the app labels off `-n` or `--remote-control `. Plumb a name arg through the wrappers. + - **Quote-safe message passing.** Bot builds the SSH command with `shlex.quote`; `gui-launch.sh` + already isolates the osascript/shell layers via a `%q` temp script — stress-test with hostile text. + - **Fail loud, not silent.** Detect a stalled launch (untrusted-repo trust gate, or a reset Terminal + Automation grant) and report it back into the room instead of hanging. - **Exit (falsifiable):** 3 consecutive real messages each correctly launch a drivable session on the phone. diff --git a/scripts/gui-launch.sh b/scripts/gui-launch.sh new file mode 100755 index 0000000..d2d0756 --- /dev/null +++ b/scripts/gui-launch.sh @@ -0,0 +1,47 @@ +#!/bin/zsh -l +# gui-launch.sh — matrix-bridge GUI launcher (Approach B: "open a real desktop terminal"). +# +# Invoked over SSH by the bot: gui-launch.sh +# +# Opens a Terminal.app window on the logged-in desktop that runs launch-claude.sh, so +# `claude` runs INSIDE the GUI session: it gets the login Keychain (no long-lived token +# needed) and a real TTY. Claude Code Remote Control then surfaces the session to the phone. +# +# Requires: the Mac logged into its desktop, and a one-time "Allow ssh to control +# Terminal" Automation grant (System Settings > Privacy & Security > Automation). +# +# Layering: gui-launch.sh owns the GUI/session seam; launch-claude.sh owns the +# environment seam (cd + exec claude). Keep that split. + +script_dir="${0:A:h}" +inner="$script_dir/launch-claude.sh" + +repo_dir="$1" +shift +prompt="$*" + +if [[ -z "$repo_dir" || -z "$prompt" ]]; then + print -u2 "usage: gui-launch.sh " + exit 2 +fi +# Fail loud on a bad directory — never open a session in the wrong place (guardrail). +if [[ ! -d "$repo_dir" ]]; then + print -u2 "gui-launch: no such repo dir: $repo_dir" + exit 1 +fi + +# Embed repo+prompt in a throwaway script via %q so user text NEVER crosses the +# AppleScript string layer — only a safe mktemp path does. This sidesteps the +# SSH -> osascript -> shell -> wrapper multi-shell quoting trap the spec warns about. +launch_script="$(mktemp -t mb-launch)" +{ + print -r -- '#!/bin/zsh -l' + printf 'exec %q %q %q\n' "$inner" "$repo_dir" "$prompt" +} >| "$launch_script" +chmod +x "$launch_script" + +# mktemp paths contain no spaces/quotes, so embedding the path directly is safe. +osascript -e "tell application \"Terminal\" to do script \"$launch_script\"" \ + -e 'tell application "Terminal" to activate' + +print -- "gui-launch: opened Terminal for $repo_dir" diff --git a/scripts/launch-claude.sh b/scripts/launch-claude.sh index 42bdb73..accb91c 100755 --- a/scripts/launch-claude.sh +++ b/scripts/launch-claude.sh @@ -21,4 +21,17 @@ fi # Fail loud on a bad directory — never launch Claude in the wrong place. cd "$repo_dir" || { print -u2 "launch-claude: no such repo dir: $repo_dir"; exit 1; } -exec claude "$prompt" +# Name the session " - " so it's identifiable in Remote Control's +# conversation index on the phone. Topic defaults to a trimmed slice of the message; +# the Phase-1 bot can override it with a curated topic via $MB_SESSION_NAME. +repo_name="${${repo_dir%/}:t}" +if [[ -n "$MB_SESSION_NAME" ]]; then + session_name="$MB_SESSION_NAME" +else + topic="${prompt//$'\n'/ }" # collapse newlines to keep the name one line + topic="${topic[1,60]}" # cap length for a tidy index entry + [[ ${#prompt} -gt 60 ]] && topic="${topic}…" + session_name="${repo_name} - ${topic}" +fi + +exec claude -n "$session_name" "$prompt"