Skip to content

Your first workspace

This tutorial walks you through AWC (Agent Workspace Cache) in one sitting. You bring up the local-dev smoke harness, then drive the whole loop yourself with awcctl: snapshot a directory, fork a workspace, mount it, make an edit and run a tool, checkpoint, diff, export, and unmount. AWC exposes five user-level primitives:

snapshot → fork → mount → checkpoint → diff/export

You touch each one. When you finish, you have a working workspace and a feel for the system.

Before you start

The harness runs on Linux only. You need:

  • Docker Engine with the docker compose plugin.
  • The Rust toolchain pinned by rust-toolchain.toml.
  • python3, curl, and coreutils.
  • A FUSE (Filesystem in Userspace) stack: the /dev/fuse device and fusermount or fusermount3 on PATH.
  • root privileges. The writable mount layer uses OverlayFS, which needs mount privilege. Run the bring-up with sudo.

See the local-dev guide for full prerequisites.

1. Bring up the local-dev smoke

The smoke harness stands up PostgreSQL and MinIO (an S3-compatible content-addressed store, or CAS) in containers, then runs the four AWC binaries on the host. It generates the credentials, builds the binaries, and proves the real cross-process paths work.

cd deploy/local-dev
bin/generate-credentials.sh
sudo ./smoke.sh

The first run takes a few minutes to build, then drives the full import, mount, checkpoint, and remount path. A green run ends with:

== SMOKE PASS ==

The smoke leaves awc-control, awc-worker, and awcd running. Keep this terminal open.

Development mode, loopback plaintext

The harness sets AWC_DEV_MODE=1, so internal RPCs (remote procedure calls) use loopback plaintext instead of mutually authenticated TLS (mTLS). Outside development mode, awcd and awc-control MUST present TLS credentials and fail to start without them. Do not reuse this mode in a deployment.

2. Point awcctl at the harness

Open a second shell in the local-dev directory. The smoke builds awcctl to ../../target/debug/awcctl. Put it on PATH, then load the CAS credentials, the dev identity, and the service token that awc-control minted on startup:

cd deploy/local-dev
export PATH="$PWD/../../target/debug:$PATH"
set -a; . config/worker.env; . config/control.env; set +a
export AWC_CONTROL_ENDPOINT="http://127.0.0.1:8080"
export AWC_HOST_SOCKET="$PWD/run/awcd.sock"
export AWC_TENANT="$AWC_DEV_TENANT" AWC_PROJECT="$AWC_DEV_PROJECT"
export AWC_TOKEN="$(grep -m1 'awc-control dev token:' run/control.log \
                   | sed 's/.*awc-control dev token: //')"

AWC_HOST_SOCKET is the Unix socket awcd serves, AWC_CONTROL_ENDPOINT is the control plane's REST (Representational State Transfer) address, and AWC_TOKEN is the bearer token the dev bootstrap minted.

3. Check host health

Ask the host daemon how it is. status reaches awcd over the socket and needs no control-plane call:

awcctl status

You see host health, the cache state, and any active mount sessions.

4. Snapshot a directory

A snapshot is an immutable, content-addressed record of a directory tree. The harness ships a tiny fixture at seed/source. Import it:

awcctl --output json snapshot create --from-dir seed/source --name first \
  > first-snap.json

--from-dir is the source and --name is an optional label. The command opens an import session over the control plane, uploads the unique objects to the CAS, and completes the import. Capture the id:

SNAP=$(python3 -c 'import json;print(json.load(open("first-snap.json"))["snapshot_id"])')
echo "$SNAP"

A successful snapshot MUST return a stable snapshot id, a root digest, and file and byte counts. The new snapshot starts in the VALIDATING state.

5. Wait for READY, then fork

A workspace can fork only from a READY snapshot. The worker validates the object closure against the CAS and advances the snapshot VALIDATING → READY. Poll until it is ready:

until [ "$(awcctl --output json snapshot inspect "$SNAP" \
          | python3 -c 'import sys,json;print(json.load(sys.stdin)["state"])')" = READY ]; do
  sleep 1
done

You can also list what the project holds:

awcctl snapshot list

Now fork a private workspace from the snapshot. Forking creates metadata only; it MUST NOT copy content objects:

awcctl --output json workspace fork --snapshot "$SNAP" --ttl 2h --name first-ws \
  > first-fork.json
WS=$(python3 -c 'import json;print(json.load(open("first-fork.json"))["id"])')
echo "$WS"

--ttl 2h sets a two-hour lifetime (required). --snapshot forks one workspace from the snapshot id you pass.

6. Mount the workspace

Mounting turns the workspace into a writable directory. The control plane issues a signed ticket; awcd redeems it, acquires an exclusive writer lease, and mounts an OverlayFS view at your target.

mkdir -p data/binds/first
awcctl --output json workspace mount "$WS" --target "$PWD/data/binds/first" \
  > first-mount.json
SESS=$(python3 -c 'import json;print(json.load(open("first-mount.json"))["mount_session_id"])')
echo "$SESS"

The target MUST fall under the host allow-list. awcd MUST NOT accept an arbitrary path from an untrusted caller without an allow-listed root. Confirm the kernel sees an OverlayFS mount:

findmnt -n -o FSTYPE --target data/binds/first     # prints: overlay

7. Edit a file and run a tool

Enter the mount. The fixture's hello.txt and README.txt are there, fetched from the CAS on demand.

cd data/binds/first
ls
printf 'my first edit\n' > notes.txt
grep -n "AWC" README.txt
cd -

You wrote a new file and ran an ordinary read tool against the merged tree. Any read-only tool works the same way.

8. Checkpoint

A checkpoint publishes your writes as a new immutable snapshot and advances the workspace head. Pass the mount session id, not the workspace id:

awcctl --output json workspace checkpoint "$SESS" \
  --mode cooperative --message "first checkpoint" > first-ckpt.json
HEAD=$(python3 -c 'import json;print(json.load(open("first-ckpt.json"))["snapshot_id"])')
echo "$HEAD"

Quiesced checkpoints need a fenced cgroup

The default is a quiesced checkpoint, which freezes an execution cgroup before it cuts the snapshot. This harness mounts without a ticket-sealed cgroup, so a quiesced checkpoint is rejected with quiesced checkpoint requires a ticket-sealed execution cgroup. Use --mode cooperative here. It is a best-effort cut with no freeze, and it is what the smoke exercises.

The checkpoint is durable only after the control plane atomically publishes the new head.

9. Diff and export

Compare your checkpoint with the original snapshot. A diff uses a Merkle-tree comparison, so unchanged directories are skipped without scanning their children:

awcctl snapshot diff "$SNAP" "$HEAD"

The diff lists each changed path. Export the changes to a file:

awcctl snapshot export "$HEAD" --against "$SNAP" --format json --out first-changes.json

$HEAD is the snapshot whose changes are exported. --against is the base. Pass --format json. The patch-bundle and tar formats are deferred, so the control plane serves only the inline json form today.

10. Unmount

Release the writer lease and tear down the mount:

awcctl workspace unmount "$SESS" --disposition discard-local

--disposition discard-local is the default. It drops any writes you did not checkpoint. Because you already checkpointed, nothing is lost here. The other dispositions are checkpoint (cut a checkpoint while unmounting) and preserve-local (leave the upper layer for recovery).

When you are done with the harness, stop its containers:

./clean.sh

What you learned

You ran all five AWC primitives against live services: snapshot into the CAS, metadata-only fork, writable OverlayFS mount behind a signed ticket, a checkpoint that advances the head, a diff and export, and an explicit unmount.

You also saw two candid limits of this harness: it runs without mTLS under AWC_DEV_MODE=1, and it checkpoints cooperatively because the mount has no fenced cgroup.

Where to go next: