Workspace file transfer
Copy your working files to a runner, then start a supervised job against the completed transfer. Repeated pushes leave unchanged files and their modification times untouched. Pull copies runner files back to the client through the same transfer path, reversed: same selection, verification, records and recovery. You can recover every transfer and job by their IDs.
Workspace transfer is built on Portal Sync, the transfer engine that resolves the selection, plans the change set, moves payload over the authenticated connection, and checks delivered bytes before reporting completion.
Compared with rsync and scp
rsync and scp copy files over SSH, and the copy ends with the session. Workspace transfer runs over ax's authenticated connection and adds selection safety, verified delivery, records and job binding:
| Workspace transfer | rsync | scp | |
|---|---|---|---|
| Selection | Git-aware, with exclusions and sensitive-name protection | file lists and globs you provide | paths you provide |
| Delivery check | delivered bytes verified before completion | optional with --checksum |
none |
| Large-file edits | content-defined delta against the destination's older copy | fixed-size block delta | the whole file again |
| Interruption | retained transfer ID; repeating it recovers the outcome | resume with partial files and care | restart from scratch |
| Records | recorded status per transfer | none | none |
| Ties to a job | run --workspace binds the transfer to a supervised job |
separate step | separate step |
| Deletion | limited delete on push; no full mirror | --delete mirroring |
none |
Workspace transfer is not a synchronization daemon: nothing runs continuously, and rsync remains the right tool for arbitrary mirroring.
Workspace policy
The first push or pull — including ax exec run --workspace — sets up the
workspace policy automatically before transferring, so there is nothing extra
to run. Setup uses the selected profile’s existing root key and records the
client and runner’s access. File contents travel over Aster RPC; this does not
start continuous file synchronization.
A --dry-run preview never changes anything, so it does not provision the
policy either: on a fresh runner it reports missing setup instead of creating
one. To review a dry run before your first transfer, prepare the policy first
with ax workspace setup --node build. Repeating setup keeps that policy, and
a revoked or deleted policy record is not reactivated by setup.
Preview and push
Push copies into a directory on the runner. Choose an existing one that the execution account can use, and create it through a normal job if needed:
ax exec run --node build --follow -- /bin/mkdir -p /home/builder/project
Run these on the client computer:
ax workspace push --node build --source . --to /home/builder/project --dry-run
ax workspace push --node build --source . --to /home/builder/project
The preview lists planned changes, source selections and exclusions, and free space on the destination. It does not publish files or change the destination. The real push returns after every selected path has completed. Files sent over the connection are checked against their delivered bytes; unchanged files may use filesystem identity and timestamps.
At a Git worktree root, selection defaults to tracked working-tree files,
including staged additions and local edits. Untracked files require an explicit
--include. Git history and toolchains are not copied. Use --directory to
select directory contents instead.
Sensitive names are excluded even when tracked. The filter applies to every
path segment, case-insensitively: names starting with .env, ending in .pem
or .key, and the exact names .ssh, .aws, .cloudflare, .gnupg,
id_rsa, id_dsa, id_ecdsa and id_ed25519. So config/.env.production
and nested/.aws/credentials are both skipped. --allow-sensitive relative/path lets one exact relative path through (repeatable); it never
disables the filter, and --include does not override it. Repository
internals such as .git stay excluded even when named. Use repeatable
--exclude patterns and --include paths to adjust selection, and review
the dry run.
Nothing is deleted by default. --delete currently removes tracked paths that
are missing from the Git working tree. Use repeatable --delete-path path for
explicit source-absent paths. This first version does not perform a full mirror
of arbitrary destination contents.
Run with your files
ax exec run --node build --cwd /home/builder/project --workspace . -- cargo test
This records the request, publishes the selected input, waits for transfer
completion and binds that directory to the allocated job before launch.
Submission then returns the normal job receipt. Use --follow, or recover the
job later with ax exec status and ax exec output.
--workspace selects the local files; --cwd selects their remote destination.
If --cwd is omitted, ax uses the saved runner default, which may be the account's
home directory. Use a project directory when that default contains the runner's
private state. Ax reports both remote paths if they overlap.
The combined form uses the default source selection. For custom exclusions, push explicitly first, then run against the remote directory. Ordinary jobs use the account’s filesystem access; the workspace is not a sandbox.
Pull files back
Pulls run the same transfer in the other direction: the runner serves the source and your client receives into a local directory. Bring build outputs or logs home with:
mkdir -p results
ax workspace pull --node build --from /home/builder/project --to ./results --dry-run
ax workspace pull --node build --from /home/builder/project --to ./results
--from is an absolute path on the runner; --to is a local directory that
must already exist. The dry run reports the selection the runner resolved, the
planned changes and free space on the local destination. The real pull returns
after every selected path has completed.
Selection resolves on the runner. At a source root with a .git directory it
defaults to tracked working-tree files, as with push; --directory selects
directory contents and --git states the Git selection explicitly. The same
sensitive-name exclusions, --exclude and --include patterns, and
--allow-sensitive overrides apply on the way back. There is no --delete for
pulls; nothing is removed from the destination.
Pulls use the same IDs, records and status as pushes: --id names one pull,
and repeating it returns or waits for the recorded outcome without copying
again. The default --durability reconstructible completes without syncing
each local file; --durability durable syncs and audits the result. See
Input durability below.
How transfer moves bytes
Connection. Transfers use the peer connection between your client and the runner — the same authenticated endpoint and identity as execution and messaging, over Portal's QUIC transport. No SSH session is opened for file movement. Payload streams run on their own protocol connections through that endpoint, so a large transfer does not block job status, output or cancellation calls.
Delta. Pushes and pulls are delta transfers. When the destination already holds an older copy of a large file, the receiving side splits that copy into content-defined chunks and sends their identifiers; the sending side chunks the current file the same way and transmits only the chunks that changed. The reassembled file is checked against its whole-file digest before the transfer reports completion. Chunk sizes scale with file size, from a few KiB up to 96 KiB for multi-gigabyte files. SQLite databases are matched block by block at their declared page size, which resends less than content-defined chunks when edits stay in place.
When the whole file is sent instead. Files under 1 MiB are sent whole — the extra negotiation would cost more than it saves — as are files with no older copy at the destination. On a fast link the receiver also skips the delta for large files: chunking costs about 1 GiB/s per core, so once the transfer's own observed rate exceeds roughly half of that, resending the file is faster. The receiver caches chunk indexes of files it delivers, so a later edit of the same file skips the re-chunking pass. Unchanged files are recognised from filesystem identity and timestamps and contribute zero file payload. The first push sends the tree's payload in full; deltas apply from the second transfer onward. The Recover and measure counters below report which of these paths each transfer took.
Recover and measure
Ax generates a transfer UUID automatically and prints it before starting. Supply
--id to choose one, and keep it when retrying an uncertain transfer. Uppercase
UUIDs, such as macOS uuidgen output, are accepted and printed in lowercase.
run --workspace derives the transfer UUID from its request ID while preserving
the original Exec request ID and request digest:
ax workspace status --node build TRANSFER_ID --format json
ax workspace push --node build --source . --to /home/builder/project --id TRANSFER_ID
An ID names one transfer request and source publication. Repeating it checks that the options match and returns or waits for the recorded outcome; it does not copy files again. A completed status is historical, so it does not establish that the destination still contains those files. Use a new ID to send changes.
If the broker or runner restarts during transfer, inspect the recorded status. A lost live source publication cannot be recaptured under the same ID. Reconcile any associated job before starting another transfer. For a run, keep the original request ID and arguments; an exact retry recovers its existing job.
Results report fetched_payload_bytes, delta_copied_bytes, delta_paths,
delta_basis_bytes, delta_cached_bases, delta_skipped_fast_link, elapsed time
and observed payload rate. Reused bytes come from files already present at the
destination. An unchanged transfer can send zero file payload. These counters
exclude QUIC overhead and shared protocol traffic. They are not wire-byte
measurements. Interrupted accounting may be incomplete after a host crash.
Structured commands support --format json, csv and tsv. Transfer completion
and job completion are separate results. A disconnected client does not cancel
an accepted transfer or job.
Input durability
Transfers use --durability reconstructible by default. This completes delivery
without syncing each destination file to disk or auditing the whole result as
a snapshot. The serving side publishes the selected source files directly and
retains no immutable recovery copy. Avoid editing them during a transfer: the
source is your client for a push and the runner for a pull.
Use --durability durable to sync prepared data and audit the selected result
before completion. Keep the same mode when retrying an ID. Neither mode makes
the directory an atomic filesystem snapshot. After a power failure, inspect
the transfer and job, then use a new transfer to establish current inputs.
Recovery never automatically reruns a job.
The live transfer protocol requires matching current client and runner builds.
Restart the local broker after upgrading: ax exec daemon restart.
Notes and limits
Workspace transfer supports runners installed under the SSH user. System installations with a separate payload account are not yet qualified.
After upgrading an older installation, policy writes may require a one-time migration. Upgrade every client and runner using that workspace policy, then run:
ax workspace migrate --node build
This checks each admitted workspace node's live layout support before moving the existing records. An unreachable or incompatible node blocks migration. New installations do not need this step.