gpty-ipc
JSON-RPC 2.0 IPC transport, client, and server for gpty workspace control.
Role in the Workspace
| Crate | Role | Depends On |
|---|---|---|
gpty-core |
Engine library (PTY, ANSI, grid, concepts) | — |
gpty-ipc |
IPC transport + protocol | gpty-core |
gpty-gdext |
Godot 4 GDExtension bridge + IPC server | gpty-core, gpty-ipc |
gpty-cli |
CLI client for workspace control | gpty-ipc |
Modules
| Module | Purpose |
|---|---|
protocol |
JSON-RPC 2.0 types: Request, Response, JsonRpcError with builders |
types |
IPC domain types: NewPaneParams, PaneInfo, InjectParams, etc. |
transport |
Platform-specific socket connection (Unix domain, named pipe) |
server |
Async IPC server: bind socket, accept connections, dispatch to handlers |
client |
Async IPC client: connect, send request, read response with timeout |
Architecture
The GUI owns the workspace state — the CLI is a stateless client: each invocation connects, sends one JSON-RPC request, reads the response, and exits. No persistent connection, no connection management.
gpty CLI ──▶ socket / named pipe ──▶ IpcServer (gdext, tokio task)
│ queues PENDING_REQUESTS
▼
GDScript _process() polls drain_ipc_requests()
→ workspace._handle_ipc_method() mutates the scene
→ respond_ipc() completes the pending oneshotThe GUI scene tree is single-threaded, so requests are never handled on
the socket thread: the server queues them and GDScript polls each frame
(crates/gpty-gdext/src/ipc.rs). version and shutdown are answered
locally in Rust; every other method round-trips through GDScript with a
5-second fallback timeout, and the response flows back through a oneshot
channel.
Daemon auto-spawn (gpty-cli): when the socket isn’t reachable, the
CLI spawns the GUI as a separate OS process — the CLI never links Godot —
and polls the socket until ready. The GUI binary is discovered via the
GPTY_GUI env var (validated before use) or beside the CLI executable
(gpty-gui, gpty-editor).
Protocol
All communication is newline-delimited JSON-RPC 2.0 over a platform socket:
- Linux: Unix domain socket at
$XDG_RUNTIME_DIR/gpty.sock(fallbacks:/run/user/<uid>/gpty.sock,/tmp/gpty-<uid>.sock; override withGPTY_SOCKET) - macOS:
$TMPDIR/gpty.sock(fallback/tmp/gpty-<uid>.sock; override withGPTY_SOCKET) - Windows: Named pipe
\\.\pipe\gpty
Example
Request:
{"jsonrpc":"2.0","id":1,"method":"newPane","params":{"type":"terminal","command":"/bin/bash","focus":true}}Response:
{"jsonrpc":"2.0","id":1,"result":{"pane_id":"T1","type":"terminal"}}Error:
{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"Unknown method: badMethod"}}Security
The IPC channel controls the whole workspace, so the server hardens the channel against other local users:
- Socket placement: Linux defaults to
$XDG_RUNTIME_DIR/gpty.sock(a per-user, 0700 directory); the socket file itself is chmod 0600. - Peer UID check: on Linux/macOS the server verifies the connecting process runs as the same effective UID as the server and drops mismatches.
- Shared secret (optional): set
GPTY_SECRETwhen launching the GUI and for every client (CLI, MCP). The server rejects requests with a missing or mismatchedgpty_secretfield (-32001). - Request size cap: request lines over 64 KiB get an
-32600error. - Connection cap: at most 16 concurrent connections; slow connections are dropped after 30 s.
Event socket (OMP observability)
A second listener, default_event_socket_path(), sits beside the
control socket (gpty.sock → gpty-events.sock). It does not honor
GPTY_SOCKET or GPTY_SECRET, is not an MCP tool, and cannot create
panes, inject input, or shut down gpty.
It registers three methods:
| Method | Auth | Purpose |
|---|---|---|
ompEvent |
GPTY_EVENT_CAPABILITY |
Submit an event for a terminal |
subscribe |
none | Open a read-only subscription (max 64) |
eventsPoll |
none | Drain events for a subscription id |
Only submission is capability-gated. subscribe/eventsPoll are open to
any same-UID process: the event channel is not confidential from a peer
that already runs as this user, and the documented threat model accepts
that — but do not treat “only ompEvent” as the surface, and do not put
anything on this channel that must stay private from same-UID code.
Each PTY receives ephemeral GPTY_TERMINAL_SESSION_ID,
GPTY_EVENT_CAPABILITY, GPTY_EVENT_SOCKET, and GPTY_EVENT_PROTOCOL=1
at spawn. A leaked capability is scoped to that terminal.
Platform: the listener runs on Linux, macOS and Windows — it
serves gpty-events.sock on Unix and \\.\pipe\gpty-events on Windows,
and GPTY_EVENT_* is injected at spawn on every platform. The shipped
@gpty/omp-events extension still speaks only to a Unix socket, so on
Windows Reasoning stays dormant until an adapter with a named-pipe
transport exists. Control IPC is unaffected on either platform.
See crates/gpty-gdext/src/omp_events.rs and
extensions/gpty-omp-events/.
Methods
| Method | Params | Result |
|---|---|---|
newPane |
type, command?, split?, title?, focus?, tags? |
{pane_id, label, type} |
listPanes |
— | {panes: [{id, label, type, title, col, row, tags, …}], count} |
killPane |
pane_id |
{success: true} |
focusPane |
pane_id |
{success: true} |
inject |
pane_id, text |
{success: true} |
paneRead |
pane_id, lines? (1–2000) |
{text} |
paneStatus |
pane_id |
{pid, running, exit_code, idle_ms} |
paneRun |
command |
{pane_id, label, type} (exit code via paneStatus) |
paneWait |
pane_id, pattern, timeout_ms? (100–60000) |
{matched, line?} / {matched: false, timed_out} — response held server-side until match or deadline |
broadcast |
tags, text |
{success, count} |
layoutSave |
name |
{success, name} |
layoutLoad |
name |
{success} |
layoutList |
— | {layouts: [string]} |
conceptList |
— | {concepts: [{name, enabled, trigger, actions, …}]} |
conceptToggle |
name |
{success, name} |
version |
— | {version, protocol} |
shutdown |
— | {success: true} |
The event socket additionally serves subscribe (→ {subscription_id}) and
eventsPoll (→ {events}), draining bounded concept/pane events.
Key Dependencies
| Crate | Version | Role |
|---|---|---|
gpty-core |
path | Engine types (PaneType, TerminalConfig) |
serde |
1 | Serialization framework |
serde_json |
1 | JSON encoding |
tokio |
1 | Async I/O + sync primitives |
thiserror |
2 | Error derive macro |
parking_lot |
0.12 | Fast Mutex (no poisoning) |
log |
0.4 | Log facade |