-
-
Notifications
You must be signed in to change notification settings - Fork 42
Transport Layer
The ServerTransport protocol unifies local and SSH I/O. Services consume transport.readFile(path), transport.runProcess(...), transport.snapshotSQLite(path), etc., without caring whether the bytes come from disk or the wire. Two implementations live under scarf/scarf/scarf/Core/Transport/: LocalTransport and SSHTransport.
ServerTransport.swift exposes:
Identity
-
contextID: ServerID— UUID; namespaces caches under~/Library/Caches/scarf/snapshots/<id>/. -
isRemote: Bool— true forSSHTransport.
File I/O
readFile(_ path) -> Data-
writeFile(_ path, data:)— atomic via temp + swap; preserves0600mode for.env/auth.json/*-tokens.json. fileExists(_ path) -> Bool-
stat(_ path) -> FileStat?— size, mtime, isDirectory. listDirectory(_ path) -> [String]-
createDirectory(_ path)— idempotent, creates intermediates. -
removeFile(_ path)— idempotent.
Processes
-
runProcess(executable, args, stdin, timeout) -> ProcessResult— blocking; captures stdout/stderr; SIGTERM on timeout. -
makeProcess(executable, args) -> Process— pre-configured but not yet started; caller owns lifecycle (used byACPClient).
SQLite snapshots
-
snapshotSQLite(remotePath) -> URL— local: returns the path unchanged. Remote:sqlite3 .backupon the remote, scp the result down, return a local URL into the snapshot cache.
Watching
-
watchPaths(_ paths) -> AsyncStream<WatchEvent>— yields.anyChangedon any change. Local: FSEvents (DispatchSourceFileSystemObject). Remote: 3-second mtime polling.
TransportErrors.swift defines TransportError:
| Case | Cause |
|---|---|
hostUnreachable(host, stderr) |
DNS, connection refused, no route. |
authenticationFailed(host, stderr) |
SSH key not loaded or rejected. |
hostKeyMismatch(host, stderr) |
~/.ssh/known_hosts mismatch. |
commandFailed(exitCode, stderr) |
Remote command exited non-zero. |
fileIO(path, underlying) |
Local FS error. |
timeout(seconds, partialStdout) |
Hit timeout parameter. |
other(message) |
Catch-all. |
Stderr-pattern classification turns raw ssh errors into the right case so the UI can render actionable text.
LocalTransport.swift — a thin wrapper around FileManager, Process, and DispatchSourceFileSystemObject.
-
Atomic writes: writes to
<path>.scarf.tmp, sets0600if the filename suggests a secret, thenreplaceItemAt(existing) ormoveItem(new). -
Process timeout: polls every 100ms until deadline;
terminate()if exceeded. -
Watching: opens each path with
O_EVTONLY, creates a dispatch source for.write/.extend/.rename, yields.anyChangedon event. - Snapshot: no-op — returns the path unchanged.
SSHTransport.swift — every primitive becomes an ssh/scp/sftp invocation, multiplexed over a single ControlMaster connection.
Without ControlMaster, every remote call re-authenticates (500ms-2s each). With it, the first call sets up the master socket; subsequent calls reuse the same TCP+crypto session at ~5ms each.
The SSH option set is constructed by sshArgs(extra:):
-o ControlMaster=auto
-o ControlPath=~/Library/Caches/scarf/ssh/%C
-o ControlPersist=600 # keep alive 600s after last use
-o ServerAliveInterval=30 # keepalive every 30s
-o ServerAliveCountMax=3 # disconnect after 3 missed
-o ConnectTimeout=10
-o StrictHostKeyChecking=accept-new
-o LogLevel=QUIET # binary-clean stdin/stdout for JSON-RPC
-o BatchMode=yes # ssh-agent only; never prompt
%C hashes (host, user, port) — multiple Scarf windows for the same host share one socket. closeControlMaster() issues ssh -O exit for clean shutdown.
Two helpers prevent shell-expansion breakage:
-
shellQuote(_:)— wraps unsafe strings in single quotes, escaping embedded singles as'\''. Safe characters (alphanumerics +@%+=:,./-_) pass through unquoted. -
remotePathArg(_:)— converts~/...to$HOME/...(because shells don't expand~inside quotes) and double-quotes so$HOMEexpands but spaces don't break.
-
readFile:ssh host -- sh -c 'cat <path>'; classifies "No such file" into typedfileIO. -
writeFile: scp to<path>.scarf.tmp, then remotemv— atomic; cleans the orphan on failure. -
stat: tries GNUstat -c "%s %Y %F", falls back to BSDstat -f "%z %m %HT". -
listDirectory:ls -A <path>.createDirectory:mkdir -p.removeFile:rm -f.
-
runProcess: wraps<exe> <args>insh -cso paths can use$HOME. InheritsSSH_AUTH_SOCKfrom the user's GUI environment so 1Password / Secretive agents work. -
makeProcess: returns/usr/bin/ssh -T <opts> host -- sh -c '<exe> <args>'. The-Tdisables PTY allocation so stdin/stdout stay binary-clean for JSON-RPC.
The trickiest operation. The remote runs:
sqlite3 "$HOME/.hermes/state.db" ".backup '/tmp/scarf-snapshot-XYZ.db'" && \
sqlite3 '/tmp/scarf-snapshot-XYZ.db' "PRAGMA journal_mode=DELETE;"
.backup is WAL-safe — it captures a consistent snapshot without blocking writers. The PRAGMA journal_mode=DELETE strips WAL mode so the snapshot is self-contained (no -wal/-shm sidecars). scp pulls it to ~/Library/Caches/scarf/snapshots/<id>/state.db. The remote temp is removed.
3-second polling: the remote runs a one-liner concatenating mtimes for the watched paths, hashed into a signature. When the signature changes, the stream yields .anyChanged. Transient connection drops are tolerated.
-
sqlite3for the snapshot operation. -
pgrepfor the Dashboard's "is Hermes running" check. -
~/.hermes/readable by the SSH user.
See Servers & Remote for setup and troubleshooting.
Last updated: 2026-04-20 — Scarf v2.0.1
Wiki edited via the local .wiki-worktree/ clone. See Wiki Maintenance for the workflow. Last sync: 2026-04-20.
Getting Started
ScarfGo (iOS)
User Guide
- Dashboard
- Insights & Activity
- Chat
- Slash Commands
- Memory & Skills
- Projects & Profiles
- Project Templates
- Template Catalog
- Template Ideas
- Platforms / Personalities / Quick Commands
- Servers & Remote
- MCP, Plugins, Webhooks, Tools
- Gateway / Cron / Health / Logs
Architecture
- Overview
- Core Services
- Design System
- Data Model
- Transport Layer
- ScarfCore Package
- Sidebar & Navigation
- ACP Subprocess
Developer Guide
Reference
Troubleshooting
Contributing
Release History
Legal & Support
Unsorted