sftp
SSH file transfer: an SSH session to one peer, its host key verified against bound known-hosts rows before any credential is used and never trusted on first use, and the seven-verb file surface of file_surface.md (list, stat, get, put, rename, delete, mkdir) over the SFTP v3 subsystem, with resumable transfers, size and time bounds and a bound root no path and no link escapes. Every decision is a pure function of its arguments; the session and the seven verbs are impure and gated by the net capability. The transport is libssh2 behind the V fork with mbedTLS its crypto backend.
sftp:resolve
[$sftp:resolve] -> string — Resolve a path against the bound root, lexically and normalizing; a path that resolves outside the root is refused CXER6705, and an absolute path is held to the same confinement.
[?lib 'cx-platform/sftp']
[$sftp:resolve '/srv/drop' 'in/../../etc/shadow']
cx-err:CXER6705
sftp:resolve-pair
[$sftp:resolve-pair] -> element — Resolve both paths of a rename against the root, answering [pair from= to=]; either side escaping is CXER6705.
[?lib 'cx-platform/sftp']
[$sftp:resolve-pair '/srv/drop' 'in/orders-1.csv' '../../tmp/orders-1.csv']
cx-err:CXER6705
sftp:link-target
[$sftp:link-target] -> string — Resolve a link entry's target against the root (absolute as the peer's own path, relative from the link's directory); a target outside the root is CXER6706, the peer's escape.
[?lib 'cx-platform/sftp']
[$sftp:link-target '/srv/drop'
[entry name='in/nightly' kind=:link target='/etc/shadow']]
cx-err:CXER6706
sftp:verify-host-key
[$sftp:verify-host-key] -> bool — Verify a presented [host-key algorithm= fingerprint=] against the bound known-hosts rows for the host and port: true, CXER6702 for no row (never a first-use pin), or CXER6701 naming the expected and presented fingerprints (never a re-pin).
[?lib 'cx-platform/sftp']
[$sftp:verify-host-key [known-hosts] 'drop.example.test' 22
[host-key algorithm=ssh-ed25519 fingerprint='sha2-256:9f2b47c1']]
cx-err:CXER6702
sftp:check-binding
[$sftp:check-binding] -> element — Report a binding's findings — no root (CXER6717), no known-hosts handle, no host — as [findings]; a credential VALUE in the binding is refused CXER6310.
[?lib 'cx-platform/sftp']
[$sftp:check-binding
[binding host='drop.example.test' port=22 root='/srv/drop'
known-hosts='drop-known-hosts' auth=public-key
private-key='-----BEGIN OPENSSH PRIVATE KEY-----']]
cx-err:CXER6310
sftp:check-name
[$sftp:check-name] -> string — Answer a name that is one path segment; a separator, CR, LF or NUL is refused CXER6707 where the name enters.
[?lib 'cx-platform/sftp']
[$sftp:check-name 'orders-1.csv\nX-Injected: 1']
cx-err:CXER6707
sftp:check-bounds
[$sftp:check-bounds] -> bool — Check an observation against the bounds, every bound inclusive: entries (CXER6713), octets seen (CXER6714), and with an instant the duration and the stall floor (CXER6715).
[?lib 'cx-platform/sftp']
[$sftp:check-bounds
[bounds max-entries=10000 max-bytes=4294967296 max-seconds=3600 stall-seconds=60]
[observed entries=10001]]
cx-err:CXER6713
sftp:bind-bounds
[$sftp:bind-bounds] -> element — Answer the effective bounds, each the lower of the ceiling and the deployment's; a deployment raising a bound is refused CXER6713.
[?lib 'cx-platform/sftp']
[$sftp:bind-bounds
[ceiling max-entries=10000 max-bytes=4294967296 max-seconds=3600 stall-seconds=60]
[bounds max-entries=1000000]]
cx-err:CXER6713
sftp:classify
[$sftp:classify] -> any — Place an SFTP [status] in the file surface's nine classes: a completion for OK and EOF, otherwise the class's err carrying class= and retryable=; an unclassified status is :protocol and not retryable.
[?lib 'cx-platform/sftp']
[$sftp:classify [status code=3 verb=get message='Permission denied']]
cx-err:CXER6709
sftp:plan-transfer
[$sftp:plan-transfer] -> element — Plan a transfer from the request and the partial already held: offset, remaining, resumed, complete; a partial longer than the source, one with nothing to compare, or one whose address is not the source's is CXER6716.
[?lib 'cx-platform/sftp']
[$sftp:plan-transfer
[request path='in/orders-1.csv' size=2048 address='sha2-256:ab12cd34']
[partial bytes=1024 address='sha2-256:99999999']]
cx-err:CXER6716
sftp:read-listing
[$sftp:read-listing] -> element — Read the peer's [names] as a complete [listing]: every name checked, every link's target resolved against the root, a refusal never a silently dropped entry.
[?lib 'cx-platform/sftp']
[$sftp:read-listing '/srv/drop'
[names
[attrs name='orders-1.csv' type=:regular size=2048 mtime='2026-09-14T09:00:00Z']
[attrs name='ok\r\nDELE *' type=:regular size=1 mtime='2026-09-14T09:00:00Z']]]
cx-err:CXER6707
sftp:read-entry
[$sftp:read-entry] -> element — Read one path's [attrs] as the [entry] of what the entry IS — a link is a :link with its target checked against the root.
sftp:open
[$sftp:open] -> element — Open a session for a binding: dial, verify the presented host key against the known-hosts rows the caller hands in opts, and only then authenticate with the resolved credential (opts credential:, passphrase:) and start the subsystem. Idempotent per binding.
[?lib 'cx-platform/sftp']
[$sftp:open [binding host='drop.example.test' port=22 root='/srv/drop'
known-hosts='drop-known-hosts' auth=public-key credential='drop-key']]
cx-err:CXER0271
sftp:close
[$sftp:close] -> null — Close a session (idempotent).
sftp:list
[$sftp:list] -> element — List one directory beneath the root as a complete, bounded [listing]; a directory over the entry bound is refused CXER6713 after reading one entry past it.
[?lib 'cx-platform/sftp']
[$sftp:list [sftp-session] 'in']
cx-err:CXER0271
sftp:stat
[$sftp:stat] -> element — Answer the [entry] for one path by LSTAT — what the entry is, never what a link points at.
sftp:get
[$sftp:get] -> element — Transfer a file from the peer, resumably (opts partial:, address:), bounded by octets, seconds and the stall floor; answers the [transfer] with the octets as its [data]; a session lost mid-read is CXER6700 carrying the [partial] a resume starts from.
[?lib 'cx-platform/sftp']
[$sftp:get [sftp-session] 'in/orders-1.csv']
cx-err:CXER0271
sftp:put
[$sftp:put] -> element — Transfer octets to the peer, resumably; a fresh put never overwrites (an existing path is CXER6710) and a resume checks the far side's size against the partial first.
[?lib 'cx-platform/sftp']
[$sftp:put [sftp-session] 'out/orders-1.csv' [source path='in/orders-1.csv']]
cx-err:CXER0271
sftp:rename
[$sftp:rename] -> element — Rename one path to another, both resolved against the root; never over an existing target.
sftp:delete
[$sftp:delete] -> element — Delete one file or one empty directory (the kind is stat's answer); never recursive, and an absent path is CXER6708.
[?lib 'cx-platform/sftp']
[$sftp:delete [sftp-session] 'in/orders-1.csv']
cx-err:CXER0271
sftp:mkdir
[$sftp:mkdir] -> element — Make one directory, one segment; no implicit parents.
sftp:test-server
[$sftp:test-server] -> element — Boot the in-process SSH/SFTP v3 test server on 127.0.0.1 over an in-memory tree of [user], [dir], [file] and [link] rows (with stall-at=, drop-at= and status= faults), answering its port, host-key algorithm and fingerprint and each key user's generated private key.
sftp:test-server-stop
[$sftp:test-server-stop] -> null — Stop a test server and close its connections (idempotent).
sftp:test-server-file
[$sftp:test-server-file] -> any — Answer the octets a test server's tree holds at a path, or absence.