Third reducing PR of the interpolation unification (**D11 resolution
(c)**): the control plane never interpolates. `InterpString` is now
strictly the user-facing workflow config language; server identity,
storage, listen, object-store, and GitHub App identifiers are plain
`String`, consumed where needed with no resolution point.
## Demoted to `String` (was `InterpString`)
Both the layer and resolved types:
- `server.listen.unix.path`, `server.api.url`, `server.web.url`
- `server.storage.root`, `server.artifacts.prefix`,
`server.slatedb.prefix`
- object store: `Local.root`, S3 `bucket` / `region` / `endpoint`
(shared by artifacts + slatedb)
- `github.app_id` / `client_id` / `slug`
**Kept `InterpString`:** `slack.default_channel` (run-time consumption —
the one server-defined survivor). `server.listen.tcp.address` stays the
`SocketAddr` `parsed_value` special case.
## Native `FABRO_WEB_URL` read
Deployment-time late binding now goes through a native env read instead
of a `{{ env.* }}` token: `FABRO_WEB_URL` overrides `server.web.url`
(**env override > settings literal > default**), applied in
`canonical_origin` and reused by the JWT issuer, cookie-secure check,
and system-info. `docker/split-web` no longer ferries the value through
a settings token (compose still sets the env var). `canonical_origin`'s
error message now advertises a knob that is actually true for everyone.
## Behavior change (release notes)
- `{{ env.* }}` / `{{ vars.* }}` tokens in the demoted server fields are
now **literal text**, not interpolated. The resolve layer emits
`warn_if_demoted_template` for every demoted field, so operators with
tokens still in server config **fail loud** rather than silently
treating the token as a literal.
- Operators who relied on env-based storage location should use the
existing native `FABRO_STORAGE_DIR` (`--storage-dir`) override.
`FABRO_STORAGE_ROOT` promotion is intentionally deferred (not a proven
need).
## Cleanup
`fabro-server`'s `crate::interp` shrinks to just the process-env lookup
facade; `resolve_interp` / `_path` / `_with` and the
`AppState::resolve_interp` seam are deleted (nothing resolves
server-scope `InterpString` anymore).
## Verification
- `cargo build --workspace` ✅
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` ✅
(incl. the `as_source` gate)
- `cargo +nightly fmt --check --all` ✅
- `cargo nextest run --workspace`: 6305 passed; added two tests covering
the `FABRO_WEB_URL` override precedence (env-wins and settings-literal
fallback).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permit GitHub App manifest form posts and signed HTTPS VNC preview iframes while keeping the rest of the SPA CSP locked down. Mirror the policy in the split-web Caddy config.
## Summary
Adds a standalone Docker Compose proof that runs the Fabro Rust API, a
Caddy static SPA server, and a Caddy edge proxy as separate services.
This demonstrates split web asset serving while keeping `/api/*`,
`/auth/*`, and `/health` same-origin with the API server.
## Changes
- Adds `docker-compose.split-web.yaml` with private `fabro-api` and
`fabro-web` services behind an exposed `edge` proxy on port 8080.
- Adds Caddy edge routing that sends `/api/*`, `/auth/*`, and `/health`
to Rust, while everything else goes to the static web service.
- Adds a static Caddy config for `apps/fabro-web/dist` with SPA
fallback, source-map blocking, security headers, immutable asset
caching, and `X-Fabro-PoC-Upstream` route-proof headers.
- Adds PoC server settings and a README with build, run, and validation
commands.
## Verification
- `cargo dev docker-build --tag fabro-sh/fabro:split-web-poc`
- `docker compose -f docker-compose.split-web.yaml up -d`
- `docker compose -f docker-compose.split-web.yaml ps`
- `curl` checks for `/runs`, `/assets/app.css`, `/assets/app.css.map`,
`/api/v1/health`, `/api/v1/auth/config`, `/auth/login/dev-token`,
`/api/v1/auth/me`, and `/api/v1/attach`
- Browser login flow via `browser-use`: loaded `/login`, submitted the
dev token, and landed on the authenticated Runs screen
- `docker compose -f docker-compose.split-web.yaml config`
- `caddy validate` for both Caddyfiles
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
When the packaged container starts as root, map the mounted Docker socket's group into the container and add the unprivileged fabro user before dropping privileges. This lets Docker sandboxes work with socket mounts from OrbStack, Docker Desktop, and Linux daemons whose socket GID varies by host.
Implement the web-first install experience across the server, CLI, API spec,
web app, and packaged SPA assets.
This also removes test-side process env mutation by pushing env-dependent
decision points behind explicit helpers and test wiring.
Now that the release workflow publishes musl binaries, the runtime
image can drop the debian:trixie-slim base for alpine:3.22. The
image shrinks from ~287 MB to ~96 MB (66% smaller) with a smaller
attack surface.
- Dockerfile: alpine:3.22 base, apk packages (ca-certificates git
tini su-exec), BusyBox adduser/addgroup, tini at /sbin/tini.
- entrypoint.sh: replace runuser with su-exec, Alpine's idiomatic
drop-privileges helper.
- release.yml docker job: pull the two linux-musl artifacts instead
of linux-gnu. The docker image and the Alpine install.sh path now
ship the same binary.
- bin/dev/docker-build.sh: compile fabro-cli for the host's musl
target in rust:1-bookworm with musl-tools, the matching CC/LINKER
env vars, and LIBZ_SYS_STATIC=1. Same pattern as CI.
Verified locally on aarch64: Alpine image builds, server binds on
$PORT (default 32276), endpoints return 200, fabro server process
runs as unprivileged UID 1000 under tini.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move docker-compose.yaml to the repo root and add docker-compose.prod.yaml
that stands up a Caddy 2 sidecar on 80/443 proxying to the fabro service
on 32276 (the CLI's default port). Auto-HTTPS is handled by Caddy when
FABRO_DOMAIN is set; certs persist in named volumes.
Switch the container's internal listener from 80 to 32276, which lets us
drop libcap2-bin and the CAP_NET_BIND_SERVICE file capability.
Replace the docker/Dockerfile-based api+web compose setup with a
single root Dockerfile that runs the fabro server with the embedded
web UI on port 80, persists state under /storage, and drops to a
non-root fabro user with CAP_NET_BIND_SERVICE.
The release workflow stages the prebuilt Linux binaries from the
compile job into a buildx context and publishes multi-arch images
to ghcr.io/fabro-sh/fabro as :<version> (always), :latest (stable
tags only), and :nightly (nightly tags only).
Also address zizmor findings in nightly.yml (pinned
create-github-app-token, persist-credentials: false with explicit
remote URL setup) and release.yml (no-cache on tag-triggered
setup-bun to close the cache-poisoning path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses supply chain hardening items from #160:
- Pin taiki-e/install-action to commit SHA
- Pin rust:1-bookworm and oven/bun:1 to image digests
- Pin mintlify to 4.2.507
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consolidate CLI and server machine defaults under settings.toml,
including loader renames, writer preservation fixes, same-machine
manifest handling, and docs/test updates for the new config model.
Aligns naming with the convention that "Config" is for file-level configuration
while "Options" and "Settings" describe runtime parameters. Also applies
rustfmt formatting fixes in web_auth.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 10 screenshots of the Fabro web UI to docs pages (workflows, runs,
retros, observability, steering)
- Update arc-web logos from Arc to Fabro (logotype.svg, logotype-light.svg)
- Hide Start and Settings nav items in arc-web for cleaner screenshots
- Mount arc-web public/ as Docker volume for logo hot-swapping
- Combine "Run agents 24/7" and "Scale infinitely" use cases
- Replace clone-and-build Quick Start with curl installer from README
- Add internal docs for updating web screenshots
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated: package.json workspaces, tsconfig path alias, CI workflow
paths, Dockerfile COPY, AGENTS.md, and doc references.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated: Cargo.toml workspace members, CI workflow paths, Dockerfile
COPY, AGENTS.md, skill mapping, and doc references.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update build context, dockerfile path, and volume mounts in
docker-compose.yaml to account for the new location.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a [checkpoint] config table with exclude_globs in both run.toml
(per-run) and server.toml (defaults). Globs are merged (union + dedup) when
both are present. Non-empty excludes use git pathspec :(glob,exclude) syntax
to prevent staging matching files during checkpoint commits.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a FeatureFlags config section with a session_sandboxes boolean
(default false) to both the Rust server config and web app config.
Gate the project/branch picker UI behind this flag. Set it to false
in the Docker demo config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-stage Dockerfile builds Rust API binary and bundles the web app
and docs into a single image. docker-compose.demo.yaml orchestrates
the three services (api, web, docs) with a shared entrypoint router.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements ExecutionEnvironment trait backed by Docker containers via
bollard. Host working directory is bind-mounted; all file ops, commands,
grep, and glob execute inside the container via docker exec. Extracts
shared format_lines_numbered() helper from LocalExecutionEnvironment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>