# oxdock-ssh-plugin
Ephemeral user-space SSH server and client for OxDock scripts, bridged to stdio pipelines through the Engine facade.
> Part of the [OxDock](https://github.com/jzombie/rust-oxdock) workspace.
## Overview
The SSH module spins up ephemeral loopback SSH servers with in-memory
credentials, dequeues each session with its metadata, and pumps bytes
through explicit DSL pipes. Import it with `IMPORT [STD, SSH]` in a
runner that registers the host module on the
[Engine facade](https://github.com/jzombie/rust-oxdock/blob/main/crates/oxdock-core/src/exec/engine.rs). The function
reference below lists every entry with signatures; the type reference
lists the session and server handles.
## Functions
Callable as `MODULE::NAME(...)` in expressions (or bare `NAME(...)` with the module imported via `IMPORT`). Introspectable from scripts with `FUNCTIONS()` and `DESCRIBE(name)`.
### SSH_ACCEPT
**Signature:** `SSH_ACCEPT($server, $in_pipe, $out_pipe) -> MAP`
**Contexts:** AST only
Accept one SSH session into pipes.
Accept the next authenticated session and pump it through explicit
pipes until the channel closes. Must run inside `ASYNC`. Returns a
MAP with `closed` (BOOL) and `command` (STRING, empty for shells).
Thin wrapper over `SSH_DEQUEUE` + `SSH_PUMP_CHANNEL` for worker loops
that need no pre-pump inspection; use those directly to route on
session metadata first. Returns a MAP with `closed` (BOOL) and
`command` (STRING, empty for shells); the example below asserts both
keys on the awaited result. The server sends first: the client side
never EOFs its input, so the reply cannot race teardown. This
complete program runs end to end under the docs conformance suite.
```oxdock
IMPORT [STD, SSH]
LET $m: MAP = SSH_SERVE("doc-ssh-demo", {username: "u", password: "p"})
LET $in: PIPE
LET $out: PIPE
LET $acc: HANDLE = ASYNC { SSH_ACCEPT($m.server, $in, $out) }
LET $cin: PIPE
LET $cout: PIPE
LET $c: HANDLE = ASYNC { SSH_CONNECT("doc-ssh-demo", "u", "p", $cin, $cout) }
WITH_IO [stdout=$in] ECHO "server-greeting"
LET $info: MAP = INSPECT($cout)
LET $empty: BOOL = $info.buffer_bytes == 0
WHILE $empty {
SLEEP 100ms
$info = INSPECT($cout)
$empty = $info.buffer_bytes == 0
}
ASSERT_CONTAINS $cout "server-greeting"
LET $done: MAP = AWAIT $acc
ASSERT_CONTAINS $done "closed"
ASSERT_CONTAINS $done "command"
CANCEL $c
SSH_CLOSE($m.server)
```
### SSH_CLOSE
**Signature:** `SSH_CLOSE($server) -> BOOL`
**Contexts:** AST only
Shut down an SSH server.
Shut a server down and join its runtime thread (bounded). Idempotent:
returns BOOL true when no thread remains.
### SSH_CONNECT
**Signature:** `SSH_CONNECT($target: STRING, $username: STRING, $password: STRING, $in_pipe, $out_pipe) -> MAP`
**Contexts:** AST only
Open an SSH client session into pipes. Target shapes: a logical port (CLI-mapped address or loopback default), a service name (CLI-mapped address only), a served address, or a host:port dial.
### SSH_DEQUEUE
**Signature:** `SSH_DEQUEUE($server) -> MAP`
**Contexts:** AST only
Dequeue one SSH session with its metadata.
Dequeue one authenticated session and expose its metadata before any
byte pumping starts, so scripts can route on the requested command or
client identity. Must run inside `ASYNC`. Returns a MAP with
`session` (SSH_SESSION), `command` (STRING, empty for shells),
`username` and `addr` (STRINGs, empty when unknown).
Routing shape: compare the dequeued command against known commands,
build a fresh pipe pair per session, and pump a synthetic reply with
`SSH_PUMP_CHANNEL`. The server sends first: the client side never
EOFs its input, so the reply cannot race teardown. This complete
program runs end to end under the docs conformance suite.
```oxdock
IMPORT [STD, SSH]
LET $m: MAP = SSH_SERVE("doc-ssh-demo", {username: "u", password: "p"})
LET $in: PIPE
LET $out: PIPE
LET $w: HANDLE = ASYNC {
LET $sess: MAP = SSH_DEQUEUE($m.server)
ASSERT_CONTAINS $sess "session"
ASSERT_CONTAINS $sess "command"
ASSERT_CONTAINS $sess "username"
ASSERT_CONTAINS $sess "addr"
SSH_PUMP_CHANNEL($sess.session, $in, $out)
}
LET $cin: PIPE
LET $cout: PIPE
LET $c: HANDLE = ASYNC { SSH_CONNECT("doc-ssh-demo", "u", "p", $cin, $cout) }
WITH_IO [stdout=$in] ECHO "server-greeting"
LET $info: MAP = INSPECT($cout)
LET $empty: BOOL = $info.buffer_bytes == 0
WHILE $empty {
SLEEP 100ms
$info = INSPECT($cout)
$empty = $info.buffer_bytes == 0
}
ASSERT_CONTAINS $cout "server-greeting"
AWAIT $w
CANCEL $c
SSH_CLOSE($m.server)
```
### SSH_PTY_RUN
**Signature:** `SSH_PTY_RUN($session, $argv, $rows: INT, $cols: INT, $in_pipe, $out_pipe) -> INT`
**Contexts:** AST only
Run a command under a sized local terminal into pipes.
Run `argv` under a local pseudo-terminal sized from the dequeued
session and pump it through explicit pipes until the child exits.
`rows`/`cols` seed the initial size when positive; non-positive falls
back to the session's requested size (the outer pty request, 24x80
default). Outer window-change requests resize this session's terminal
live; every session owns its size cell, so concurrent guests never
observe each other. Must run inside `ASYNC`. Returns the INT exit
code. Environment is inherited from the host process and layered with
the script environment like `RUN`: block-scoped `ENV` (such as the
session's `SSH_USER` / `SSH_CLIENT` / `SSH_SERVER` / `SSH_COMMAND`
relay) reaches the child; the working directory comes from the script.
### SSH_PUMP
**Signature:** `SSH_PUMP($from_pipe, $to_pipe) -> INT`
**Contexts:** AST only
Copy one pipe into another until EOF.
Copy one pipe into another until EOF, then close the target.
Returns the INT byte count. Either task placement works, as long as
the other end is live (usually an `ASYNC` task).
### SSH_PUMP_CHANNEL
**Signature:** `SSH_PUMP_CHANNEL($session, $in_pipe, $out_pipe) -> MAP`
**Contexts:** AST only
Pump a dequeued SSH session through pipes.
Pump a dequeued session between explicit DSL pipes until the channel
closes. Must run inside `ASYNC`. The session ends are take-once: a
second pump on the same session bails instead of splitting bytes.
Returns a MAP with `closed` (BOOL).
### SSH_SERVE
**Signature:** `SSH_SERVE($bind: STRING, $options) -> MAP`
**Contexts:** AST only
Serve SSH on a virtual service endpoint.
## Value types
### Value type: SSH_SERVER
Handle to one ephemeral SSH server instance.
Minted by `SSH_SERVE`, consumed by `SSH_ACCEPT` and `SSH_CLOSE`.
Cloning the value shares the server; dropping the last clone
signals shutdown.
### Value type: SSH_SESSION
Handle to one dequeued SSH session instance.
Minted by `SSH_DEQUEUE`, consumed once by `SSH_PUMP_CHANNEL`. Cloning
the value shares the session; metadata reads never consume. Display
shows id and peer only: the command string may carry secrets.
## License
`oxdock-ssh-plugin` is distributed under the terms of the [Apache License (Version 2.0)](https://github.com/jzombie/rust-oxdock/blob/main/LICENSE).