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 "<repo> - <topic>"` 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.
This commit is contained in:
@@ -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://<your-synapse-host>
|
||||||
|
MATRIX_USER=@<bot>:<your-domain>
|
||||||
|
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=
|
||||||
@@ -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.
|
the bot ever handles sensitive content over untrusted transport.
|
||||||
- **D10 — Spark Control manages the bot (Phase 3).** Status on the dashboard + one-click
|
- **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.
|
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
|
## Sovereignty constraint
|
||||||
|
|
||||||
@@ -128,9 +136,51 @@ once" is not done.
|
|||||||
- **Scaffolded 2026-06-15** from a prior scoping package (SPEC/DECISIONS/CLAUDE/KICKOFF),
|
- **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 +
|
folded into this AGENTS.md (decisions + placement), `ROADMAP.md` (phases), and the wrapper +
|
||||||
config skeleton. No bot code yet — by design.
|
config skeleton. No bot code yet — by design.
|
||||||
- **Next: Phase 0 (manual chain validation, N=3)** — Matrix onboarding (Element, Space, first
|
- **Phase 0 — SSH leg proven (2026-06-15).** Mac Remote Login is on. The Spark `spark-32d0`
|
||||||
room, a bot user), write + locally test `scripts/launch-claude.sh`, passwordless SSH from the
|
(user `modelo`) reaches the Mac over `starttunnel`/WireGuard at `10.59.211.5` — *not* the
|
||||||
Spark to the Mac, prove the full chain (message → SSH → wrapper → Claude session → phone
|
LAN (the Spark isn't on the Mac's LAN subnet). A dedicated per-machine key
|
||||||
notification I can drive) by hand at least 3 times, and record the first room→repo mapping.
|
(`spark-control@spark-32d0` = `~/.ssh/id_ed25519` on the Spark) is in the Mac's
|
||||||
Bot code starts only after Phase 0 is proven. The original KICKOFF prompt is the step-by-step
|
`authorized_keys`. SSH alias **`mac-bridge`** in the Spark's `~/.ssh/config` selects that key
|
||||||
for Phase 0.
|
(`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 '<command>'`. *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:<onion>` 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/<repo>`. `@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 "<repo> - <topic>"` (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
|
||||||
|
<repo_dir> <message>` (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).
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ after it.
|
|||||||
|
|
||||||
- matrix-nio bot in a container on the Spark, logged in as a bot Matrix user.
|
- 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.
|
- 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 "<repo> — <topic>"` so the Remote Control
|
||||||
|
conversation index is readable (project + topic). Bot derives `<topic>` from the message; confirm
|
||||||
|
whether the app labels off `-n` or `--remote-control <name>`. 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
|
- **Exit (falsifiable):** 3 consecutive real messages each correctly launch a drivable
|
||||||
session on the phone.
|
session on the phone.
|
||||||
|
|
||||||
|
|||||||
Executable
+47
@@ -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 <repo_dir> <prompt...>
|
||||||
|
#
|
||||||
|
# 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 <repo_dir> <prompt>"
|
||||||
|
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"
|
||||||
@@ -21,4 +21,17 @@ fi
|
|||||||
# Fail loud on a bad directory — never launch Claude in the wrong place.
|
# 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; }
|
cd "$repo_dir" || { print -u2 "launch-claude: no such repo dir: $repo_dir"; exit 1; }
|
||||||
|
|
||||||
exec claude "$prompt"
|
# Name the session "<repo> - <topic>" 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user