After Dark Systems / Service Automation HubCompletely Programmable Infrastructure

OpenSAH / Suite utilities

The SAH utilities: what exists, and what each is for.

The suite is a set of small command-line programs that an operator or an agent runs on their own machine against hosts enrolled in SystemAPI. Each does one job. This page is the map: every name, its job, its state today, and which one to reach for.

The map

sah means Service Automation Hub. The hub is the platform (SystemAPI, ComputeAPI, the execution shell adssh) plus this suite of utilities. Every utility has its own repository, version and release lifecycle. Five are usable end to end today, one of them from a repository that is not public yet; one works between two machines you control but is not yet joined to the hub; one is a library with no command; one name is an idea and one is reserved.

ToolJobStateVersion
opensahLocal index. Lists every suite name with its role and state, and checks which binaries are on PATH.Available0.1.0
sahconnectLog in, open a shell on a host through its agent, call the SystemAPI HTTP API, or pass arguments to adssh.Available0.2.0
sahapiA loopback HTTP service with the suite catalog and three read-only SystemAPI proxies, for local tools and agents.Available0.1.0
sahlinkStart a process and capture its stdio. A TCP forward through a host's agent is written but not yet published.Available0.1.0
sahsyncCopy and sync files and directories through a host's agent. The scp, sftp and rsync-over-SSH replacement.Available0.1.0, repository not public yet
sahxferMove one large file fast on its own encrypted UDP plane, once a grant allows it.Early developmentunversioned
sah2grpcgRPC debug bridge: a daemon and a client that prints the hub's decisions.Early developmentno binary yet
sahmeshAn idea. One design note, no code.Idea—
sahcoreReserved name. No role, binary or service.Unassigned—

opensah tools prints the same names on your machine with its own short state words (built, optional, early, not_built, external) and adds the platform names adssh, systemapi and computeapi.

Which one do I reach for?

  • I want a shell on a host. sahconnect. The host's outbound agent runs the terminal, so the host needs no inbound SSH.
  • I want to call the SystemAPI HTTP API from a script. sahconnect api with a token or an API key. A local program that should not hold the key talks to sahapi instead.
  • I want to copy a few files, a directory, or keep a tree in sync. sahsync. It is audited, refuses symlink tricks, and plans before it deletes.
  • I want to move one big file as fast as the network allows. sahxfer. One file, one direction, one grant, its own UDP data plane.
  • I want to reach a port on a host, such as a database or RDP. sahlink's TCP forward, the local half of ssh -L without SSH, once the branch that adds it is published.
  • I want to record exactly what a program I start reads and writes. sahlink run.
  • I want to see which suite tools are installed here. opensah ready.
  • I want to drive and inspect SAH decisions from a test harness. sah2grpc, once it has a binary.
  • I want to run commands under policy, with approvals and an audit chain. That is the shell, adssh, not a suite utility.

sahsync and sahxfer are not the same tool. sahsync moves many paths over the agent's file channel, one operation at a time, with an audit line per change. sahxfer moves exactly one regular file and puts the bytes on a separate UDP path with its own encryption, loss repair and pacing. Neither one grows a second login: both use the token that sahconnect login stores.

opensah

Available 0.1.0

The local index. opensah tools prints one tab-separated line per suite name: name, state, role. opensah ready checks PATH and exits 1 when sahconnect, sahapi or sahlink (the built tools) is missing; a missing adssh, sahsync or sahxfer is reported without failing the check, and names without a binary are listed as no-binary. It does not start the other tools and does not call their APIs.

opensah tools
opensah ready
opensah version

Source and README ↗

sahconnect

Available 0.2.0

The client. sahconnect login gets a bearer token by OAuth device login (it prints a URL and a code; you open the URL and enter the code, so a forwarded link alone cannot approve a login) and stores it in ~/.config/sahconnect/token, mode 0600. sahconnect <host> then opens an interactive shell on that host: the host's outbound agent runs the PTY, so nothing has to listen on the host. Piped stdin runs without raw mode, and the exit code is the remote shell's. --root asks for a root shell and needs the terminal:root scope.

export SYSTEMAPI_URL=https://systemapi.example
sahconnect login --username alice     # prints a URL and a code
sahconnect web1                       # shell on web1
echo uptime | sahconnect web1         # one command, piped
sahconnect api GET /api/v1/fleet      # HTTP; SYSTEMAPI_API_KEY works too
sahconnect ssh -- version             # pass through to the installed adssh
sahconnect logout

sahconnect api makes authenticated SystemAPI HTTP requests. sahconnect ssh launches SAHCONNECT_ADSSH, or adssh on PATH, with your arguments; it does not set ADSSH_SAH. The other file tools reuse this login and this configuration.

Source and README ↗

sahapi

Available 0.1.0

A small HTTP service for the machine it runs on. It listens on a loopback address, checks a local bearer of 16 to 512 bytes, and serves a fixed suite catalog plus three fixed, read-only proxies into the operator's SystemAPI: fleet, alerts, and audit verification. /health is unauthenticated. It does not serve OAuth; device login stays in SystemAPI and dashboard login stays in Authentik. Use it when a local tool or agent should query the fleet without holding the SystemAPI key itself.

export SAHAPI_TOKEN='replace-with-16-or-more-bytes'
export SAHAPI_LISTEN=127.0.0.1:8787
export SYSTEMAPI_URL=https://systemapi.example
# Set SYSTEMAPI_API_KEY through your secret manager.
sahapi serve

Source and README ↗

Available 0.1.0 published

sahlink run -- <program> starts a program and records its stdin, stdout and stderr; a control socket on the local machine can read those streams and write to stdin, and an optional relay listens on loopback and dials one upstream host and port. It only sees the process it started: no attaching to an existing PID, no ptrace, and no sockets the child opens on its own.

Coming in 0.2.0, written but not yet pushed to GitHub: a TCP forward through the host's agent. sahlink web1:5432 listens on 127.0.0.1 and the agent on web1 dials 127.0.0.1:5432, the local half of ssh -L without SSH; sahlink rdp win1 opens that forward for RDP and prints the matching mstsc, xfreerdp and macOS open commands. Installing from GitHub today gives you 0.1.0 without it.

sahlink run -- /bin/echo ok      # stderr prints the socket and token-file paths

Captured output can contain secrets the child prints. Treat the capture like the terminal it came from.

Source and README ↗

sahsync

Available 0.1.0 Repository not public yet

File copy and sync through a host's SystemAPI agent: the SAH replacement for scp, sftp and rsync over SSH. A remote is HOST:PATH. put uploads one regular file, get downloads one (or a tree with -r), ls lists, and sync makes a destination tree match a source tree in either direction (--from pulls). It copies whole files with no rolling checksums. --delete without --yes only prints the plan, as does --dry-run; --create makes a missing destination. Every real change writes one audit op=… path=… size=… sha256=… line to stderr, and a refusal is one code word such as exists, symlink_refused or agent_unsupported. --root runs the host helper as root and needs the admin-only file:root scope.

sahsync put ./app.tar.gz web1:/srv/app/
sahsync get web1:/srv/app/note.txt ./
sahsync ls web1:/srv/app/
sahsync sync ./site/ web1:/srv/www/
sahsync sync --from web1:/srv/www/ ./site-copy/
sahsync sync --delete ./site/ web1:/srv/www/      # prints the plan only; add --yes to apply

How it joins. sahsync opens its own file channel on the host's agent (file:read, file:write or the admin-only file:root ticket, then one WebSocket session of sequential operations), separate from the terminal channel sahconnect uses. That channel is on SystemAPI's main branch and the suite contract lists sahsync as a wired join. A Linux agent needs a kernel with openat2; a macOS or other non-Linux agent answers agent_unsupported. The client carries import-path-only copies of sahconnect's client, config, token and secure-file packages, so login and configuration are the same. The GitHub repository is still private; ask for access until it is published.

sahxfer

Early development unversioned

Bulk transfer of one large file. SAH decides whether the transfer may happen; sahxfer moves the bytes. The decision is a grant: host, root, path, size, hash, direction, expiry and a per-transfer key. The data plane is UDP with a 64-byte header, ChaCha20-Poly1305 sealing, NACK-based loss repair and a BBR-style pacer, so a big file crosses a long path at the rate the path allows rather than one TCP stream's share. One regular file, one direction, one grant: no directories, no deltas, no resume. A failed transfer deletes its temp file and starts over. A Linux end opens paths with openat2 (no symlinks, no fallback), so a Linux host needs a kernel that has it.

What works today. The grant-file mode, between two machines you control: grant writes a bootstrap grant file (mode 0600, the only disk copy of the key), recv listens with that grant on the receiving host, and put or get runs on the other side. relay forwards sealed datagrams between the two and cannot decrypt them; in grant-file mode it reads the grant file and keeps only the transfer id, route token and byte cap. Every finished command prints the grant id, the transfer id and ok; stderr is a single code word, and --diag adds the socket-buffer sizes and a warning when the receive buffer is too small for the arrival rate.

# receiving host
sahxfer recv --grant g.json --listen 0.0.0.0:47100 --root /srv/inbox

# sending machine
sahxfer put --grant g.json --to 203.0.113.9:47100 ./image.bin

What does not work yet. The hosted path, sahxfer put --url https://sah.example …, asks the hub for a grant and expects the host's agent to spawn recv. SystemAPI already serves the grant route, but no agent advertises xfer yet, so every real host answers agent_unsupported; no relay is deployed; and only put over UDP is issued, so hosted get and the QUIC transport are refused. The suite contract lists sahxfer as partially wired. Agents will start recv as the host user; it does not drop privileges itself.

Source, README and PROTOCOL.md ↗

sah2grpc

Early development no binary yet

Debug and integration tooling for SAH over gRPC. The design describes sah2grpc, a daemon that binds loopback only and drives adssh sah for you (an Execute call spawns it, SubmitIdentity feeds it SAML, OIDC or passkey results), and grpc2sah, a client that prints the recorded decisions, so you can test policy, leases, identity flows and bad frames without hand-building frames. The design is accepted and the repository holds the protocol definitions and library packages, but no command has been built yet and nothing is wired: identity results stay not_wired until SAH consumes them.

Source and design ↗

sahcore

Unassigned

The name is reserved so it is not reused for a different product. There is no binary, module or service, and opensah ready lists it as no-binary. Nothing should be added there until a role is assigned.

Placeholder repository ↗

Install

Each installable tool is a Go module with a single command under cmd/; Go 1.26 or later builds them. Clone and install the ones you want, put GOBIN (or $(go env GOPATH)/bin) on PATH, then let opensah confirm what is there. The install guide walks through the three core tools and the shell.

for r in opensah sahconnect sahapi sahlink sahxfer; do
  git clone https://github.com/afterdarksys/$r.git
  (cd $r && go install ./cmd/$r)
done
opensah ready

No tool has a packaged release download yet; the versions above are the version strings on each repository's default branch.

How to read the states

  • Available. Built, tested, and listed as a wired join in the suite contract: it works against today's SystemAPI.
  • Early development. The program or library exists, but the hub or agent side it needs is not wired yet, so an end-to-end run is refused. opensah tools prints these as early.
  • opensah's built versus optional. opensah ready fails only when a built tool (sahconnect, sahapi, sahlink) is missing; optional tools such as sahsync and early ones such as sahxfer are reported without failing.
  • Unassigned. A reserved name with nothing behind it.

The availability and roadmap page tracks the joins between the platform pieces; this page tracks the utilities themselves.

Built on 2026-10-18 from each tool's README and source and from the SystemAPI suite contract (docs/sah-suite.md), which decides what counts as wired when a README disagrees.