Skip to main content

Ssh

@swamp/sshv2026.07.28.1· 25d agoMODELS
01README

Run commands, copy files, and open port forwards across a declared set of hosts over SSH. Define the hosts once on the model, then target any subset by host name or tag (controlplane), an explicit name list, a CEL expression ('cel:"prod" in host.tags'), or all. Every host runs in parallel and records its own result resource, so individual outcomes are auditable and workflows can gate on them.

OpenSSH targets reuse long-lived connections via ControlMaster; Tailscale SSH targets authenticate with tailnet identity and bypass multiplexing. Hosts can mix both transports in one fleet, and each host can override the fleet defaults (user, port, identity, proxy jump, auth mode, and more).

Highlights

  • Define your fleet once via globalArguments.hosts[] — each host can override the fleet-default transport (kind, user, port, identity, etc.).
  • Mix OpenSSH and Tailscale hosts in the same fleet.
  • Per-host operations land as one resource each — run-<method>-<name> — tagged fleet / host / method / exitCode, so you can diff, gate workflows, query the audit trail by tag, and read a run's outcome straight off the returned data handle.
  • exec and script take an optional okExitCodes for guard commands whose non-zero exit is an answer, not a failure — an array replacing the default [0], or "any".
  • Selector forms: "all", an explicit name list ["web-1"], a bare host name or tag (controlplane), name:/tag:/cel: prefixes, or a cel: predicate (e.g. 'cel:"prod" in host.tags'). A bare CEL string is deprecated — prefix it with cel:.
  • CEL helpers bundled on every call: matchesRegex(s, pat), cidrContains(cidr, addr).
  • resolve answers "which hosts does this selector match?" as pure data — no SSH connection, zero matches is a success with count: 0, and the write's count tag lets a runModel caller gate without reading the resource.
  • Auth modes: key-based (file, agent, IdentityAgent, inline content via identityContent) and password-based via sshpass -e with the password supplied through ${{ vault.get('<vault>', '<key>') }}.

Methods

Method What it does
apply Materialise the host list as host-* resources; deletes stale entries that disappeared from hosts[].
resolve Resolve a host selector to the matching fleet members without connecting — writes a selection resource (addressing + tags/attrs, never credentials). Zero matches is data (count: 0), not an error.
open Establish ControlMaster sockets for selected ssh hosts (no-op for tailscale).
check Probe ControlMaster (ssh -O check) or run true over tailscale ssh.
close Tear down ControlMaster sockets (ssh -O exit).
exec Run a single shell command on each selected host; optional sudo: true prefixes sudo -n --; optional okExitCodes widens which exits count as success.
script Pipe a multi-line script over stdin to sh -s -- (or chosen interpreter) — no remote-side shell-quoting from us. Takes okExitCodes too.
copy scp (or rsync -e ssh) files to/from each selected host.
forward Local/remote port forwarding via the master, or detached tailscale ssh -N -L child for tailscale hosts.
collect-host-public-key Read and validate a remote host's SSH public key file; emit algorithm, fingerprint, and raw key as a hostPublicKey resource.

Exit codes

A host fails when it exits with a code that isn't allowed, is killed by a signal, or fails to spawn — and one failed host fails the method. By default the only allowed code is 0.

exec and script take an optional okExitCodes to widen that:

Value Meaning
omitted Only exit 0 succeeds. The default.
[0, 1] Exit 0 and 1 succeed; everything else fails.
[1] Exit 1 succeeds and exit 0 fails — the array replaces the default, it does not extend it.
"any" Every exit code succeeds.

Codes must be integers in 0..255. Spawn errors, timeouts, and signal kills still fail the host whatever the setting — they produce no exit status for a guard command to interpret. An allowed exit code is not a failure and does not trip failFast. Prefer an explicit list to "any": OpenSSH reports its own errors (unreachable host, rejected key) as exit 255, and "any" accepts those too.

# `test -f` answers 0 or 1; both are legitimate answers, not failures.
swamp model method run awesome exec \
  --input hosts=all \
  --input command='test -f /etc/nginx/nginx.conf' \
  --input okExitCodes:json='[0, 1]' --json

Results and tags

Each selected host records a run-<method>-<host> resource with the verbatim exitCode, and the method returns one data handle per host in fleet order. An allowed non-zero exit leaves the resource's error field unset. Resources are written before any error is raised, so they are there whether the method succeeded or failed.

Every run-<method>-<host> write is tagged:

Tag Value
fleet globalArguments.name.
host Host name from the fleet definition.
method exec, script, or copy.
exitCode Exit code as a string ("0", "1"). Absent when the process was killed by a signal or failed to spawn.

A model calling this one via context.runModel() gets those handles back as result.resources on success, and can branch on handle.tags.exitCode === "0" without fetching resource content — which is the point of okExitCodes, since a thrown method returns { ok: false, error } with no handles at all. In workflow CEL, data.findByTag("host", "web-1") selects by the same tags.

See the bundled README for worked examples and per-command recipes.

Output capture

Captured by default, opt out at the fleet (captureOutput: false) or per call. No cap — stdout/stderr land verbatim in the matching run-<method>-<host> resource.

Requirements

  • OpenSSH client (ssh, scp) on the runner.
  • rsync if any copy call sets useRsync: true.
  • tailscale if any host uses the tailscale transport.
  • sshpass if any host uses auth.kind: password.
  • The swamp host must expose ctx.createCelEnvironment() on the extension model context (introduced 2026-05).

Usage

swamp extension pull @swamp/ssh

# Create the fleet, then author its globalArguments (transport + hosts).
swamp model create @swamp/ssh awesome
swamp model edit awesome

# Run methods — every selector-taking method requires a `hosts` argument.
swamp model method run awesome open --input hosts=all --json
swamp model method run awesome exec \
  --input hosts='tag:prod' \
  --input command='uptime' --json

See the bundled README for the full configuration shape, transport options, authentication modes, and worked examples.

02Models1
@swamp/sshv2026.07.28.1ssh.ts
fn apply()
Materialize the host list as host-* resources and prune stale ones.
fn resolve()
Resolve a host selector to the matching fleet members without
fn open()
Establish ControlMaster sockets for selected ssh hosts (no-op for tailscale).
fn check()
Probe connectivity: ssh -O check for ssh hosts, `true` over tailscale ssh.
fn close()
Tear down ControlMaster sockets (ssh -O exit).
fn exec()
Run a single shell command on each selected host.
fn script()
Pipe a multi-line script over stdin to sh/bash/python3 on each host.
fn copy()
scp (or rsync) files to/from each selected host.
fn forward()
Open/cancel/list a port forward (ssh -O forward, or detached tailscale child).

Resources

host(infinite)— One record per fleet member, written by `apply`. Tagged with the
runResult(infinite)— Per-host outcome of a single method invocation (exec/script/copy).
forwardState(infinite)— Tracks an open port forward — pid for tailscale, ControlPath for ssh.
masterAudit(infinite)— ControlMaster open/check/exit events, per host.
hostPublicKey(infinite)— Observed SSH host public key. One per host, written by
selection(infinite)— Result of a `resolve` call: the selector, its matched fleet
03Previous Versions14
2026.07.27.2
2026.07.27.1
2026.07.24.1
2026.07.02.1

Modified 1 models

2026.06.27.1

Added 1, removed 1 models

2026.06.04.2
2026.06.04.1
2026.06.03.1
2026.06.01.2
2026.06.01.1
2026.05.29.2
2026.05.29.1
2026.05.25.1
2026.05.19.1
04Stats
A
100 / 100
Downloads
646
Archive size
76.8 KB
Verified by Swamp
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License declared1/1earned
  • Verified public repository2/2earned
05Platforms
06Labels