JSON-RPC 2.0 over a Unix domain socket. Used by the seqforge CLI and
by external agents to drive a running SeqForge GUI process. Stage 2.5d.
- Protocol: JSON-RPC 2.0, newline-delimited (one request per line, one response per line).
- Transport: Unix domain socket. Path is published in the
SEQFORGE_SOCKETenvironment variable (set in the SeqForge embedded terminal automatically; agents launched outside the GUI must read it from a wrapper or be told). - Path format:
/tmp/seqforge-<pid>.sock. One socket per GUI process; the path is removed when the GUI exits. - Concurrency: each accepted connection runs in its own thread. Requests on a single connection are processed in order; the GUI's applier serializes everything to one mutation site so cross-connection request ordering is well-defined.
Request envelope:
{"jsonrpc":"2.0","id":<any>,"method":"<method-name>","params":{...}}Response envelope (success):
{"jsonrpc":"2.0","id":<echoed>,"result":{"kind":"<variant>",...}}Response envelope (error):
{"jsonrpc":"2.0","id":<echoed>,"error":{"code":<int>,"message":"..."}}id is echoed verbatim and supports any JSON value (numeric, string,
null). Notifications (no id) are not supported — every request
gets a reply.
Open a file in the viewer. Workspace-scoped: creates a new View and
its Buffer (or switches to an existing tab if the file is already
open by path). Replaces any Tab::Welcome placeholder.
{"method":"open","params":{"path":"/abs/path/plasmid.gb"}}Response: {"kind":"ok"}.
Close the active view. Drops the underlying buffer if no other view references it. ⌘W equivalent.
{"method":"close","params":{}}Response: {"kind":"ok"}. Errors with NoActiveView if nothing's open.
Navigate to a 1-based sequence position. Scrolls + places a cursor.
{"method":"goto","params":{"position":1234}}
{"method":"goto","params":{"position":1234,"view":17}} // explicit targetResponse: {"kind":"navigated","position":1234}. Errors with
OutOfRange if position is 0 or exceeds the sequence length.
IUPAC pattern search on both strands; the first hit becomes the selection.
{"method":"find","params":{"pattern":"GAATTC","mismatches":0}}
{"method":"find","params":{"pattern":"GAATTC","view":17}} // explicit targetResponse: {"kind":"search_results","count":N,"hits":[{...}]}.
Empty pattern clears results.
Show restriction sites for the named enzymes. Names matched case-insensitively against the bundled enzyme library; unknown names are silently skipped.
{"method":"enzymes","params":{"enzymes":["EcoRI","BamHI"]}}
{"method":"enzymes","params":{"enzymes":["EcoRI"],"view":17}} // explicit targetResponse: {"kind":"cut_sites","count":N,"sites":[{...}]}. Empty
enzymes clears the cut-site overlay.
Document-scoped methods carry a flattened target: two optional,
mutually exclusive params, view and path. They are flattened into the
params object, so they sit alongside the method's own fields rather than
nested.
| params | meaning |
|---|---|
| neither | the workspace's currently active view (workspace.active_view) |
"view": <ViewId> |
that view explicitly |
"path": "<file>" |
open that file and operate on it |
{"method":"find","params":{"pattern":"GAATTC"}} // active view
{"method":"find","params":{"pattern":"GAATTC","view":17}} // explicit view
{"method":"find","params":{"pattern":"GAATTC","path":"p.gb"}} // a fileAn omitted target serializes to nothing at all, so a request written against the pre-target protocol parses unchanged.
viewon a closed view returnsViewNotFound(-32000, messageview ViewId(N) not found).viewandpathtogether is rejected. The CLI's argument parser catches it first (--inconflicts with--view); a caller that speaks JSON-RPC directly gets aDispatchErrorfrom the target resolver.pathover the socket is refused.--inmeans headless: it resolves in the calling process against a workspace that lives for one request, which is what the CLI already does (a path target never reaches the socket). Apatharriving at a running session is therefore a category error — it asks the session to act on a document it does not have.openthe file first, then target the resulting view.pathon a write method returnsUnimplemented— writing through a file target is not yet supported, and the file is left untouched.
A request changes what is on screen exactly when its target resolves to a live
view — the active one, or the view named. That is a property of the target,
not of the verb. A path target never has a view, so it never updates anything;
that is the whole of the rule.
Which document a request names is part of what the request means, not
transport configuration — unlike SEQFORGE_SOCKET, which only decides
where the request is delivered. That is why it lives in params.
FeatureId and PrimerId are session-scoped (decision 12). Ids
returned by a path-targeted request name entries in a workspace that
exists only for that one resolution, so they cannot address a document in
the running session — and vice versa. An agent that reads with path and
then writes with view must re-read ids from the session it is writing
to. This is inherent to ids being minted at load, not a gap.
Agents that operate across multiple open files should:
- Track view ids returned from prior interactions, or enumerate with
buffers. - Pass
view: <id>explicitly to avoid races against user tab switches. - Be prepared to handle
ViewNotFoundand re-enumerate.
There is no pane targeting. After Stage 2.5c/e, panes are a layout
concept owned by egui_dock (split-view tab groups in the dock tree),
not addressable identity. The set of open views is the source of truth
for "what files are open"; how the user has arranged them spatially is
not part of the protocol surface.
Standard JSON-RPC codes plus one app-specific:
| Code | Source | Meaning |
|---|---|---|
-32700 |
Parse | Body wasn't valid JSON. |
-32600 |
Invalid | Couldn't deliver to the running viewer. |
-32601 |
Method | Unknown method name. |
-32602 |
Params | Params didn't deserialize into the variant. |
-32000 |
App | DispatchError from seqforge_core::dispatch. |
DispatchError variants surfaced under -32000:
NoActiveView— request needs a view, none is active.ViewNotFound(ViewId(N))— explicitviewtarget doesn't exist.OutOfRange { position, seq_len }— forgoto.PoisonedLock— buffer'sRwLockpoisoned (panic in a previous writer; should never happen in single-threaded apply path).BioError(msg)— load / search / cut-site computation failed.Unimplemented(name)— placeholder for future variants.
Each socket connection waits up to 5 seconds for the GUI's applier to process a request. If the GUI is busy beyond that (heavy paint, modal dialog), the client gets:
{"error":{"code":-32000,"message":"viewer did not respond within timeout"}}The request may still complete inside the GUI; clients should not retry a non-idempotent request after a timeout.
The socket is a local control plane, not a network endpoint.
- The socket path is in
/tmp/seqforge-<pid>.sock. On a multi-user Unix host, anyone with read access to/tmpcan see the path; access is gated by filesystem permissions on the socket file itself, which defaults to the owner's umask (typicallysrwxr-xr-x, so write access is owner-only on a normal setup). - A connecting process is implicitly trusted: it can
openarbitrary files (subject to GUI process's filesystem access), trigger arbitrary searches, and read sequence data. Any process running as the same user can do this. - The protocol does not authenticate. There is no shared secret, no capability handshake. Adding one would block agent interoperability for the MVP and offers little real protection on a single-user dev host (the attacker can ptrace the GUI anyway).
- The socket exposes no shell escape: methods take typed params parsed via serde; method dispatch is a closed-enum match in Rust. Adversarial JSON cannot reach arbitrary file paths, exec syscalls, or untyped fields.
Open { path }does not validatepath. A malicious agent could load a 10 GB file and hang the GUI. This is a denial-of-service surface, not a privilege-escalation one — the attacker already had read access to the file.
If/when SeqForge ships a multi-user or networked variant, this
threat model needs revisiting: capability tokens, per-method
allow/deny, sandboxed buffer loaders, rate limiting on find /
enzymes for sequences over some threshold.
The bundled seqforge CLI is the canonical client.
$ seqforge open /path/to/plasmid.gb
$ seqforge goto 1234
$ seqforge find GAATTC
$ seqforge find GAATTC --mismatches 1
$ seqforge enzymes EcoRI BamHI
$ seqforge closeDocument targeting:
$ seqforge goto 1234 --view 17 # a tab in the running viewer
$ seqforge find GAATTC --view 17
$ seqforge find GAATTC --in plasmid.gb # a file — no socket, no viewerRouting follows the document. A request naming a path runs in the
calling process against an ephemeral workspace; anything else is
forwarded over the socket. So info, digest, orfs, translate,
primers, and any read verb given --in need no viewer running, while
the same verb without a target requires one.