Skip to content

Run locally

The local-dev harness is a reproducible, single-machine environment for running and exercising AWC end to end on a developer laptop. It stands up the external dependencies the AWC services rely on (PostgreSQL and an S3-compatible object store) in containers, runs the AWC binaries themselves on the host (so the code under development is what executes), generates the credentials and configuration they need, and runs a smoke script that proves the real cross-process paths work, including a byte-for-byte read of an imported snapshot through a real /dev/fuse base mount.

It targets the single-machine variant of the reference topology.

Topology

host
 ├── docker compose ──────────────────────────────────────
 │    postgres:16   control-plane metadata store (awc-db)
 │    minio         S3-compatible object store (tenant CAS target)
 │    minio-init    one-shot: creates the awc-cas bucket
 │ ────────────────────────────────────────────────────────
 ├── awc-control   control plane: REST + gRPC, migrates schema, dev-token
 ├── awc-worker    background worker: drains the job queue over the s3:// CAS
 ├── awcd          host daemon: Unix-socket gRPC + health HTTP + real FUSE base mount
 └── awcctl        CLI — drives the control plane + host daemon

All container ports bind to 127.0.0.1, so nothing is exposed off the host. The four AWC binaries are built once with their object-store and FUSE cargo features and then invoked directly. Cargo's default feature set would rebuild the wrong variants.

Prerequisites

  • Docker Engine and the docker compose plugin, with a reachable daemon.
  • The Rust toolchain pinned by rust-toolchain.toml (for the build step).
  • python3, curl, and common coreutils on the host.
  • Linux with a usable FUSE stack: /dev/fuse present, and fusermount (or fusermount3) on PATH. awcd mounts a real read-only FUSE base filesystem.
  • For non-root use: user_allow_other enabled in /etc/fuse.conf. The FUSE base mount uses AutoUnmount, which adds the allow_other FUSE option. FUSE only honours allow_other for non-root mounters when user_allow_other is set. As root this is not required.

Quick start

cd deploy/local-dev
bin/generate-credentials.sh     # writes .env, config/{awcd.toml,worker.env,control.env}
sudo ./smoke.sh                  # writable OverlayFS needs mount privilege; verifies end to end

A green smoke prints == SMOKE PASS == and leaves PostgreSQL, MinIO, and the three host services running for further use. Tear everything down with:

./clean.sh          # stop containers, keep volumes + config
./clean.sh -v       # also remove the postgres + minio volumes
./clean.sh --purge  # full reset: volumes, generated config, and runtime data

First time? Read the tutorial

For a guided, step-by-step walk that explains each step, see Your first workspace. This page is the reference procedure for repeating the local-dev smoke.

What the smoke verifies

Each step fails loudly if the real path does not work. There are no mocked successes.

  1. cargo build of awc-control, awc-worker, awcd, awcctl with their s3 and fuse features (CARGO_INCREMENTAL=0 keeps the build off the disk cliff).
  2. PostgreSQL and MinIO become healthy. The awc-cas bucket is created.
  3. awc-control connects to PostgreSQL, applies the awc-db schema migrations, serves REST and gRPC, and the dev bootstrap mints a service token (captured from the log).
  4. The seed identity rows (seed/seed.sql) load with the Admin role on the dev project.
  5. awc-worker connects to PostgreSQL over the durable s3:// MinIO CAS and starts its run loop.
  6. awcd starts from the generated config/awcd.toml, mounts a real read-only FUSE base filesystem over MinIO, and serves its host API. The kernel is polled until it reports a fuse* filesystem at the basefs mountpoint. /healthz corroborates that the base-fuse component is up.
  7. awcctl snapshot create runs the full import protocol (session over the control plane, objects uploaded straight to the MinIO CAS, completion over the control plane) and reports the snapshot id, manifest digest, and file count.
  8. awcctl snapshot inspect and list round-trip against the control plane.
  9. The just-imported snapshot is read through the FUSE base mount. Bytes are fetched from MinIO on demand, content-verified, served, and compared byte-for-byte with the source fixture.
  10. The worker validates the snapshot object closure by reading it back from the CAS, and advances it VALIDATING -> READY. The smoke polls until READY.
  11. awcctl workspace fork from the READY snapshot, then mount (the ticket is issued over the control plane and redeemed at the host daemon; the session is registered), then unmount.

Inspect intermediate output under run/ (build.log, control.log, worker.log, awcd.log, bucket.log, seed.log, awcctl.log).

Generated credentials and configuration

bin/generate-credentials.sh produces:

File Contents
.env Random POSTGRES_PASSWORD, MINIO_ROOT_PASSWORD, and AWC_SERVICE_TOKEN_KEY, plus reproducible identifiers and ports
config/awcd.toml awcd config: host_id, roots under data/, loopback health port, cache/checkpoint/lease limits, and the control_plane_endpoint at the control gRPC port
config/worker.env DATABASE_URL, AWC_CAS_URL=s3://…, worker tuning, and the S3 staging variables
config/control.env awc-control env: DATABASE_URL, REST and gRPC listen addresses, the shared MinIO CAS and S3 variables, the service-token key, and the dev-bootstrap identity

Secrets are random. Identifiers (for example host_01arz3ndektsv4rrffq69g5fav) are fixed and reproducible so the seeded host row matches awcd's configured host_id. Re-running the generator reuses an existing .env so its password keeps matching an initialized PostgreSQL volume. Pass --force to regenerate secrets, then run ./clean.sh -v, since a new password does not match the old volume. Generated files are gitignored and never committed.

awcctl is driven through environment variables in local-dev. The smoke sources config/worker.env (for the MinIO CAS upload) and sets the control-plane and host endpoints and the dev token:

set -a; . config/worker.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_PROJECT        # the seeded dev tenant/project
export AWC_TOKEN="<dev service token>"  # minted by awc-control on startup
awcctl snapshot create --from-dir seed/source --name demo
awcctl status                          # host health via awcd

See configuration and environment variables for the full key and variable reference.

Manual bring-up

The smoke automates all of this. The individual steps for a manual run:

bin/generate-credentials.sh
docker compose up -d postgres minio
docker compose run --rm minio-init            # create the CAS bucket

# awc-control migrates the schema on startup and mints the dev token.
set -a; . config/control.env; set +a
awc-control &  CONTROL_PID=$!
DEV_TOKEN=$(grep -m1 'awc-control dev token:' run/control.log | sed 's/.*awc-control dev token: //')

docker compose exec -T postgres psql -U awc -d awc < seed/seed.sql

set -a; . config/worker.env; set +a
awc-worker &

awcd config/awcd.toml run/awcd.sock &

export AWC_CONTROL_ENDPOINT="http://127.0.0.1:8080" AWC_HOST_SOCKET="$PWD/run/awcd.sock"
export AWC_TENANT AWC_PROJECT AWC_TOKEN="$DEV_TOKEN"
awcctl snapshot create --from-dir seed/source --name demo

Current scope (honest)

This environment wires every component that is implemented today and is explicit about what is not, rather than faking a working service.

Verified working end to end by the smoke:

  • awc-control is a live service (REST and gRPC). It migrates the schema, serves /v1/*, verifies bearer service tokens, and writes imports into the durable MinIO CAS. Completing an import enqueues a snapshot_validate job, so the worker advances snapshots VALIDATING -> READY.
  • The worker reads from MinIO (AWC_CAS_URL=s3://awc-cas), not an in-process store. It drains the queue and validates snapshot object closure against the real CAS.
  • awcd mounts a real read-only FUSE base filesystem, not a no-op. Objects are fetched from MinIO on demand, content-verified, and served. The smoke reads an imported snapshot through $BASEFS/tenants/<tenant>/snapshots/<manifest>/… and compares bytes with the source.

The privileged smoke also verifies the writable path. It checks that the target is OverlayFS, mutates and deletes files, publishes a cooperative checkpoint, unmounts, immediately reacquires the writer lease, and verifies the changes from the new immutable head after remount.

Not exercised by this smoke:

  • Production mTLS. The smoke sets AWC_DEV_MODE=1 and uses loopback plaintext. Outside that mode, awcd and awc-control require mutually authenticated TLS and fail startup on missing credentials. See the security model.

Files

local-dev/
├── docker-compose.yml        postgres + minio + minio-init
├── .env.example              tunables (committed); .env is generated
├── bin/
│   └── generate-credentials.sh
├── config/
│   ├── awcd.toml.example     generated shape (committed); awcd.toml is generated
│   ├── worker.env.example    generated shape (committed); worker.env is generated
│   └── control.env.example   generated shape (committed); control.env is generated
├── seed/
│   ├── seed.sql              deterministic identity rows (tenant/project/principal/host)
│   └── source/               fixture dir the snapshot-import flow uploads
├── smoke.sh                  end-to-end verification
├── clean.sh                  teardown
└── README.md                 in-tree pointer to this page