RTerm Gateway — Remote Control Skill
RTerm can run as a headless service. When its gateway is enabled, it opens a
WebSocket JSON-RPC endpoint. Any program or agent that can open a WebSocket can
drive the full RTerm feature set — no UI, no human.
Use this skill to:
- Run AI agent tasks on the gateway (
agent:startTask / agent:startTaskAsync).
- Open and control terminals on SSH / WinRM / Serial / local targets and run commands.
- Transfer and edit files on any connected host.
- Manage settings, command policy, skills, memory, and scheduled automation.
- Orchestrate fleets and scheduled jobs (cron) completely headlessly.
1. How the gateway works (mental model)
Your agent / program ──WebSocket JSON-RPC──> RTerm Gateway (ws://host:17888)
│
┌────────────────────────┼─────────────────────────┐
│ │ │
AgentService TerminalService AutomationManager
(run AI tasks) (SSH/WinRM/Serial/local) (playbooks, cron, change MOP)
│ │ │
LLM + tools run commands, files scheduler, ledgers
- Requests are JSON-RPC:
{ "id": "1", "method": "<name>", "params": { ... } }.
- Responses echo the
id wrapped in a gateway:response envelope:
- success →
{ "type": "gateway:response", "id": "1", "ok": true, "result": { ... } }
- error →
{ "type": "gateway:response", "id": "1", "ok": false, "error": { "code", "message" } }
- Events stream to you asynchronously as
{ "type": "gateway:event" | "gateway:raw" | "gateway:ui-update", "channel"?, "payload": ... }.
Default endpoint: ws://<host>:17888 (default port 17888, configurable).
HTTP on the same port (v3.0.2+): the gateway also serves plain HTTP on that port —
GET /dashboard renders the live unified dashboard (WS-push updates in the browser,
falls back to polling GET /dashboard/json, which returns the raw state). Same auth
model: loopback open, remote needs a token (Authorization: Bearer, x-access-token,
or ?access_token=).
Auth
- Token auth via
Authorization: Bearer <token> header (or a token provided per deployment).
- Localhost bypass: connections from
127.0.0.1 / ::1 skip the token by default
(allowLocalhostWithoutToken: true). If you are on the same machine as RTerm, you usually
need no token.
- Optional IP allow-list (CIDR) may restrict which hosts can connect.
If a connection is rejected, you'll get a close frame with a reason — treat that as an
auth/IP problem, not a protocol problem.
Bootstrapping an access token (no CLI)
There is no gybackend create-token command — token creation normally happens through
the gateway's own RPC/UI, which is a chicken-and-egg problem for the first token on a
headless daemon. The store is a simple JSON file at {DATA_DIR}/access-tokens.json
({schemaVersion:1, tokens:[…]}). Each record holds a scrypt hash, never the plaintext:
token = "gys_at_" + base64url(randomBytes(24)) # the plaintext you hand out (shown once)
tokenSalt = base64(randomBytes(16)) # per-token salt
tokenHash = base64(scrypt(token, salt, 32)) # stored; verified with timingSafeEqual
record = { id: "atk_"+base64url(randomBytes(12)), name, createdAt, tokenSalt, tokenHash }
To mint the first token, write a record matching that exact algorithm into the file (mode
0600). The running backend re-reads the file on every verify(), so no restart is
needed. Verify over the non-loopback interface: valid token → 200, no/bad token → 401.
Tokens are accepted as Authorization: Bearer <token>, x-access-token: <token>, or
?access_token=<token> (query). To revoke, delete the record by id.
1b. Standalone neuralos daemon vs. the RTerm desktop app
The gateway is served by gybackend — the headless engine. There are two ways it runs:
- RTerm.app (desktop): embeds gybackend and spawns it in-process. Backend lives only
while the app is open. Its data dir is
~/Library/Application Support/rterm.
neuralos (npm package, npm i -g neuralos): the same gybackend engine, packaged
standalone with no GUI. Runs as an always-on/headless service (e.g. a macOS
LaunchAgent with RunAtLoad+KeepAlive). Its data dir is ~/.gybackend-data
(override with GYBACKEND_DATA_DIR).
They are separate profiles. The app and the daemon each keep their own
settings.json, access-tokens.json, gyshell-*.sqlite, skills/, memory.md.
Installing neuralos gives you a fresh, empty profile — none of the GUI's
connections/playbooks/schedules. To share state you either copy the data over or point one
at the other (see §10a). The GUI's backend-settings file is gyshell-backend-settings.json;
the daemon's equivalent is settings.json — same schema, so it maps 1:1. Preserve
the daemon's own gateway block when copying (the GUI may have gateway.ws.access: "disabled", which would shut off the very gateway you're trying to reach).
Feature parity: every backend capability works in neuralos — SSH, WinRM, serial,
local PTY, playbooks, MOP changes, triggers, vault, recording, observability (APM/DEM/
cloud/on-call/GitOps/cost), the full agent runtime, and all bundled plugins. The only thing
absent is the desktop UI (which is a client of the gateway, not a backend feature).
Gotcha — neuralos ships without ssh2 and serialport. The npm package bundles a
single bin/gybackend.cjs with no node_modules; SSH and serial are lazy, guarded
external requires. The daemon starts fine, but opening an SSH tab throws
ssh2 is not available in this build (… WinRM/serial/local terminals still work), and a
serial tab throws "install serialport". Fix once:
cd "$(npm config get prefix)/lib/node_modules/neuralos" && npm install ssh2 serialport --no-save --omit=dev
WinRM (pure HTTP) and local PTY are unaffected.
2. The 30-second start
- Verify the gateway is up and reachable:
node scripts/rterm-gw.mjs --url ws://127.0.0.1:17888 ping
# -> { "pong": true, "ts": ... }
- List terminals / sessions:
node scripts/rterm-gw.mjs terminal-list
node scripts/rterm-gw.mjs session-list
- Run a command on a saved WinRM/SSH connection (the headline use case):
node scripts/rterm-gw.mjs exec-winrm \
--name "AWS-Windows-Server-1" \
--command "powershell -NoProfile -Command \"Update-MpSignature; (Get-MpComputerStatus).AntispywareSignatureVersion\""
The bundled helper scripts/rterm-gw.mjs wraps the whole protocol (connect, RPC, events,
waits) into subcommands. Use it directly or read it as a reference client.
3. Choosing the right method (decision guide)
| I want to… |
Use |
| Have the AI agent figure out & run a multi-step task |
agent:startTask (block) / agent:startTaskAsync (fire-and-forget) |
| Run one command on an SSH/WinRM/local/Serial host |
terminal:createTab → terminal:write → terminal:getBufferDelta (PTY) or route via the agent for WinRM |
| WinRM command/response execution |
Prefer the agent path (agent:startTask) — WinRM has no live stdin; the agent's exec_command uses the structured executeCommand path that returns output. |
| Read/write/transfer files on a host |
filesystem:* |
| Manage saved connections, settings, policy |
settings:*, settings:addCommandPolicyRule, agentSettings:* |
| Create/modify scheduled cron jobs |
settings:set (automation section) — scheduler runs them headless |
| Orchestrate a fleet or a playbook |
agent:startTask ("run the X playbook on group Y") |
| Watch live progress |
subscribe to events (gateway:event / gateway:raw) |
Key gotcha — WinRM is command/response, not a PTY. terminal:write to a WinRM tab is a
no-op (returns ok but runs nothing). For WinRM, drive commands through the agent
(agent:startTask), whose tools route through the structured executeCommand path and
return real output. SSH/local PTY tabs work fine with terminal:write + getBufferDelta.
4. RPC method reference (123 methods: 70 core + 52 observability:* + gateway:describe)
v3.0.0 — ask the gateway instead of reading this list. Call gateway:describe (params {}, or {category:"observability"} / {prefix:"settings:"}) to get the full, live method registry ({version, count, categories, methods:[{name,category,description,since,params}]}) straight from the source of truth. From the agent, use the list_gateway_methods tool (same category/prefix filters). The registry below is a static snapshot — gateway:describe is always current.
Params are passed as a JSON object under params. … = see source for full shape.
Gateway / session lifecycle
| Method |
Params |
Returns |
Notes |
gateway:ping |
— |
{pong:true, ts} |
liveness |
gateway:isSameMachine |
— |
{sameMachine} |
true if client is co-located |
gateway:createSession |
— |
{sessionId} |
new agent/chat session |
gateway:describe (v3.0.0) |
{category?, prefix?} |
{version, count, total, categories, methods} |
self-discovery — the live method registry |
session:list |
— |
{sessions:[…]} |
session summaries |
session:get |
{sessionId} |
{session} |
one session snapshot |
Agent (AI task execution)
| Method |
Params |
Returns |
Notes |
agent:startTask |
{sessionId, userInput, options?} |
{ok:true} |
blocks until the task completes; userInput = string or {text, images?} |
agent:startTaskAsync |
{sessionId, userInput, options?} |
{ok:true} |
fire-and-forget; watch events for progress |
agent:stopTask |
{sessionId} |
{ok:true} |
abort a running task |
agent:replyMessage |
{messageId, payload} |
… |
answer an agent prompt |
agent:replyCommandApproval |
{approvalId, decision} |
… |
decision: "allow" or "deny" |
agent:getUiMessages |
{sessionId} |
{messages:[…]} |
transcript for a session |
agent:getAllChatHistory |
— |
[…] |
all chat history |
agent:exportHistory |
{sessionId, mode?} |
… |
mode: "simple" or "detailed" |
agent:loadChatSession |
{id} |
… |
load a session into the agent |
agent:renameSession |
{sessionId, title} |
… |
rename |
agent:deleteChatSession |
{sessionId} |
{ok:true} |
delete one |
agent:deleteChatSessions |
{sessionIds:[…]} |
… |
delete many |
agent:branchFromMessage |
{sessionId, messageId} |
… |
branch a session at a message |
agent:rollbackToMessage |
{sessionId, messageId} |
… |
roll a session back |
Terminals (SSH / WinRM / Serial / local)
| Method |
Params |
Returns |
Notes |
terminal:list |
— |
{terminals:[…]} |
all tabs with runtimeState |
terminal:createTab |
{config} |
{id} |
config = a TerminalConfig (see §5) |
terminal:write |
{terminalId, data} |
{ok:true} |
write to PTY (SSH/local); no-op for WinRM |
terminal:writePaths |
{terminalId, …} |
… |
write file paths (drop) |
terminal:resize |
{terminalId, cols, rows} |
… |
resize PTY |
terminal:kill |
{terminalId} |
… |
close tab |
terminal:reconnect |
{terminalId} |
… |
reconnect an exited tab |
terminal:setSelection |
{terminalId, selectionText} |
… |
set selection text |
terminal:getBufferDelta |
{terminalId, fromOffset} |
{…} |
read accumulated output (PTY) |
terminal:generateCommandDraft |
{terminalId, …} |
… |
AI command draft |
Filesystem (per terminal/host)
| Method |
Params |
Notes |
filesystem:list |
{terminalId, dirPath?} |
list a directory |
filesystem:readTextFile |
{terminalId, filePath} |
read a text file |
filesystem:readFileBase64 |
{terminalId, filePath} |
read binary as base64 |
filesystem:writeTextFile |
{terminalId, filePath, content} |
write text |
filesystem:writeFileBase64 |
{terminalId, filePath, contentBase64} |
write binary |
filesystem:createDirectory |
{terminalId, dirPath} |
mkdir |
filesystem:createFile |
{terminalId, filePath} |
touch |
filesystem:deletePath |
{terminalId, targetPath} |
delete |
filesystem:renamePath |
{terminalId, oldPath, newPath} |
rename/move |
filesystem:transferEntries |
{…} |
multi-entry transfer plan |
filesystem:startTransfer |
{…} |
start an upload/download |
filesystem:getTransfer |
{transferId} |
transfer status |
filesystem:listTransfers |
— |
list transfers |
filesystem:cancelTransfer |
{transferId} |
cancel |
filesystem:cancelTransferTask |
{transferId} |
cancel a task |
Settings, policy, skills, memory, models
| Method |
Params |
Notes |
settings:get |
— |
full settings (incl. connections, automation) |
settings:set |
{settings} |
patch settings (e.g. add scheduled task) |
settings:getCommandPolicyLists |
— |
allow/ask/deny lists |
settings:addCommandPolicyRule |
{list, rule} |
add rule to allowlist/asklist/denylist |
settings:deleteCommandPolicyRule |
{list, rule} |
remove rule |
agentSettings:get |
— |
agent settings |
agentSettings:saveCurrent |
{…} |
save a profile slot |
agentSettings:apply |
{…} |
apply a profile |
agentSettings:overwrite |
{…} |
overwrite |
agentSettings:delete |
{…} |
delete profile |
skills:getAll / skills:list / skills:getEnabled |
— |
list skills |
skills:setEnabled |
{name, enabled} |
toggle a skill |
skills:create / skills:delete / skills:reload |
{…} |
manage skills |
memory:get |
— |
global memory |
memory:setContent |
{content} |
set memory |
models:getProfiles |
— |
model profiles |
models:setActiveProfile |
{profileId} |
switch model |
models:probe |
{…} |
probe a model |
tools:getMcp / tools:reloadMcp / tools:setMcpEnabled |
{…} |
MCP tools |
tools:getBuiltIn / tools:setBuiltInEnabled |
{name, enabled} |
built-in tools |
system:saveImageAttachment |
{…} |
attach an image |
Settings blocks (v2.9.6+, schema v5) — persisted + live-reloaded on settings:set (no restart), all editable in the desktop Settings UI: cost.modelPrices+cost.budgets (Settings → AI Cost), alerts.channels[] (Settings → Alerts), oncall.pagingChannels[] (Settings → On-Call), cloud.accounts[] (Settings → Cloud), agentspan.serverUrl+agentspan.authSecretRef (Settings → AgentSpan). Secrets are always secretRef pointers into the vault, never inline. Example: settings:set {"cost":{"modelPrices":{"moonshotai/kimi-k3":{"promptPer1M":3,"completionPer1M":15},"default":{"promptPer1M":0.7,"completionPer1M":2.2}},"budgets":[{"id":"monthly-all","model":"*","period":"monthly","capUsd":2500,"warnAt":0.8,"overAction":"throttle"}]}}.
Observability / SRE (v2.0.0–v2.3.1) — driven via the agent
The observability modules are wired into the backend and driven through agent:startTask (the agent reads/writes them via its built-in tools). Key capabilities to ask for:
| Area |
Example userInput |
| Unified dashboard |
"Build the dashboard:state — fleet health, SLO, uptime, incidents, APM, DEM, capacity" |
| SRE metrics |
"Report golden signals + capacity forecast for all hosts; days-to-disk-full" |
| Uptime watchdog |
"Add an uptime watchdog for web-01 (tcp 443) and report its state" |
| SLO |
"Create an SLO api-uptime 99.9% over 30d and evaluate burn rate" |
| Incidents |
"List open incidents and generate the postmortem for the latest one" |
| APM (OTLP) |
"Ingest these OTLP spans and report the slowest traces + bottleneck services" |
| DEM (RUM) |
"Report p75 LCP/INP and error rate per page; which pages are poor on Core Web Vitals?" |
| k8s/cloud |
"Collect cluster health (pods, restarts, node readiness, cpu/mem % of limit)" |
| ETW (Windows) |
"Run a network ETW trace on AWS-Windows-Server-1 for 60s and summarize connections" |
| Predictive |
"Detect anomalies in cpu/disk for all hosts and any forecast breaches within 7 days" |
| Behavioral |
"Flag any run-spikes, token-blowouts, error-spikes, or unusual models vs the baseline" |
| Evals |
"Run the embedded eval harness on the golden set and report accuracy/tool/safety/replay %" |
| Notify (Slack/Teams/SMTP/Telegram) |
"Wire a Slack alert channel (webhook …) and fire a test alert" |
| dagu workflows (v2.4.0+) |
"Compile + run this dagu YAML workflow (paste YAML) using daguParser, show the DAG waves, and report per-step results" |
| Browser dashboard (v3.0.2+) |
"Open http://localhost:17888/dashboard — the live unified dashboard (WS-push updates, /dashboard/json fallback) is served on the same port as the gateway" |
| AWS APerf deep-dive (v2.6.0+) |
"Run an APerf performance deep-dive on web-01 — deploy aperf, record CPU/mem/disk/PMU/processes/hotspot for 60s, parse the findings, and report the top issues" |
| Plugin system (v2.5.0+) |
"List installed plugins and their tools/triggers/panels" |
| Patch management (plugin) |
"Check patch status on web-01, build a patch plan for the security patches, and submit it for approval" |
| Request router (plugin) |
"Submit a request to restart nginx on web-01 with justification 'planned maintenance', then list pending requests" |
| SOP assistant (plugin) |
"Search the SOP library for 'database failover' and show me the steps; then execute the restart-service SOP on web-01 with service=nginx" |
| IAM connector (plugin) |
"Review all users on web-01 and identify privileged accounts; what groups is john in?" |
| Web intelligence (v3.0.9+) |
"Search the web for 'Cisco BGP-5-ADJCHANGE IOS XE 17.18 known issue' and cite sources; then watch the Cisco advisory page for changes and propose a change if it updates" |
| FraudOps (plugin) |
"Check the fraud pipeline status (Flink/NATS/Kafka health) and summarize recent fraud decisions" |
| Netdata integration (plugin) |
"Correlate this Netdata alert with RTerm's metrics and incidents for RCA" |
| Monitor diagnostics (v2.7.6+) |
"Run monitor status diagnostics — why aren't stats displaying for terminal X? Report publisher/session/inFlight/connected/last-collect per terminal" |
| AGT policy engine (v2.7.7+) |
"Evaluate the action 'restart nginx' on target 'prod-web-01' against the governance policy — allow, deny, or escalate?" |
| Review model / maker-checker (v2.7.8+) |
"Review this action with the checker model: type=restart target=prod-web-01 command='systemctl restart nginx' — verify correctness, completeness, safety, compliance, accuracy" |
The observability ledgers feed the unified dashboard and are driven by the agent's
built-in tools — no separate RPC methods are needed beyond agent:startTask /
agent:getUiMessages for these areas.
Plugins (v2.5.0+)
RTerm ships with 6 official plugins (auto-discovered on startup) plus any user plugins
in ~/.gybackend-data/plugins. Plugin tools are called the same way as built-in tools —
through agent:startTask.
| Plugin |
Agent tools |
Use for |
| patch-manager |
patch_status, patch_plan, patch_apply |
Autonomous patch management across hosts (yum/apt/Windows Update) with MOP approval + compliance dashboard |
| request-router |
submit_request, approve_request, list_requests, request_status |
Automated request handling — classify risk (low/med/high), route (auto-approve/queue/MOP), execute, audit |
| sop-assistant |
sop_search, sop_get, sop_execute, iam_lookup |
SOP retrieval + step-by-step guided execution. 8 built-in SOPs (restart-service, disk-cleanup, reset-password, database-failover, ssl-cert-renewal, user-offboarding, backup-restore, incident-response) + 4 IAM policies |
| iam-connector |
iam_user_info, iam_user_groups, iam_disable_user, iam_access_review |
IAM integration — user/group management, privileged access identification, access review (Linux id/groups/usermod + Windows Get-LocalUser) |
| fraudops |
fraudops_pipeline_status, fraudops_str_assign, fraudops_str_status, fraudops_decision_summary |
FraudOps operational layer — Flink/NATS/Kafka health, STR workflow (7-day CBN deadline), decision summary |
| netdata-rterm |
netdata_alert_summary, netdata_correlate |
Netdata Cloud webhook ingestion + correlation with RTerm metrics/incidents for RCA |
| agentspan-bridge (v2.9.9+) |
agentspan_health, agentspan_run, agentspan_status, agentspan_approve, agentspan_list, agentspan_stop |
Durable, crash-resilient agent execution on an AgentSpan (Netflix Conductor) server — runs resume from the last completed step; plan-execute determinism; Kafka/SQS/AMQP event triggers. Configure in Settings → AgentSpan (agentspan.serverUrl, default http://localhost:6767, + optional agentspan.authSecretRef). See the agentspan skill for the standalone SDK/CLI. |
| synapse-bridge (v3.1.4+) |
synapse_health, synapse_discover, synapse_dispatch, synapse_register, synapse_agents_summary |
Synapse mesh interop — discover live Synapse agents, dispatch tasks to mesh.agent.{id}.inbox (durable), and register RTerm as a mesh agent (bidirectional). Speaks Synapse protocol v0.3.0 over a shared NATS server (uses the v3.1.2 auth/request-reply/JetStream transport). Config settings.synapse (url/servers/prefix/agentId/auth). See the synapse skill. |
| numbat-bridge (v3.1.4+) |
numbat_health, numbat_deploy, numbat_ingest, numbat_findings_summary |
Numbat (endpoint AI-agent detection/EDR) integration — Numbat detects, RTerm responds. Deploy numbat to hosts (inventory/scan/install-monitor/install-enforce/status/uninstall via policy-gated exec) and ingest NDJSON findings → fire RTerm triggers to auto-remediate. Config settings.numbat (binaryPath/recordsPath/ingestToken/minSeverity). |
Plugin triggers fire autonomously (e.g., patch_failure → propose-change, fraudops_pipeline_down → run-playbook, netdata_critical_alert → auto-remediation, agentspan_execution_failed → investigate/re-run, synapse_mesh_event → cross-mesh remediation, numbat_finding → isolate/kill/incident).
To install a custom plugin on a headless backend, drop the plugin folder (with a valid
plugin.json + index.mjs) into {GYBACKEND_DATA_DIR}/plugins/ and restart — the
PluginRegistry auto-discovers it.
4b. Observability RPC methods (v2.9.0+) — direct, no agent needed
v2.9.0 added 9 platform capabilities as backend modules; v2.9.2 exposed them as 41 first-class
observability:* RPC methods on the gateway (no agent:startTask round-trip needed). v2.9.3 made
the matching agent tools visible in the Tools section. These are the same methods the agent's
get_metrics/manage_secret/manage_oncall/get_cost/manage_recording/manage_gitops/
manage_playbook_version/get_cloud_inventory/get_live_dashboard tools call.
Call them like any other RPC ({id, method, params}):
{"id":"1","method":"observability:costSummary","params":{"period":"daily"}}
| Area |
Methods |
Notes |
| Metrics / dashboard |
observability:metricsPrometheus, observability:dashboardState, observability:dashboardSummary |
Prometheus exposition text for a scraper; unified dashboard state/summary |
| OTel push |
(via OTEL_EXPORTER_OTLP_ENDPOINT env) |
Pushes OTLP/HTTP JSON to a collector on an interval |
| Secrets vault |
observability:secretsList, secretsSet, secretsDelete, secretsHas |
AES-256-GCM, metadata only — never values. Needs RTERM_SECRETS_MASTER_KEY at boot |
| On-call / escalation |
observability:oncallListPolicies, oncallRegisterPolicy, oncallOpenPages, oncallPage, oncallAck, oncallResolve, oncallTick |
Multi-level escalation policies, ack deadlines, paging |
| AI cost & budgets |
observability:costSummary, costRecord, costCheck, costListBudgets, costSetBudget, costRemoveBudget |
USD spend per model/profile; warn/throttle/deny budgets |
| Session recording |
observability:recordingList, recordingStart, recordingStop, recordingReplay, recordingExportCast, recordingDelete |
asciinema .cast v2 export/import + scrub/replay |
| GitOps |
observability:gitopsExport, gitopsDrift, gitopsInSync, gitopsReconcile |
Desired-state manifest, drift detection, gated reconcile |
| Playbook versioning |
observability:playbookLint, playbookHistory, playbookSave, playbookRollback, playbookDiff |
Version history + static lint (undefined params, dependsOn cycles, missing rollback) |
| Cloud inventory |
observability:cloudSummary, cloudQuery, cloudSync, cloudAddAccount |
Normalized AWS/GCP/Azure instances (inject fetchers) |
| Live dashboard |
observability:liveDashboardState, liveDashboardSubscriberCount, liveDashboardSubscribe |
Push-based multi-client dashboard state; GET /dashboard + GET /dashboard/json (v3.0.2) serve the live page + state on the same port |
| APM ingestion |
observability:apmIngestSpans, apmSummary |
Feed OTLP/HTTP-JSON trace spans → trace store (bottleneck services, slowest traces) |
| DEM ingestion |
observability:demIngestBeacon, demSummary |
Feed Core Web Vitals RUM beacons (page, LCP/INP/CLS/TTFB, JS errors) → per-page p75 + error rate |
| Infra ingestion |
observability:infraCollect, infraClusters, infraUnhealthy |
Collect k8s cluster health (kubectl text or JSON payload) → not-ready, CrashLoopBackOff |
| ETW ingestion |
observability:etwStartTrace, etwStopTrace, etwParse, etwSessions |
Windows ETW diagnostics — logman start/stop commands + parse Get-WinEvent/Get-Counter output |
| Web intelligence (v3.0.9+) |
Plugin tools: webintel_health, web_search, web_fetch, web_crawl, web_research, web_find_similar, web_watch_add, web_watch_list, web_watch_remove |
Local-first web intelligence via the web-intel plugin (wigolo daemon). Multi-engine search, clean-page fetch, site crawl, research (synthesis by RTerm agent — no LLM key), page-watch → webintel_page_changed trigger. Lean by default (WIGOLO_NO_WARMUP=1); ~1.5 GB browser engine + models are opt-in (webIntel.warmupOnInit). |
Example — set a secret then verify it (values never come back):
./scripts/rterm-gw.mjs call observability:secretsSet '{"key":"aws-access-key","value":"AKIA…","labels":{"service":"aws"}}'
./scripts/rterm-gw.mjs call observability:secretsList '{}'
# → [{"key":"aws-access-key","labels":{"service":"aws"},"createdAt":…,"updatedAt":…}] (no "value" field)
Example — check AI spend + gate a run:
./scripts/rterm-gw.mjs call observability:costSummary '{"period":"daily"}'
# → {"totalUsd":3.42, "byModel":[{"model":"gpt-4o","usd":3.42,…}]}
./scripts/rterm-gw.mjs call observability:costCheck '{"model":"gpt-4o"}'
# → {"action":"ok"|"warn"|"throttle"|"deny", "statuses":[…]}
Example — record + export a session:
./scripts/rterm-gw.mjs call observability:recordingStart '{"terminalId":"local-main","title":"deploy"}'
# → {"recordingId":"r-…"}
./scripts/rterm-gw.mjs call observability:recordingExportCast '{"recordingId":"r-…"}' > session.cast
Agent-tool equivalents (when you'd rather let the agent drive): get_metrics, manage_secret,
manage_oncall, get_cost, manage_recording, manage_gitops, manage_playbook_version,
get_cloud_inventory, get_live_dashboard, ingest_apm_spans, get_apm_summary,
ingest_dem_beacon, get_dem_summary, collect_infra, manage_etw. Ask for them in userInput,
e.g. "add this API key to the vault", "show my AI spend today", "page the on-call for the DB
incident", "lint this playbook", "ingest these OTLP spans", "ingest this RUM beacon", "collect the
k8s cluster health", "start an ETW network trace on the Windows host".
5. TerminalConfig shapes (for terminal:createTab)
// SSH
{ "type": "ssh", "id": "t1", "title": "web-01", "cols": 120, "rows": 32,
"host": "10.0.0.5", "port": 22, "username": "deploy",
"password": "…", // or "privateKey" / "privateKeyPath" / "agent"
"algorithmsPreset": "modern|legacy|cisco", "termType": "xterm-256color|vt100" }
// WinRM (command/response — drive commands via the AGENT, not terminal:write)
{ "type": "winrm", "id": "w1", "title": "win-01", "cols": 140, "rows": 40,
"host": "44.197.31.152", "port": 5985, "username": "Administrator",
"password": "…", "transport": "http", "auth": "basic", "domain": "" }
// Serial
{ "type": "serial", "id": "s1", "title": "switch-console", "cols": 120, "rows": 32,
"path": "/dev/ttyUSB0", "baudRate": 9600, "dataBits": 8, "parity": "none",
"stopBits": 1, "flowControl": "none" }
// Local
{ "type": "local", "id": "l1", "title": "local", "cols": 120, "rows": 32,
"cwd": "/work", "shell": "/bin/zsh" }
Saved connections already known to RTerm can be opened by asking the agent to
"open the saved connection named X" (see §6), or by reading settings:get →
connections.{ssh,winrm,serial} and passing the same fields to terminal:createTab.
Gotcha — terminal:createTab does NOT resolve saved-connection names or ids.
It passes config straight through to the terminal service; there is no
savedConnectionName / connectionId lookup. If you pass {type:"ssh", savedConnectionName:"Remote"} the tab opens with host=undefined, username=undefined
and fails with Invalid username (retried in a loop). To open a saved connection over
RPC you must read it from settings:get and expand it into the full inline config
yourself (host/port/username/password/…). The bundled helper scripts/gy-open.mjs
does exactly this (by name or id, across ssh/winrm/serial, incl. proxy/jumpHost) — see §8c.
6. Events (watching progress live)
You do not subscribe explicitly — events stream to every connected client.
| Wire type |
Meaning |
Payload |
gateway:event |
A structured GatewayEvent |
{id, timestamp, type, sessionId?, payload} where type ∈ agent:event | session:update | ui:action | system:notification |
gateway:raw |
Raw channel data |
{channel, payload} — e.g. channel:"terminal:data" carries {terminalId, data, offset} |
gateway:ui-update |
UI action broadcast |
action object |
For agent:startTask, watch for agent:event payloads (tool calls, streamed model text,
completion). The bundled client prints events to stderr so you can observe them.
7. Command policy & autonomy
Every command the agent runs is evaluated against the command policy:
smart — run autonomously (unless explicitly denylisted). Headless-friendly.
standard — asks for approval on unrecognized commands. A remote client must
answer with agent:replyCommandApproval ({approvalId, decision:"allow"|"deny"}).
safe — denies unrecognized commands.
Check the mode with settings:get → commandPolicyMode. For unattended operation, either
use smart mode or pre-allowlist the commands your workflow needs
(settings:addCommandPolicyRule {list:"allowlist", rule:"Update-MpSignature*"}).
7a. Governance, audit & the maker/checker model (v2.7.x)
Audit trail (v2.7.1) — hash-chained, tamper-evident
Every audit-relevant event (agent runs, command evaluations, approvals, MOP changes,
playbook steps, trigger firings, alert ingestions) is appended to a hash-chained audit
ledger — each record carries the SHA-256 hash of the previous one, so any tampering is
detectable via verify(). The evidence sealer computes a Merkle-tree root over the
records → sealed, independently-verifiable evidence bundles (KLA framework domain 11).
Ask the agent: "Show the audit ledger for command X and verify the chain", or
"Seal the audit ledger and produce the evidence bundle for the auditor".
AGT policy engine (v2.7.7) — governance before execution
The AGT policy engine evaluates every consequential action against a YAML policy
before execution. Decisions: allow / deny / escalate (route to approval).
- Glob-style action patterns (
"read" matches "read /etc/passwd"), target wildcards
(prod-*), first-match-wins, case-insensitive.
- Built-in default policy: allow read/status/list; deny delete/drop/format; escalate
restart/patch/deploy on
prod-*; allow them otherwise.
- Drop a custom
policy.yaml in the data dir to override.
Ask the agent: "Evaluate the action 'restart nginx' on target 'prod-web-01' against the
governance policy" — the agent returns the decision + the matched rule + reason.
Review model / maker-checker (v2.7.8) — independent verification
The review model (a second LLM, the "checker") independently verifies the action
model's (the "maker's") output on 5 dimensions: correctness, completeness, safety,
compliance, and accuracy.
- Verdicts:
approved / needs_revision / escalate.
- Modes:
strict (block on any issue), advisory (flag but allow), auto-approve
(skip review for low-risk actions).
- Fast output mode: if no
reviewModelId is set in the profile, reviews are skipped
entirely (zero added latency).
Configure it in the Settings UI (v2.7.9+): the profile has a Review Model dropdown
((None — skip reviews) = fast mode) and a Review Mode dropdown.
Monitor diagnostics (v2.7.6) — "why aren't stats displaying?"
monitorStatus reports exactly why monitor stats aren't displaying per terminal:
publisher wired? session exists? collection stuck in-flight? terminal connected? platform
detected? last-collect time? Diagnoses: terminal_not_connected, no_monitor_session,
collection_stuck_in_flight, never_collected, stale_collection, publisher_not_wired.
Ask the agent: "Run monitor status diagnostics and report any terminals not collecting".
8. Bundled helper: scripts/rterm-gw.mjs
A dependency-light reference client (Node ≥18, uses ws). Subcommands:
# liveness + discovery
node scripts/rterm-gw.mjs ping
node scripts/rterm-gw.mjs terminal-list
node scripts/rterm-gw.mjs session-list
node scripts/rterm-gw.mjs settings-get
# generic RPC (escape hatch — any method)
node scripts/rterm-gw.mjs rpc --method models:getProfiles
# open a saved WinRM/SSH connection and run one command
node scripts/rterm-gw.mjs exec-winrm --name "AWS-Windows-Server-1" --command "<powershell>"
# run an AI agent task (blocking or async)
node scripts/rterm-gw.mjs agent-task --text "Update AV signatures on AWS-Windows-Server-1 and report the version"
node scripts/rterm-gw.mjs agent-task --async --text "Run the Friday cleanup playbook on group prod-web"
# read a file on a connected host
node scripts/rterm-gw.mjs fs-read --terminalId <id> --path C:\Temp\log.txt
Flags: --url (default ws://127.0.0.1:17888), --token (or RTERM_GW_TOKEN), --timeout.
See examples/ for ready-made programs.
8a. Shell one-liners with websocat (no Node, no Python)
You don't need Node or Python — the gateway is plain WebSocket + JSON, so
websocat drives it from any shell. A prebuilt
binary is in DrOlu/agent-tools
(websocat.exe v1.14.1),
or install from your package manager (brew install websocat, cargo install websocat).
One-shot RPC (reconnects each call; good for quick reads — use -n1 = close after one reply):
# ping
echo '{"id":"1","method":"gateway:ping"}' | websocat -n1 ws://127.0.0.1:17888
# -> {"type":"gateway:response","id":"1","ok":true,"result":{"pong":true,"ts":...}}
# list terminals
echo '{"id":"2","method":"terminal:list"}' | websocat -n1 ws://127.0.0.1:17888
With jq for scripting:
echo '{"id":"2","method":"terminal:list"}' \
| websocat -n1 ws://127.0.0.1:17888 \
| jq -r '.result.terminals[] | "\(.title) [\(.type)] \(.runtimeState)"'
Persistent session (required for agent:startTask* and for streaming gateway:events):
websocat ws://127.0.0.1:17888
# then paste JSON-RPC lines; responses + live events arrive on the same socket:
{"id":"1","method":"gateway:createSession"}
{"id":"2","method":"agent:startTaskAsync","params":{"sessionId":"<sid>","userInput":"Update AV signatures on AWS-Windows-Server-1 and report the version"}}
With a token (when not connecting from localhost):
websocat -H="Authorization: Bearer <token>" ws://rterm-host:17888
Note: websocat is line-oriented — perfect for request→response RPC and jq
pipelines. For long agent tasks you must keep the socket open and read the streaming
gateway:event messages yourself (or use the Node/Python client which manages that loop).
8b. Python client (websockets, no Node)
Any Python ≥3.9 agent can drive the gateway with the websockets library
(pip install websockets). This is a minimal, complete client covering connect,
RPC, an agent task, and event streaming:
import asyncio, json, sys
import websockets # pip install websockets
URL = "ws://127.0.0.1:17888" # localhost skips token auth
TOKEN = None # or "..." -> Authorization: Bearer <token>
class RTermGW:
def __init__(self):
self._seq = 0
self._pending = {} # id -> asyncio.Future
self.events = [] # async events (gateway:event / gateway:raw / ...)
async def connect(self):
# `websockets` renamed extra_headers -> additional_headers in v14/v15
headers = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else None
try:
self.ws = await websockets.connect(URL, additional_headers=headers)
except TypeError:
self.ws = await websockets.connect(URL, extra_headers=headers)
self._reader = asyncio.create_task(self._read_loop())
async def _read_loop(self):
async for raw in self.ws:
msg = json.loads(raw)
if msg.get("type") == "gateway:response" or ("id" in msg and ("result" in msg or "error" in msg or "ok" in msg)):
fut = self._pending.pop(msg.get("id"), None)
if fut and not fut.done():
if msg.get("ok") is False or "error" in msg:
err = msg.get("error") or {}
fut.set_exception(RuntimeError(f"{err.get('code')}: {err.get('message')}"))
else:
fut.set_result(msg.get("result", msg))
else:
self.events.append(msg)
async def rpc(self, method, params=None, timeout=60):
self._seq += 1
rid = f"c{self._seq}"
fut = asyncio.get_event_loop().create_future()
self._pending[rid] = fut
await self.ws.send(json.dumps({"id": rid, "method": method, "params": params or {}}))
return await asyncio.wait_for(fut, timeout)
async def main():
gw = RTermGW()
await gw.connect()
print("ping:", await gw.rpc("gateway:ping"))
# Run an AI agent task (blocking) and print the transcript tail
sess = await gw.rpc("gateway:createSession")
sid = sess["sessionId"]
await gw.rpc("agent:startTask", {
"sessionId": sid,
"userInput": "Update AV signatures on the saved WinRM connection "
"AWS-Windows-Server-1 and report AntispywareSignatureVersion."
}, timeout=180)
ui = await gw.rpc("agent:getUiMessages",
…(truncated)
1---2name: rterm-gateway3description: Remotely drive any RTerm instance over its WebSocket gateway — run AI agent tasks, open/control SSH/WinRM/Serial/local terminals, transfer files, manage settings & scheduled automation, fully headless. Use when an agent needs to call an RTerm gateway to execute commands on remote servers, run playbooks, orchestrate fleets, or schedule jobs from another program or agent (e.g. Pi, CI pipelines, other LLM agents).4---56# RTerm Gateway — Remote Control Skill78RTerm can run as a **headless service**. When its gateway is enabled, it opens a9**WebSocket JSON-RPC** endpoint. Any program or agent that can open a WebSocket can10drive the full RTerm feature set — no UI, no human.1112Use this skill to:13- Run AI agent tasks on the gateway (`agent:startTask` / `agent:startTaskAsync`).14- Open and control terminals on **SSH / WinRM / Serial / local** targets and run commands.15- Transfer and edit files on any connected host.16- Manage settings, command policy, skills, memory, and scheduled automation.17- Orchestrate fleets and scheduled jobs (cron) completely headlessly.1819---2021## 1. How the gateway works (mental model)2223```24Your agent / program ──WebSocket JSON-RPC──> RTerm Gateway (ws://host:17888)25 │26 ┌────────────────────────┼─────────────────────────┐27 │ │ │28 AgentService TerminalService AutomationManager29 (run AI tasks) (SSH/WinRM/Serial/local) (playbooks, cron, change MOP)30 │ │ │31 LLM + tools run commands, files scheduler, ledgers32```3334- **Requests** are JSON-RPC: `{ "id": "1", "method": "<name>", "params": { ... } }`.35- **Responses** echo the `id` wrapped in a `gateway:response` envelope:36 - success → `{ "type": "gateway:response", "id": "1", "ok": true, "result": { ... } }`37 - error → `{ "type": "gateway:response", "id": "1", "ok": false, "error": { "code", "message" } }`38- **Events** stream to you asynchronously as `{ "type": "gateway:event" | "gateway:raw" | "gateway:ui-update", "channel"?, "payload": ... }`.3940Default endpoint: **`ws://<host>:17888`** (default port `17888`, configurable).4142**HTTP on the same port (v3.0.2+):** the gateway also serves plain HTTP on that port —43**`GET /dashboard`** renders the live unified dashboard (WS-push updates in the browser,44falls back to polling **`GET /dashboard/json`**, which returns the raw state). Same auth45model: loopback open, remote needs a token (`Authorization: Bearer`, `x-access-token`,46or `?access_token=`).4748### Auth4950- **Token auth** via `Authorization: Bearer <token>` header (or a token provided per deployment).51- **Localhost bypass:** connections from `127.0.0.1` / `::1` skip the token by default52 (`allowLocalhostWithoutToken: true`). If you are on the same machine as RTerm, you usually53 need no token.54- Optional **IP allow-list (CIDR)** may restrict which hosts can connect.5556If a connection is rejected, you'll get a close frame with a reason — treat that as an57auth/IP problem, not a protocol problem.5859### Bootstrapping an access token (no CLI)6061There is **no `gybackend create-token` command** — token creation normally happens through62the gateway's own RPC/UI, which is a chicken-and-egg problem for the *first* token on a63headless daemon. The store is a simple JSON file at `{DATA_DIR}/access-tokens.json`64(`{schemaVersion:1, tokens:[…]}`). Each record holds a **scrypt hash**, never the plaintext:6566```67token = "gys_at_" + base64url(randomBytes(24)) # the plaintext you hand out (shown once)68tokenSalt = base64(randomBytes(16)) # per-token salt69tokenHash = base64(scrypt(token, salt, 32)) # stored; verified with timingSafeEqual70record = { id: "atk_"+base64url(randomBytes(12)), name, createdAt, tokenSalt, tokenHash }71```7273To mint the first token, write a record matching that exact algorithm into the file (mode74`0600`). The running backend re-reads the file on **every** `verify()`, so **no restart is75needed**. Verify over the non-loopback interface: valid token → `200`, no/bad token → `401`.76Tokens are accepted as `Authorization: Bearer <token>`, `x-access-token: <token>`, or77`?access_token=<token>` (query). To revoke, delete the record by `id`.7879---8081## 1b. Standalone `neuralos` daemon vs. the RTerm desktop app8283The gateway is served by **gybackend** — the headless engine. There are two ways it runs:8485- **RTerm.app (desktop):** embeds gybackend and spawns it in-process. Backend lives only86 while the app is open. Its data dir is `~/Library/Application Support/rterm`.87- **`neuralos` (npm package, `npm i -g neuralos`):** the *same* gybackend engine, packaged88 standalone with **no GUI**. Runs as an always-on/headless service (e.g. a macOS89 LaunchAgent with `RunAtLoad`+`KeepAlive`). Its data dir is **`~/.gybackend-data`**90 (override with `GYBACKEND_DATA_DIR`).9192**They are separate profiles.** The app and the daemon each keep their own93`settings.json`, `access-tokens.json`, `gyshell-*.sqlite`, `skills/`, `memory.md`.94Installing `neuralos` gives you a **fresh, empty profile** — none of the GUI's95connections/playbooks/schedules. To share state you either copy the data over or point one96at the other (see §10a). The GUI's backend-settings file is `gyshell-backend-settings.json`;97the daemon's equivalent is `settings.json` — **same schema**, so it maps 1:1. **Preserve98the daemon's own `gateway` block when copying** (the GUI may have `gateway.ws.access:99"disabled"`, which would shut off the very gateway you're trying to reach).100101**Feature parity:** every backend capability works in `neuralos` — SSH, WinRM, serial,102local PTY, playbooks, MOP changes, triggers, vault, recording, observability (APM/DEM/103cloud/on-call/GitOps/cost), the full agent runtime, and all bundled plugins. The only thing104absent is the desktop UI (which is a *client* of the gateway, not a backend feature).105106> **Gotcha — `neuralos` ships without `ssh2` and `serialport`.** The npm package bundles a107> single `bin/gybackend.cjs` with **no `node_modules`**; SSH and serial are *lazy, guarded*108> external requires. The daemon starts fine, but opening an **SSH** tab throws109> `ssh2 is not available in this build (… WinRM/serial/local terminals still work)`, and a110> **serial** tab throws "install serialport". Fix once:111> ```bash112> cd "$(npm config get prefix)/lib/node_modules/neuralos" && npm install ssh2 serialport --no-save --omit=dev113> ```114> WinRM (pure HTTP) and local PTY are unaffected.115116---117118## 2. The 30-second start1191201. **Verify the gateway is up and reachable:**121 ```bash122 node scripts/rterm-gw.mjs --url ws://127.0.0.1:17888 ping123 # -> { "pong": true, "ts": ... }124 ```1252. **List terminals / sessions:**126 ```bash127 node scripts/rterm-gw.mjs terminal-list128 node scripts/rterm-gw.mjs session-list129 ```1303. **Run a command on a saved WinRM/SSH connection** (the headline use case):131 ```bash132 node scripts/rterm-gw.mjs exec-winrm \133 --name "AWS-Windows-Server-1" \134 --command "powershell -NoProfile -Command \"Update-MpSignature; (Get-MpComputerStatus).AntispywareSignatureVersion\""135 ```136137The bundled helper `scripts/rterm-gw.mjs` wraps the whole protocol (connect, RPC, events,138waits) into subcommands. Use it directly or read it as a reference client.139140---141142## 3. Choosing the right method (decision guide)143144| I want to… | Use |145|---|---|146| Have the **AI agent** figure out & run a multi-step task | `agent:startTask` (block) / `agent:startTaskAsync` (fire-and-forget) |147| **Run one command** on an SSH/WinRM/local/Serial host | `terminal:createTab` → `terminal:write` → `terminal:getBufferDelta` (PTY) **or** route via the agent for WinRM |148| **WinRM command/response** execution | Prefer the **agent path** (`agent:startTask`) — WinRM has no live stdin; the agent's `exec_command` uses the structured `executeCommand` path that returns output. |149| Read/write/transfer **files** on a host | `filesystem:*` |150| Manage **saved connections**, **settings**, **policy** | `settings:*`, `settings:addCommandPolicyRule`, `agentSettings:*` |151| Create/modify **scheduled cron jobs** | `settings:set` (automation section) — scheduler runs them headless |152| Orchestrate a **fleet** or a **playbook** | `agent:startTask` ("run the X playbook on group Y") |153| Watch **live progress** | subscribe to events (`gateway:event` / `gateway:raw`) |154155> **Key gotcha — WinRM is command/response, not a PTY.** `terminal:write` to a WinRM tab is a156> no-op (returns `ok` but runs nothing). For WinRM, drive commands through the **agent**157> (`agent:startTask`), whose tools route through the structured `executeCommand` path and158> return real output. SSH/local PTY tabs work fine with `terminal:write` + `getBufferDelta`.159160---161162## 4. RPC method reference (123 methods: 70 core + 52 observability:* + gateway:describe)163164> **v3.0.0 — ask the gateway instead of reading this list.** Call **`gateway:describe`** (params `{}`, or `{category:"observability"}` / `{prefix:"settings:"}`) to get the full, live method registry (`{version, count, categories, methods:[{name,category,description,since,params}]}`) straight from the source of truth. From the agent, use the **`list_gateway_methods`** tool (same category/prefix filters). The registry below is a static snapshot — `gateway:describe` is always current.165166Params are passed as a JSON object under `params`. `…` = see source for full shape.167168### Gateway / session lifecycle169| Method | Params | Returns | Notes |170|---|---|---|---|171| `gateway:ping` | — | `{pong:true, ts}` | liveness |172| `gateway:isSameMachine` | — | `{sameMachine}` | true if client is co-located |173| `gateway:createSession` | — | `{sessionId}` | new agent/chat session |174| `gateway:describe` (v3.0.0) | `{category?, prefix?}` | `{version, count, total, categories, methods}` | **self-discovery** — the live method registry |175| `session:list` | — | `{sessions:[…]}` | session summaries |176| `session:get` | `{sessionId}` | `{session}` | one session snapshot |177178### Agent (AI task execution)179| Method | Params | Returns | Notes |180|---|---|---|---|181| `agent:startTask` | `{sessionId, userInput, options?}` | `{ok:true}` | **blocks** until the task completes; `userInput` = string or `{text, images?}` |182| `agent:startTaskAsync` | `{sessionId, userInput, options?}` | `{ok:true}` | fire-and-forget; watch events for progress |183| `agent:stopTask` | `{sessionId}` | `{ok:true}` | abort a running task |184| `agent:replyMessage` | `{messageId, payload}` | … | answer an agent prompt |185| `agent:replyCommandApproval` | `{approvalId, decision}` | … | `decision`: `"allow"` or `"deny"` |186| `agent:getUiMessages` | `{sessionId}` | `{messages:[…]}` | transcript for a session |187| `agent:getAllChatHistory` | — | `[…]` | all chat history |188| `agent:exportHistory` | `{sessionId, mode?}` | … | `mode`: `"simple"` or `"detailed"` |189| `agent:loadChatSession` | `{id}` | … | load a session into the agent |190| `agent:renameSession` | `{sessionId, title}` | … | rename |191| `agent:deleteChatSession` | `{sessionId}` | `{ok:true}` | delete one |192| `agent:deleteChatSessions` | `{sessionIds:[…]}` | … | delete many |193| `agent:branchFromMessage` | `{sessionId, messageId}` | … | branch a session at a message |194| `agent:rollbackToMessage` | `{sessionId, messageId}` | … | roll a session back |195196### Terminals (SSH / WinRM / Serial / local)197| Method | Params | Returns | Notes |198|---|---|---|---|199| `terminal:list` | — | `{terminals:[…]}` | all tabs with `runtimeState` |200| `terminal:createTab` | `{config}` | `{id}` | config = a `TerminalConfig` (see §5) |201| `terminal:write` | `{terminalId, data}` | `{ok:true}` | write to PTY (SSH/local); **no-op for WinRM** |202| `terminal:writePaths` | `{terminalId, …}` | … | write file paths (drop) |203| `terminal:resize` | `{terminalId, cols, rows}` | … | resize PTY |204| `terminal:kill` | `{terminalId}` | … | close tab |205| `terminal:reconnect` | `{terminalId}` | … | reconnect an exited tab |206| `terminal:setSelection` | `{terminalId, selectionText}` | … | set selection text |207| `terminal:getBufferDelta` | `{terminalId, fromOffset}` | `{…}` | read accumulated output (PTY) |208| `terminal:generateCommandDraft` | `{terminalId, …}` | … | AI command draft |209210### Filesystem (per terminal/host)211| Method | Params | Notes |212|---|---|---|213| `filesystem:list` | `{terminalId, dirPath?}` | list a directory |214| `filesystem:readTextFile` | `{terminalId, filePath}` | read a text file |215| `filesystem:readFileBase64` | `{terminalId, filePath}` | read binary as base64 |216| `filesystem:writeTextFile` | `{terminalId, filePath, content}` | write text |217| `filesystem:writeFileBase64` | `{terminalId, filePath, contentBase64}` | write binary |218| `filesystem:createDirectory` | `{terminalId, dirPath}` | mkdir |219| `filesystem:createFile` | `{terminalId, filePath}` | touch |220| `filesystem:deletePath` | `{terminalId, targetPath}` | delete |221| `filesystem:renamePath` | `{terminalId, oldPath, newPath}` | rename/move |222| `filesystem:transferEntries` | `{…}` | multi-entry transfer plan |223| `filesystem:startTransfer` | `{…}` | start an upload/download |224| `filesystem:getTransfer` | `{transferId}` | transfer status |225| `filesystem:listTransfers` | — | list transfers |226| `filesystem:cancelTransfer` | `{transferId}` | cancel |227| `filesystem:cancelTransferTask` | `{transferId}` | cancel a task |228229### Settings, policy, skills, memory, models230| Method | Params | Notes |231|---|---|---|232| `settings:get` | — | full settings (incl. `connections`, `automation`) |233| `settings:set` | `{settings}` | patch settings (e.g. add scheduled task) |234| `settings:getCommandPolicyLists` | — | allow/ask/deny lists |235| `settings:addCommandPolicyRule` | `{list, rule}` | add rule to `allowlist`/`asklist`/`denylist` |236| `settings:deleteCommandPolicyRule` | `{list, rule}` | remove rule |237| `agentSettings:get` | — | agent settings |238| `agentSettings:saveCurrent` | `{…}` | save a profile slot |239| `agentSettings:apply` | `{…}` | apply a profile |240| `agentSettings:overwrite` | `{…}` | overwrite |241| `agentSettings:delete` | `{…}` | delete profile |242| `skills:getAll` / `skills:list` / `skills:getEnabled` | — | list skills |243| `skills:setEnabled` | `{name, enabled}` | toggle a skill |244| `skills:create` / `skills:delete` / `skills:reload` | `{…}` | manage skills |245| `memory:get` | — | global memory |246| `memory:setContent` | `{content}` | set memory |247| `models:getProfiles` | — | model profiles |248| `models:setActiveProfile` | `{profileId}` | switch model |249| `models:probe` | `{…}` | probe a model |250| `tools:getMcp` / `tools:reloadMcp` / `tools:setMcpEnabled` | `{…}` | MCP tools |251| `tools:getBuiltIn` / `tools:setBuiltInEnabled` | `{name, enabled}` | built-in tools |252| `system:saveImageAttachment` | `{…}` | attach an image |253254**Settings blocks (v2.9.6+, schema v5)** — persisted + live-reloaded on `settings:set` (no restart), all editable in the desktop Settings UI: `cost.modelPrices`+`cost.budgets` (Settings → AI Cost), `alerts.channels[]` (Settings → Alerts), `oncall.pagingChannels[]` (Settings → On-Call), `cloud.accounts[]` (Settings → Cloud), `agentspan.serverUrl`+`agentspan.authSecretRef` (Settings → AgentSpan). Secrets are always `secretRef` pointers into the vault, never inline. Example: `settings:set {"cost":{"modelPrices":{"moonshotai/kimi-k3":{"promptPer1M":3,"completionPer1M":15},"default":{"promptPer1M":0.7,"completionPer1M":2.2}},"budgets":[{"id":"monthly-all","model":"*","period":"monthly","capUsd":2500,"warnAt":0.8,"overAction":"throttle"}]}}`.255256### Observability / SRE (v2.0.0–v2.3.1) — driven via the agent257The observability modules are wired into the backend and driven through `agent:startTask` (the agent reads/writes them via its built-in tools). Key capabilities to ask for:258259| Area | Example `userInput` |260|---|---|261| **Unified dashboard** | "Build the dashboard:state — fleet health, SLO, uptime, incidents, APM, DEM, capacity" |262| **SRE metrics** | "Report golden signals + capacity forecast for all hosts; days-to-disk-full" |263| **Uptime watchdog** | "Add an uptime watchdog for web-01 (tcp 443) and report its state" |264| **SLO** | "Create an SLO api-uptime 99.9% over 30d and evaluate burn rate" |265| **Incidents** | "List open incidents and generate the postmortem for the latest one" |266| **APM (OTLP)** | "Ingest these OTLP spans and report the slowest traces + bottleneck services" |267| **DEM (RUM)** | "Report p75 LCP/INP and error rate per page; which pages are poor on Core Web Vitals?" |268| **k8s/cloud** | "Collect cluster health (pods, restarts, node readiness, cpu/mem % of limit)" |269| **ETW (Windows)** | "Run a network ETW trace on AWS-Windows-Server-1 for 60s and summarize connections" |270| **Predictive** | "Detect anomalies in cpu/disk for all hosts and any forecast breaches within 7 days" |271| **Behavioral** | "Flag any run-spikes, token-blowouts, error-spikes, or unusual models vs the baseline" |272| **Evals** | "Run the embedded eval harness on the golden set and report accuracy/tool/safety/replay %" |273| **Notify (Slack/Teams/SMTP/Telegram)** | "Wire a Slack alert channel (webhook …) and fire a test alert" |274| **dagu workflows (v2.4.0+)** | "Compile + run this dagu YAML workflow (paste YAML) using daguParser, show the DAG waves, and report per-step results" |275| **Browser dashboard (v3.0.2+)** | "Open http://localhost:17888/dashboard — the live unified dashboard (WS-push updates, /dashboard/json fallback) is served on the same port as the gateway" |276| **AWS APerf deep-dive (v2.6.0+)** | "Run an APerf performance deep-dive on web-01 — deploy aperf, record CPU/mem/disk/PMU/processes/hotspot for 60s, parse the findings, and report the top issues" |277| **Plugin system (v2.5.0+)** | "List installed plugins and their tools/triggers/panels" |278| **Patch management (plugin)** | "Check patch status on web-01, build a patch plan for the security patches, and submit it for approval" |279| **Request router (plugin)** | "Submit a request to restart nginx on web-01 with justification 'planned maintenance', then list pending requests" |280| **SOP assistant (plugin)** | "Search the SOP library for 'database failover' and show me the steps; then execute the restart-service SOP on web-01 with service=nginx" |281| **IAM connector (plugin)** | "Review all users on web-01 and identify privileged accounts; what groups is john in?" |282| **Web intelligence (v3.0.9+)** | "Search the web for 'Cisco BGP-5-ADJCHANGE IOS XE 17.18 known issue' and cite sources; then watch the Cisco advisory page for changes and propose a change if it updates" |283| **FraudOps (plugin)** | "Check the fraud pipeline status (Flink/NATS/Kafka health) and summarize recent fraud decisions" |284| **Netdata integration (plugin)** | "Correlate this Netdata alert with RTerm's metrics and incidents for RCA" |285| **Monitor diagnostics (v2.7.6+)** | "Run monitor status diagnostics — why aren't stats displaying for terminal X? Report publisher/session/inFlight/connected/last-collect per terminal" |286| **AGT policy engine (v2.7.7+)** | "Evaluate the action 'restart nginx' on target 'prod-web-01' against the governance policy — allow, deny, or escalate?" |287| **Review model / maker-checker (v2.7.8+)** | "Review this action with the checker model: type=restart target=prod-web-01 command='systemctl restart nginx' — verify correctness, completeness, safety, compliance, accuracy" |288289> The observability ledgers feed the unified dashboard and are driven by the agent's290> built-in tools — no separate RPC methods are needed beyond `agent:startTask` /291> `agent:getUiMessages` for these areas.292293### Plugins (v2.5.0+)294295RTerm ships with **6 official plugins** (auto-discovered on startup) plus any user plugins296in `~/.gybackend-data/plugins`. Plugin tools are called the same way as built-in tools —297through `agent:startTask`.298299| Plugin | Agent tools | Use for |300|---|---|---|301| **patch-manager** | `patch_status`, `patch_plan`, `patch_apply` | Autonomous patch management across hosts (yum/apt/Windows Update) with MOP approval + compliance dashboard |302| **request-router** | `submit_request`, `approve_request`, `list_requests`, `request_status` | Automated request handling — classify risk (low/med/high), route (auto-approve/queue/MOP), execute, audit |303| **sop-assistant** | `sop_search`, `sop_get`, `sop_execute`, `iam_lookup` | SOP retrieval + step-by-step guided execution. 8 built-in SOPs (restart-service, disk-cleanup, reset-password, database-failover, ssl-cert-renewal, user-offboarding, backup-restore, incident-response) + 4 IAM policies |304| **iam-connector** | `iam_user_info`, `iam_user_groups`, `iam_disable_user`, `iam_access_review` | IAM integration — user/group management, privileged access identification, access review (Linux id/groups/usermod + Windows Get-LocalUser) |305| **fraudops** | `fraudops_pipeline_status`, `fraudops_str_assign`, `fraudops_str_status`, `fraudops_decision_summary` | FraudOps operational layer — Flink/NATS/Kafka health, STR workflow (7-day CBN deadline), decision summary |306| **netdata-rterm** | `netdata_alert_summary`, `netdata_correlate` | Netdata Cloud webhook ingestion + correlation with RTerm metrics/incidents for RCA |307| **agentspan-bridge** (v2.9.9+) | `agentspan_health`, `agentspan_run`, `agentspan_status`, `agentspan_approve`, `agentspan_list`, `agentspan_stop` | Durable, crash-resilient agent execution on an AgentSpan (Netflix Conductor) server — runs resume from the last completed step; plan-execute determinism; Kafka/SQS/AMQP event triggers. Configure in Settings → AgentSpan (`agentspan.serverUrl`, default `http://localhost:6767`, + optional `agentspan.authSecretRef`). See the `agentspan` skill for the standalone SDK/CLI. |308| **synapse-bridge** (v3.1.4+) | `synapse_health`, `synapse_discover`, `synapse_dispatch`, `synapse_register`, `synapse_agents_summary` | Synapse mesh interop — discover live Synapse agents, dispatch tasks to `mesh.agent.{id}.inbox` (durable), and register RTerm as a mesh agent (bidirectional). Speaks Synapse protocol v0.3.0 over a shared NATS server (uses the v3.1.2 auth/request-reply/JetStream transport). Config `settings.synapse` (url/servers/prefix/agentId/auth). See the `synapse` skill. |309| **numbat-bridge** (v3.1.4+) | `numbat_health`, `numbat_deploy`, `numbat_ingest`, `numbat_findings_summary` | Numbat (endpoint AI-agent detection/EDR) integration — Numbat detects, RTerm responds. Deploy numbat to hosts (inventory/scan/install-monitor/install-enforce/status/uninstall via policy-gated exec) and ingest NDJSON findings → fire RTerm triggers to auto-remediate. Config `settings.numbat` (binaryPath/recordsPath/ingestToken/minSeverity). |310311**Plugin triggers** fire autonomously (e.g., `patch_failure` → propose-change, `fraudops_pipeline_down` → run-playbook, `netdata_critical_alert` → auto-remediation, `agentspan_execution_failed` → investigate/re-run, `synapse_mesh_event` → cross-mesh remediation, `numbat_finding` → isolate/kill/incident).312313To **install a custom plugin** on a headless backend, drop the plugin folder (with a valid314`plugin.json` + `index.mjs`) into `{GYBACKEND_DATA_DIR}/plugins/` and restart — the315`PluginRegistry` auto-discovers it.316317---318319## 4b. Observability RPC methods (v2.9.0+) — direct, no agent needed320321v2.9.0 added 9 platform capabilities as backend modules; **v2.9.2 exposed them as 41 first-class322`observability:*` RPC methods** on the gateway (no `agent:startTask` round-trip needed). v2.9.3 made323the matching agent tools visible in the Tools section. These are the same methods the agent's324`get_metrics`/`manage_secret`/`manage_oncall`/`get_cost`/`manage_recording`/`manage_gitops`/325`manage_playbook_version`/`get_cloud_inventory`/`get_live_dashboard` tools call.326327**Call them like any other RPC** (`{id, method, params}`):328329```json330{"id":"1","method":"observability:costSummary","params":{"period":"daily"}}331```332333| Area | Methods | Notes |334|---|---|---|335| **Metrics / dashboard** | `observability:metricsPrometheus`, `observability:dashboardState`, `observability:dashboardSummary` | Prometheus exposition text for a scraper; unified dashboard state/summary |336| **OTel push** | (via `OTEL_EXPORTER_OTLP_ENDPOINT` env) | Pushes OTLP/HTTP JSON to a collector on an interval |337| **Secrets vault** | `observability:secretsList`, `secretsSet`, `secretsDelete`, `secretsHas` | AES-256-GCM, **metadata only — never values**. Needs `RTERM_SECRETS_MASTER_KEY` at boot |338| **On-call / escalation** | `observability:oncallListPolicies`, `oncallRegisterPolicy`, `oncallOpenPages`, `oncallPage`, `oncallAck`, `oncallResolve`, `oncallTick` | Multi-level escalation policies, ack deadlines, paging |339| **AI cost & budgets** | `observability:costSummary`, `costRecord`, `costCheck`, `costListBudgets`, `costSetBudget`, `costRemoveBudget` | USD spend per model/profile; warn/throttle/deny budgets |340| **Session recording** | `observability:recordingList`, `recordingStart`, `recordingStop`, `recordingReplay`, `recordingExportCast`, `recordingDelete` | asciinema `.cast` v2 export/import + scrub/replay |341| **GitOps** | `observability:gitopsExport`, `gitopsDrift`, `gitopsInSync`, `gitopsReconcile` | Desired-state manifest, drift detection, gated reconcile |342| **Playbook versioning** | `observability:playbookLint`, `playbookHistory`, `playbookSave`, `playbookRollback`, `playbookDiff` | Version history + static lint (undefined params, dependsOn cycles, missing rollback) |343| **Cloud inventory** | `observability:cloudSummary`, `cloudQuery`, `cloudSync`, `cloudAddAccount` | Normalized AWS/GCP/Azure instances (inject fetchers) |344| **Live dashboard** | `observability:liveDashboardState`, `liveDashboardSubscriberCount`, `liveDashboardSubscribe` | Push-based multi-client dashboard state; **`GET /dashboard` + `GET /dashboard/json`** (v3.0.2) serve the live page + state on the same port |345| **APM ingestion** | `observability:apmIngestSpans`, `apmSummary` | Feed OTLP/HTTP-JSON trace spans → trace store (bottleneck services, slowest traces) |346| **DEM ingestion** | `observability:demIngestBeacon`, `demSummary` | Feed Core Web Vitals RUM beacons (page, LCP/INP/CLS/TTFB, JS errors) → per-page p75 + error rate |347| **Infra ingestion** | `observability:infraCollect`, `infraClusters`, `infraUnhealthy` | Collect k8s cluster health (kubectl text or JSON payload) → not-ready, CrashLoopBackOff |348| **ETW ingestion** | `observability:etwStartTrace`, `etwStopTrace`, `etwParse`, `etwSessions` | Windows ETW diagnostics — logman start/stop commands + parse Get-WinEvent/Get-Counter output |349| **Web intelligence (v3.0.9+)** | Plugin tools: `webintel_health`, `web_search`, `web_fetch`, `web_crawl`, `web_research`, `web_find_similar`, `web_watch_add`, `web_watch_list`, `web_watch_remove` | Local-first web intelligence via the `web-intel` plugin (wigolo daemon). Multi-engine search, clean-page fetch, site crawl, research (synthesis by RTerm agent — no LLM key), page-watch → `webintel_page_changed` trigger. Lean by default (`WIGOLO_NO_WARMUP=1`); ~1.5 GB browser engine + models are opt-in (`webIntel.warmupOnInit`). |350351**Example — set a secret then verify it (values never come back):**352353```bash354./scripts/rterm-gw.mjs call observability:secretsSet '{"key":"aws-access-key","value":"AKIA…","labels":{"service":"aws"}}'355./scripts/rterm-gw.mjs call observability:secretsList '{}'356# → [{"key":"aws-access-key","labels":{"service":"aws"},"createdAt":…,"updatedAt":…}] (no "value" field)357```358359**Example — check AI spend + gate a run:**360361```bash362./scripts/rterm-gw.mjs call observability:costSummary '{"period":"daily"}'363# → {"totalUsd":3.42, "byModel":[{"model":"gpt-4o","usd":3.42,…}]}364./scripts/rterm-gw.mjs call observability:costCheck '{"model":"gpt-4o"}'365# → {"action":"ok"|"warn"|"throttle"|"deny", "statuses":[…]}366```367368**Example — record + export a session:**369370```bash371./scripts/rterm-gw.mjs call observability:recordingStart '{"terminalId":"local-main","title":"deploy"}'372# → {"recordingId":"r-…"}373./scripts/rterm-gw.mjs call observability:recordingExportCast '{"recordingId":"r-…"}' > session.cast374```375376> Agent-tool equivalents (when you'd rather let the agent drive): `get_metrics`, `manage_secret`,377> `manage_oncall`, `get_cost`, `manage_recording`, `manage_gitops`, `manage_playbook_version`,378> `get_cloud_inventory`, `get_live_dashboard`, `ingest_apm_spans`, `get_apm_summary`,379> `ingest_dem_beacon`, `get_dem_summary`, `collect_infra`, `manage_etw`. Ask for them in `userInput`,380> e.g. "add this API key to the vault", "show my AI spend today", "page the on-call for the DB381> incident", "lint this playbook", "ingest these OTLP spans", "ingest this RUM beacon", "collect the382> k8s cluster health", "start an ETW network trace on the Windows host".383384---385386## 5. TerminalConfig shapes (for `terminal:createTab`)387388```jsonc389// SSH390{ "type": "ssh", "id": "t1", "title": "web-01", "cols": 120, "rows": 32,391 "host": "10.0.0.5", "port": 22, "username": "deploy",392 "password": "…", // or "privateKey" / "privateKeyPath" / "agent"393 "algorithmsPreset": "modern|legacy|cisco", "termType": "xterm-256color|vt100" }394395// WinRM (command/response — drive commands via the AGENT, not terminal:write)396{ "type": "winrm", "id": "w1", "title": "win-01", "cols": 140, "rows": 40,397 "host": "44.197.31.152", "port": 5985, "username": "Administrator",398 "password": "…", "transport": "http", "auth": "basic", "domain": "" }399400// Serial401{ "type": "serial", "id": "s1", "title": "switch-console", "cols": 120, "rows": 32,402 "path": "/dev/ttyUSB0", "baudRate": 9600, "dataBits": 8, "parity": "none",403 "stopBits": 1, "flowControl": "none" }404405// Local406{ "type": "local", "id": "l1", "title": "local", "cols": 120, "rows": 32,407 "cwd": "/work", "shell": "/bin/zsh" }408```409410Saved connections already known to RTerm can be opened by asking the agent to411"open the saved connection named X" (see §6), or by reading `settings:get` →412`connections.{ssh,winrm,serial}` and passing the same fields to `terminal:createTab`.413414> **Gotcha — `terminal:createTab` does NOT resolve saved-connection names or ids.**415> It passes `config` **straight through** to the terminal service; there is no416> `savedConnectionName` / `connectionId` lookup. If you pass `{type:"ssh",417> savedConnectionName:"Remote"}` the tab opens with `host=undefined`, `username=undefined`418> and fails with `Invalid username` (retried in a loop). To open a saved connection over419> RPC you must **read it from `settings:get` and expand it into the full inline config420> yourself** (host/port/username/password/…). The bundled helper **`scripts/gy-open.mjs`**421> does exactly this (by name or id, across ssh/winrm/serial, incl. proxy/jumpHost) — see §8c.422423---424425## 6. Events (watching progress live)426427You do **not** subscribe explicitly — events stream to every connected client.428429| Wire type | Meaning | Payload |430|---|---|---|431| `gateway:event` | A structured `GatewayEvent` | `{id, timestamp, type, sessionId?, payload}` where `type` ∈ `agent:event` \| `session:update` \| `ui:action` \| `system:notification` |432| `gateway:raw` | Raw channel data | `{channel, payload}` — e.g. `channel:"terminal:data"` carries `{terminalId, data, offset}` |433| `gateway:ui-update` | UI action broadcast | action object |434435For `agent:startTask`, watch for `agent:event` payloads (tool calls, streamed model text,436completion). The bundled client prints events to stderr so you can observe them.437438---439440## 7. Command policy & autonomy441442Every command the agent runs is evaluated against the **command policy**:443444- **`smart`** — run autonomously (unless explicitly denylisted). Headless-friendly.445- **`standard`** — **asks** for approval on unrecognized commands. A remote client must446 answer with `agent:replyCommandApproval` (`{approvalId, decision:"allow"|"deny"}`).447- **`safe`** — denies unrecognized commands.448449Check the mode with `settings:get` → `commandPolicyMode`. For unattended operation, either450use `smart` mode or pre-allowlist the commands your workflow needs451(`settings:addCommandPolicyRule {list:"allowlist", rule:"Update-MpSignature*"}`).452453---454455## 7a. Governance, audit & the maker/checker model (v2.7.x)456457### Audit trail (v2.7.1) — hash-chained, tamper-evident458459Every audit-relevant event (agent runs, command evaluations, approvals, MOP changes,460playbook steps, trigger firings, alert ingestions) is appended to a **hash-chained audit461ledger** — each record carries the SHA-256 hash of the previous one, so any tampering is462detectable via `verify()`. The **evidence sealer** computes a Merkle-tree root over the463records → sealed, independently-verifiable evidence bundles (KLA framework domain 11).464465Ask the agent: *"Show the audit ledger for command X and verify the chain"*, or466*"Seal the audit ledger and produce the evidence bundle for the auditor"*.467468### AGT policy engine (v2.7.7) — governance before execution469470The **AGT policy engine** evaluates every consequential action against a YAML policy471before execution. Decisions: `allow` / `deny` / `escalate` (route to approval).472473- Glob-style action patterns (`"read"` matches `"read /etc/passwd"`), target wildcards474 (`prod-*`), first-match-wins, case-insensitive.475- Built-in default policy: allow read/status/list; deny delete/drop/format; **escalate**476 restart/patch/deploy on `prod-*`; allow them otherwise.477- Drop a custom `policy.yaml` in the data dir to override.478479Ask the agent: *"Evaluate the action 'restart nginx' on target 'prod-web-01' against the480governance policy"* — the agent returns the decision + the matched rule + reason.481482### Review model / maker-checker (v2.7.8) — independent verification483484The **review model** (a second LLM, the "checker") independently verifies the action485model's (the "maker's") output on **5 dimensions**: correctness, completeness, safety,486compliance, and **accuracy**.487488- Verdicts: `approved` / `needs_revision` / `escalate`.489- Modes: `strict` (block on any issue), `advisory` (flag but allow), `auto-approve`490 (skip review for low-risk actions).491- **Fast output mode:** if no `reviewModelId` is set in the profile, reviews are skipped492 entirely (zero added latency).493494Configure it in the Settings UI (v2.7.9+): the profile has a **Review Model** dropdown495(`(None — skip reviews)` = fast mode) and a **Review Mode** dropdown.496497### Monitor diagnostics (v2.7.6) — "why aren't stats displaying?"498499`monitorStatus` reports exactly why monitor stats aren't displaying per terminal:500publisher wired? session exists? collection stuck in-flight? terminal connected? platform501detected? last-collect time? Diagnoses: `terminal_not_connected`, `no_monitor_session`,502`collection_stuck_in_flight`, `never_collected`, `stale_collection`, `publisher_not_wired`.503504Ask the agent: *"Run monitor status diagnostics and report any terminals not collecting"*.505506---507508## 8. Bundled helper: `scripts/rterm-gw.mjs`509510A dependency-light reference client (Node ≥18, uses `ws`). Subcommands:511512```bash513# liveness + discovery514node scripts/rterm-gw.mjs ping515node scripts/rterm-gw.mjs terminal-list516node scripts/rterm-gw.mjs session-list517node scripts/rterm-gw.mjs settings-get518519# generic RPC (escape hatch — any method)520node scripts/rterm-gw.mjs rpc --method models:getProfiles521522# open a saved WinRM/SSH connection and run one command523node scripts/rterm-gw.mjs exec-winrm --name "AWS-Windows-Server-1" --command "<powershell>"524525# run an AI agent task (blocking or async)526node scripts/rterm-gw.mjs agent-task --text "Update AV signatures on AWS-Windows-Server-1 and report the version"527node scripts/rterm-gw.mjs agent-task --async --text "Run the Friday cleanup playbook on group prod-web"528529# read a file on a connected host530node scripts/rterm-gw.mjs fs-read --terminalId <id> --path C:\Temp\log.txt531```532533Flags: `--url` (default `ws://127.0.0.1:17888`), `--token` (or `RTERM_GW_TOKEN`), `--timeout`.534535See `examples/` for ready-made programs.536537---538539## 8a. Shell one-liners with `websocat` (no Node, no Python)540541You don't need Node or Python — the gateway is plain WebSocket + JSON, so542[`websocat`](https://github.com/vi/websocat) drives it from any shell. A prebuilt543binary is in **DrOlu/agent-tools**544([`websocat.exe` v1.14.1](https://raw.githubusercontent.com/DrOlu/agent-tools/main/websocat.exe)),545or install from your package manager (`brew install websocat`, `cargo install websocat`).546547**One-shot RPC** (reconnects each call; good for quick reads — use `-n1` = close after one reply):548549```bash550# ping551echo '{"id":"1","method":"gateway:ping"}' | websocat -n1 ws://127.0.0.1:17888552# -> {"type":"gateway:response","id":"1","ok":true,"result":{"pong":true,"ts":...}}553554# list terminals555echo '{"id":"2","method":"terminal:list"}' | websocat -n1 ws://127.0.0.1:17888556```557558**With `jq` for scripting:**559560```bash561echo '{"id":"2","method":"terminal:list"}' \562 | websocat -n1 ws://127.0.0.1:17888 \563 | jq -r '.result.terminals[] | "\(.title) [\(.type)] \(.runtimeState)"'564```565566**Persistent session** (required for `agent:startTask*` and for streaming `gateway:event`s):567568```bash569websocat ws://127.0.0.1:17888570# then paste JSON-RPC lines; responses + live events arrive on the same socket:571{"id":"1","method":"gateway:createSession"}572{"id":"2","method":"agent:startTaskAsync","params":{"sessionId":"<sid>","userInput":"Update AV signatures on AWS-Windows-Server-1 and report the version"}}573```574575**With a token** (when not connecting from localhost):576577```bash578websocat -H="Authorization: Bearer <token>" ws://rterm-host:17888579```580581> **Note:** `websocat` is line-oriented — perfect for request→response RPC and `jq`582> pipelines. For long agent tasks you must keep the socket open and read the streaming583> `gateway:event` messages yourself (or use the Node/Python client which manages that loop).584585---586587## 8b. Python client (`websockets`, no Node)588589Any Python ≥3.9 agent can drive the gateway with the `websockets` library590(`pip install websockets`). This is a minimal, complete client covering connect,591RPC, an agent task, and event streaming:592593```python594import asyncio, json, sys595import websockets # pip install websockets596597URL = "ws://127.0.0.1:17888" # localhost skips token auth598TOKEN = None # or "..." -> Authorization: Bearer <token>599600class RTermGW:601 def __init__(self):602 self._seq = 0603 self._pending = {} # id -> asyncio.Future604 self.events = [] # async events (gateway:event / gateway:raw / ...)605606 async def connect(self):607 # `websockets` renamed extra_headers -> additional_headers in v14/v15608 headers = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else None609 try:610 self.ws = await websockets.connect(URL, additional_headers=headers)611 except TypeError:612 self.ws = await websockets.connect(URL, extra_headers=headers)613 self._reader = asyncio.create_task(self._read_loop())614615 async def _read_loop(self):616 async for raw in self.ws:617 msg = json.loads(raw)618 if msg.get("type") == "gateway:response" or ("id" in msg and ("result" in msg or "error" in msg or "ok" in msg)):619 fut = self._pending.pop(msg.get("id"), None)620 if fut and not fut.done():621 if msg.get("ok") is False or "error" in msg:622 err = msg.get("error") or {}623 fut.set_exception(RuntimeError(f"{err.get('code')}: {err.get('message')}"))624 else:625 fut.set_result(msg.get("result", msg))626 else:627 self.events.append(msg)628629 async def rpc(self, method, params=None, timeout=60):630 self._seq += 1631 rid = f"c{self._seq}"632 fut = asyncio.get_event_loop().create_future()633 self._pending[rid] = fut634 await self.ws.send(json.dumps({"id": rid, "method": method, "params": params or {}}))635 return await asyncio.wait_for(fut, timeout)636637async def main():638 gw = RTermGW()639 await gw.connect()640 print("ping:", await gw.rpc("gateway:ping"))641642 # Run an AI agent task (blocking) and print the transcript tail643 sess = await gw.rpc("gateway:createSession")644 sid = sess["sessionId"]645 await gw.rpc("agent:startTask", {646 "sessionId": sid,647 "userInput": "Update AV signatures on the saved WinRM connection "648 "AWS-Windows-Server-1 and report AntispywareSignatureVersion."649 }, timeout=180)650 ui = await gw.rpc("agent:getUiMessages",651652…(truncated)