Development
Everything runs on one machine, with one command, and needs no Claude subscription, Cloudflare account or GitHub App.
Setup
- Rust via rustup —
rust-toolchain.tomlpins the version and adds thewasm32-unknown-unknowntarget. - Node.js 22 or newer (
npm ciat the root installs the web app and wrangler). The web app depends on @hyperstructure/design through git, sonpm cineeds read access to that repository while it is private. - Optional, for release builds: zig and
cargo install cargo-zigbuild.
The local stack
cargo xtask devThis starts:
- the control plane under
wrangler devat http://127.0.0.1:8787, with local D1 and Durable Objects (state incrates/fctl-control/.wrangler/); - the web app under Vite at http://localhost:5173, with hot reload, proxying
/apito the control plane; - a host using the
processdriver whose agents run fake-claude: a scripted stand-in for Claude Code that speaks the same stream-json protocol.
The first run creates an owner (dev@fctl.test / fctl-dev-password), a
Claude credential, a runner and a project called Playground, and stores a CLI
token in .fctl-dev/token:
export FCTL_SERVER=http://127.0.0.1:8787 FCTL_TOKEN=$(cat .fctl-dev/token)
cargo run -p fctl -- run -p playground "write notes.md"fake-claude reacts to words in the prompt: write <name> creates and
commits a file, slow streams slowly, limit hits a usage limit, overage
reports paid usage (the session must pause), refresh asks the host for a
new OAuth token, and crash exits mid-turn. Anything else gets a short
answer with a tool call.
--no-host skips the host; --no-vite serves the built web app from
wrangler instead. Delete .fctl-dev/ and crates/fctl-control/.wrangler/
to start over.
Checks
cargo xtask check # what CI runs
cargo xtask check --quick # without the production buildsThat is cargo fmt --check, a check that web/src/gen matches the Rust
types, Biome and TypeScript for the web app, Clippy (native and wasm) with
warnings as errors, cargo test, the web app's Vitest suite, and a
production build of the web app.
Repository map
| Path | What |
|---|---|
crates/fctl-proto |
Wire types for every protocol; TypeScript is generated from them |
crates/fctl-domain |
Pure logic: permissions, validation, tokens, naming, scheduling, event shaping |
crates/fctl-control |
The control plane Worker (Rust → wasm): API, auth, Durable Objects, GitHub |
crates/fctl-client |
HTTP and WebSocket client used by the CLI and hosts |
crates/fctl-host |
The host daemon: control connection, sessions, sandbox drivers, images |
crates/fctl-agent |
The supervisor inside each sandbox: runs Claude, journals events, prepares git |
crates/fctl |
The fctl binary: CLI plus fctl host |
crates/fake-claude |
The scripted Claude stand-in |
xtask |
cargo xtask: dev, check, gen, deploy, secrets, dist, publish |
web |
The web app (React, TanStack Router and Query, Tailwind, WebGL) |
images/agent |
The default sandbox image |
deploy/instances |
Instance descriptions for cargo xtask deploy |
docs |
These docs; docs/adr records decisions |
Changing a protocol
- Change the types in
crates/fctl-proto— add fields asOption(with#[serde(default)]where needed), never repurpose atypetag. cargo xtask genregeneratesweb/src/gen.- Keep older peers working: hosts and agents can lag the control plane by a version, and the web app talks to whatever is deployed.
Working on the control plane
- It compiles to
wasm32-unknown-unknownand runs on workers-rs. Nounwrap,expector panics (Clippy denies them): a panic kills the request, and in a Durable Object it can poison the instance. - Durable Object SQL must not fail on a schema that is already migrated:
create tables with
IF NOT EXISTSand add columns withadd_column, which checks first. - D1 migrations are in
crates/fctl-control/migrations;cargo xtask deployapplies them before deploying. Never edit a migration that has shipped.
Working on the web app
cd web
npm run dev # with the control plane from `cargo xtask dev --no-vite`, or any instance via FCTL_API
npm run check # Biome and TypeScript
npm testDesign tokens, fonts, the WebGL scenes and the mark come from
@hyperstructure/design, pinned
to a tag in web/package.json (it has a playground showing all of them).
Use the semantic colors (canvas, surface, ink, accent, highlight,
pink, blue…) rather than raw values. The scenes render dithered to the
brand palette; they pause when hidden and render one still frame for people
who prefer reduced motion.
Other repositories
| Repository | What | How it connects |
|---|---|---|
| hyperstructure/design | Tokens, fonts, WebGL scenes, the marks | web/ depends on a tag; bumps arrive as pull requests |
| hyperstructure/fctl-site | fctl.sh: landing page, these docs, /install, /releases |
renders docs/ at the commit in its fctl-docs.ref, which this repository's CI updates when docs/ changes on main; serves the releases cargo xtask publish writes to the KV namespace in deploy/releases.toml |
Releasing
- Bump
versionin the rootCargo.tomland commit. - Tag it:
git tag v0.2.0 && git push --tags. CI builds the archives (cargo xtask dist: fctl for macOS and Linux on arm64 and x86_64, and the Linux agent), attaches them to a GitHub release, and publishes them tohttps://fctl.sh/releases(cargo xtask publish --latest).
To build locally: cargo xtask dist (or --target aarch64-apple-darwin).
Deploying
main deploys app.fctl.sh from CI (fctl.sh deploys from
hyperstructure/fctl-site). By hand:
cargo xtask deploy fctl-sh