zyris-terminal
Register a shell addon, choose exec or interactive PTYs, interpret output and manage session lifetimes.
zyris-terminal provides PtyTerminal, implementing terminal v3. It supports two different
workflows: one-shot commands and interactive terminal sessions. It does not require a graphical desktop.
Dependencies and registration
[dependencies]
zyris = { git = "https://github.com/attacca-cc/zyris-protocol", features = ["caps"] }
zyris-terminal = { git = "https://github.com/attacca-cc/zyris-protocol" }use std::time::Duration;
use zyris::caps::TerminalServer;
use zyris::{Node, NodeKind};
use zyris_terminal::PtyTerminal;
fn terminal_node() -> zyris::Result<Node> {
let terminal = PtyTerminal::rooted("/srv/work")
.with_idle_timeout(Duration::from_secs(600));
Node::builder()
.name("shell")
.kind(NodeKind::Service)
.capability(TerminalServer(terminal))
.build()
}Add a TLS provider for wss:// and use Connecting to bring the node online.
Async calls need an application Tokio runtime. The idle sweeper starts on the first PTY open,
not in the constructor.
Pick the right workflow
| Workflow | Tools | Use when |
|---|---|---|
| One-shot | exec | A command finishes and you want stdout, stderr and an exit status. |
| Interactive, unary | open → read / screen / write → close | A shell, REPL or TUI must remain alive between calls. |
| Interactive, streaming | open_stream + write / resize / close | A typed client can receive raw terminal bytes continuously. |
One-shot commands without quoting problems
use std::time::Duration;
use zyris::caps::{Terminal, TerminalClient};
use zyris::Connection;
async fn git_status(conn: &Connection) -> zyris::Result<()> {
let terminal: TerminalClient = conn.wait_capability(Duration::from_secs(5)).await?;
let result = terminal.exec(
None,
Some(vec!["git".into(), "status".into(), "--short".into()]),
None,
Some(10_000),
None,
None,
None,
).await?;
println!("exit={} timed_out={}", result.exit_code, result.timed_out);
println!("{}", result.stdout);
Ok(())
}exec argument | Meaning |
|---|---|
command | A shell string. Unix defaults to /bin/sh -c; Windows uses cmd /C. |
argv | A nonempty program/argument vector, with no shell interpretation. |
cwd | Optional working directory, relative to the addon root or an absolute path. |
timeout_ms | Execution deadline in milliseconds. Set it explicitly for predictable behavior. |
stdin | Optional text written to the child; stdin is then closed. |
env | Entries override the host process environment; this is not a clean environment. |
shell | Unix shell override in command mode. Ignored in Windows command mode. |
Supply exactly one of command and argv. Prefer argv unless you need pipes, redirects or other
shell syntax. For a PowerShell script, use argv = ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "-"] and put the script in stdin.
Timeout kills the process tree and returns partial output with timed_out = true and exit code -1.
Stdout and stderr are each capped at 1 MiB; check their separate truncation flags. Surplus output
is drained and discarded so a full pipe does not leave the child blocked.
Interactive sessions
use std::time::Duration;
use zyris::caps::{Settle, Terminal, TerminalClient};
use zyris::Connection;
async fn inspect_shell(conn: &Connection) -> zyris::Result<()> {
let terminal: TerminalClient = conn.wait_capability(Duration::from_secs(5)).await?;
let opened = terminal.open(None, 100, 30).await?;
let result = terminal.screen(
opened.pty.clone(),
None,
Some(Settle { quiet_ms: 0, timeout_ms: 1000 }),
).await;
let cleanup = terminal.close(opened.pty).await;
let screen = result?;
cleanup?;
println!("{}", screen.lines.join("\n"));
Ok(())
}open takes an optional shell, columns and rows, returning PtyOpened { pty }. The default shell
uses the host's SHELL on Unix, falling back to /bin/sh; Windows uses its platform default.
read(pty, input, settle)writes optional text first, then returns new sanitized text. Control sequences are stripped except newlines and tabs.screen(pty, input, settle)returns the current terminal grid, cursor and shell exit code. It does not consumereadoutput. Use it for full-screen programs.write(pty, data: Blob)sends terminal input bytes.resize(pty, cols, rows)updates the PTY.open_streamreturns a head with the PTY handle and rawPtyChunkitems, including escape codes. An agent's unary caller should useopenplusreadinstead.close(pty)releases the session. Close explicitly even if you drop a streaming subscriber.
Interpret output correctly
| Field | Meaning |
|---|---|
PtyRead.more | More bytes remain buffered; another read can retrieve them. |
PtyRead.dropped | Bytes were lost to ring-buffer overflow; another read cannot recover them. |
PtyRead.exited | None while the shell lives; the exit code after it exits. |
PtyScreen.lines | The rendered grid, not an append-only output log. |
Settle defaults to 300ms of quiet with a 5000ms wait ceiling. Hitting that ceiling does not
prove a command finished. An echoed input line or a quiet output interval is not a completion signal;
inspect program output and shell state. quiet_ms = 0 snapshots immediately.
Limits and isolation
There are at most eight simultaneous PTYs; the next open fails with too_many_ptys. A session idle
for ten minutes is eligible for sweeping by default. Disconnect does not immediately close all
sessions; the idle sweep is the fallback, so do not promise instantaneous cleanup on wire loss.
rooted chooses a working directory, not a sandbox. Commands retain the host user's privileges
and inherited environment. Use OS-level isolation and application authorization for untrusted callers.
Restricting cwd alone cannot restrict what a shell or executable accesses.