gmux agent fleet ↔ QALS wallets + phone approvals · gmux-bridge/README.md
gmux-bridge — the QALS economy wired into gmux's agent fleet
Created 2026-09-11 by the integrator session (project: crypto_currency).
Answer to the question "check that it could be used for the gmux interface
also": yes — and here is the bridge that does it.
gmux (terminal/tmux generation, ~/projects/gmux, read-only — this bridge
changes nothing there) shows a fleet of AI agents as coloured panes: 🟢
working, 🔴 waiting, ⚠ permission needed, 🟡 idle. What gmux does NOT know is
what each agent costs. gmux-bridge adds the money layer: every gmux
session maps to a loopd capped wallet, every permission request becomes a
message on the owner's phone, and one screen shows status colour AND
Qals spent/cap for every agent.
Architecture map
gmux box (unchanged, read-only for v1)
┌────────────────────────────────────────────────────────────┐
│ tmux panes ── phone_bridge.py :8768 GET /status {panes[]} │
│ (state from QalCode2 API: /session/status, │
│ SSE /event → working/waiting/permission) │
│ or monitor state file /tmp/gmux-pane-state.json│
└──────────────┬─────────────────────────────────────────────┘
│ ① auto-poll (same box) or ② POST /fleet/report (remote)
▼
gmux-bridge :8842 (this folder, zero-dep)
│ │
│ ③ provision │ ④ alerts
▼ ▼
loopd :8823 qalchat relay :8830
gmux-<session> identity "gmux-bridge" → owner handle
capped wallet → encrypted message → owner's phone
(200c/day default) inbox (same UI as normal chat)
▲
│ ⑤ the agent itself pays via MCP:
qalmcp (--handle gmux-<session>) → send_payment / buy_compute
→ loopd hold → settle — hard-capped server-side, prompt-injection
can only spend what the cap allows
Mapping table:
| gmux concept | QALS concept | Mechanism |
|---|---|---|
| tmux session / window (pane name) | loopd agent gmux-<session> |
POST /agents/provision (idempotent, 200c/day cap, 2000c seed) |
| Pane state colour (working/waiting/permission) | — (display only) | merged verbatim from gmux into /fleet/status |
Permission request (! orange pane) |
qalchat alert to owner | POST /alerts → encrypted DM from gmux-bridge |
| Agent pays for tools/compute | loopd hold → settle | agent's agent_id+api_secret into qalmcp; caps enforced by loopd, not the model |
| Phone dashboard :8768 (gmux) | phone inbox (qalchat/qalbridge) | same device, same inbox for messages + money alerts |
Run it
# prereqs already running: loopd :8823, qalchat relay :8830
python3 qalchat/qalchat.py init gmux-bridge # once — the bridge's identity
python3 gmux-bridge/serve.py # serves http://127.0.0.1:8842
open http://127.0.0.1:8842/ # one-screen fleet ⊕ Qals view
API
| Endpoint | What |
|---|---|
GET /fleet/status |
every gmux pane (state + colour) merged with its wallet (spent/cap today, budget ok/warn/exceeded); usage totals; unprovisioned_sessions list |
POST /agents/provision {"gmux_session_name":"doofing"} |
creates (or returns the existing) gmux-doofing loopd wallet → {agent_id, api_secret, cap_cents_per_day} — hand these to the agent's qalmcp config |
POST /alerts {"session":"ai_diary","level":"permission","title":"write tool","detail":"..."} |
encrypted qalchat message to $GMUXBRIDGE_OWNER; also appended to data/alerts.jsonl |
GET /alerts?limit=20 |
recent alerts (programmatic check) |
GET /usage/today |
sum of all gmux-* wallets' spend + per-wallet rows |
POST /fleet/report {"panes":[...]} |
manual ingest — see below |
Config via env: GMUXBRIDGE_PORT (8842), GMUXBRIDGE_LOOPD,
GMUXBRIDGE_GMUX_URL (:8768), GMUXBRIDGE_STATE_FILE
(/tmp/gmux-pane-state.json), GMUXBRIDGE_OWNER (default owner),
GMUXBRIDGE_QALCHAT_IDENTITY (gmux-bridge), GMUXBRIDGE_SEED_CENTS (2000),
GMUXBRIDGE_DEFAULT_CAP_CENTS (200).
Wiring the real gmux box (read-only over there)
gmux-bridge auto-detects panes in this order:
- HTTP —
GET http://127.0.0.1:8768/status(gmux's phone_bridge, aiohttp; if that server is up on the same box, zero config). - State file —
/tmp/gmux-pane-state.json(written by gmux's monitor even when the phone bridge isn't serving; same box only). - Manual ingest — for a REMOTE gmux box (or if neither exists): have any
gmux hook (tmux hook, cron, the phone_bridge process itself) post its panes.
A manual report is trusted for
GMUXBRIDGE_MANUAL_TTL_MS(default 60 s) — past that it goes stale and/fleet/statushonestly drops tosource: offlinerather than showing a ghost fleet. Post at least that often.
# The exact curl a gmux-side hook would call — no gmux code changes needed,
# e.g. a cron'd one-liner or a tmux periodic hook on the gmux box:
curl -s -X POST http://<bridge-box>:8842/fleet/report \
-H 'Content-Type: application/json' \
-d "$(curl -s http://127.0.0.1:8768/status)" # forward gmux verbatim
# or the raw monitor state file (shape is accepted too):
curl -s -X POST http://<bridge-box>:8842/fleet/report \
-H 'Content-Type: application/json' -d "{\"panes\": $(cat /tmp/gmux-pane-state.json)}"
# (when posting the raw dict shape, wrap it OR post it as-is — both accepted)
Alerts the other way (gmux → phone): anything that can see a permission event
can POST /alerts. A natural v2 hook: gmux's pane-watcher notices a permission
state and posts the alert; v1 ships the endpoint and documents the call.
Test
bash gmux-bridge/test_bridge.sh # 30 checks, live loopd+relay, ALL GREEN
Proves: gmux HTTP ingest (against mock_gmux.py, byte-identical /status
shape), provisioning + idempotence, real HMAC hold→settle spend, colour/spend
merge, alert → owner mailbox via qalchat recv, manual ingest fallback, usage
sums, HTML dashboard.
Honest v1 limits
- Manual ingest may be the real path. gmux's phone bridge is dormant
(last active 2026-05) and the
:8769push-receiver mentioned indocs/QALCODE2_INTEGRATION.mdwas never committed — and that port now hosts the Qalarc Hub anyway. If gmux isn't running its HTTP server,/fleet/statushonestly reportssource: offlineuntil a hook posts/fleet/report. - Approvals are notifications, not buttons.
POST /alertsdelivers to the owner's qalchat inbox, but the owner still approves in gmux itself (phone dashboard :8768, ortmux send-keys). gmux'sPOST /permission/:id/respond(accept/deny → keys to the pane) is NOT proxied by the bridge yet — the phone cannot one-tap-approve from the QALS side in v1. - Secrets at rest.
data/provisions.jsonstores wallet secrets so re-provisioning is idempotent (a lost registry would orphan wallets in loopd). localhost paper-mode trade-off; 0600 the folder if paranoid. - Pane↔wallet matching is by name. Session
AI_diary→ walletgmux-ai_diary. Rename the window and you get a fresh unprovisioned session (the old wallet becomes an orphan row until reclaimed). - Poll, not push. gmux is polled per request (2s timeout); there is no websocket/stream from either gmux or loopd.
- Spend is opt-in for the agent. Provisioning a wallet does not force the
agent to pay; wiring
qalmcp --handle gmux-<session>into the agent's MCP config is a separate (manual) step.