Skip to content

docs(legal): say what hosts download for projects' images - #16

Merged
kalaspuff merged 1 commit into
mainfrom
fctl/39-can-you-check-if-something-is-left-to-do
Oct 18, 2026
Merged

kalaspuff merged 1 commit into
mainfrom
fctl/39-can-you-check-if-something-is-left-to-do

Conversation

@fctl-sh

@fctl-sh fctl-sh Bot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

The privacy policy says what fctl host downloads and what app.fctl.sh keeps about hosts. hyperstructure/fctl#88 changes both, for hyperstructure/fctl#16:

  • What hosts download. Hosts pull the images projects choose, from those images' registries. Where their owners allow it, they also download a project's repository from GitHub to build the image the project builds from it.
  • What app.fctl.sh keeps. Hosts report the sandbox image they use, and the images and repositories they allow. app.fctl.sh keeps these with the rest of a host's details.

The policy's updated date is already today's.

Refs hyperstructure/fctl#16
Pairs with hyperstructure/fctl#88.

How I verified it

npm run check passes, with the same two warnings as main. FCTL_DOCS_DIR=<that branch> npm run build renders the policy and the docs from the fctl branch. Every link and anchor between the docs pages resolves, including the new sections of "Sandbox images".

Session: https://app.fctl.sh/s/ses_01M57YPG1ATK9ARAXTZG43G51J

Hosts now pull the images projects choose from their registries, and
download a project's repository from GitHub to build the image it builds
from it, when their owners allow builds (hyperstructure/fctl#16). They
also report the images and repositories they allow.
kalaspuff pushed a commit to hyperstructure/fctl that referenced this pull request Oct 18, 2026
Finishes #16. A project starts its sessions from an image of its own, or
from one that hosts build out of a Dockerfile or devcontainer.json in
its repository. Host owners decide which images their machines run and
which repositories they build from, and sessions go only to runners that
allow their image. The docs now say how to do all of it.

Fixes #16

## What changed, by the issue's points

1. **The contract** (`docs/sandbox-images.md`). "What an image needs"
now lists `gh`, a non-root user, both architectures and the oldest
Claude Code fctl works with. That is **2.1.101**, the first release that
asks its host for fresh OAuth tokens (`oauth_token_refresh`;
`remote_control` came in 2.1.63). I found both by checking published
Claude Code builds, because the changelog doesn't say. fctl-agent now
runs `claude --version` before it starts Claude, and fails the session
with a clear message when Claude Code is missing or older. "What fctl
writes into a sandbox" is new. The worked example adds .NET rather than
Rust, since the default image has had Rust since #45.
2. **CLI.** `fctl projects set` gets `--image`, `--build` /
`--build-context`, `--setup-script` (the script, `@FILE`, or `-` for
stdin), `--env` / `--unset-env`, and `--cpus` / `--memory` (`default`
for the runner's own), with `--json` like the rest. `fctl projects show`
prints them.
3. **The image's Claude settings stay.** fctl-agent writes its own keys
over `~/.claude/settings.json` instead of replacing the file. It owns
the permission mode, `attribution`, `disableClaudeAiConnectors` and the
auto-updater. It drops `apiKeyHelper`, `ANTHROPIC_*` and provider
variables, and `permissions.disableBypassPermissionsMode`. Plugins,
marketplaces, hooks and permission rules stay.
4. **Builds from the repository** (`crates/fctl-host/src/build.rs`). The
new project setting `image_build` names a Dockerfile (plus the directory
it builds in) or a devcontainer.json.
- **When it builds:** a host that allows it reads the file at the tip of
the default branch through GitHub's API, using the session's token. It
hashes the file, the Dockerfile and the git tree of the build directory,
and builds only when no image has that hash.
- **How it builds:** it downloads the repository archive (up to 2 GiB)
and unpacks only the build directory and the Dockerfile. It sends the
token only to GitHub's API origin. Then it runs `docker build` or
`container build`, one build at a time.
- **Limits:** `build_timeout_minutes` (60) and `max_build_mb` (20 GiB).
It keeps each project's newest build, and starts from the last build
while GitHub is unreachable.
- **Dev containers:** fctl reads `image`,
`build.dockerfile`/`context`/`args`/`target`. It leaves out features and
lifecycle commands, and says so in the session's log.
5. **Host owners decide what runs.**
- `host.toml` gets `allowed_images` (references, `/*` prefixes, digest
pins) and `allowed_builds` (`owner/name`, `owner/*`). `fctl host
install` gets `--allow-image` and `--allow-build` for them.
- Hosts send both as optional hello fields. FleetDO assigns a session
only to a runner that runs or builds its image
(`fctl_domain::schedule::pick`). A session no runner takes waits and
says why ("No runner allows this project's image, …" / "No runner builds
this project's image from …").
- The host checks again before it accepts a session. `docs/security.md`
says that whoever sets the image, the build or the setup script can read
the credentials of the people who start sessions.
6. **The app.** Settings → Sandbox → Image picks each runner's own
image, an image from a registry, or a build from the repository. Its
hint says which drivers use images. Admin → Runners, `fctl admin
runners` and `fctl host status` show each runner's image and what else
it runs and builds.
7. **Private registries.** Hosts pull with their own login (`docker
login`, `container registry login`), and the docs say so. fctl stores no
registry credentials.

ADR 0017 records the decisions. One worth a look: `allowed_images` unset
still means any image, so upgrading no host stops a project's sessions.

## Compatibility

- **New fields:** all optional. `ProjectSettings.image_build` is always
serialized, as `null` when there is none. `HostHello.allowed_images` /
`allowed_builds` and `Runner.image` / `allowed_*` are skipped when
empty.
- **Older hosts** send no lists: they run any image and build none, as
before. Older agents ignore `image_build`.
- **Older clients** (an old web tab, the previous CLI) that send
settings without `image_build` keep a project's build, unless they
choose an image instead (`settings::keep_unsent`).
- **Validation:** image references are now validated with Docker's
grammar.
- **Images with Claude Code older than 2.1.101** now fail at start with
a message. Before, they failed once an OAuth token expired.

## How I verified it

- **Rust:** `cargo xtask check` passes (fmt, generated types, Biome and
TypeScript, Clippy native and wasm, all tests, the web build). New unit
tests cover:
  - reference parsing and pattern matching, and scheduling by image;
  - `keep_unsent` and build validation;
- JSONC and devcontainer parsing, the build key, tar unpacking (hard
links skipped, nothing escapes through links);
  - the settings merge and the version check.
- One host test builds end to end against a fake GitHub (API, plus a
second origin for the archive) and a fake `docker`. It covers: build
once, reuse, no rebuild after an unrelated commit, rebuild after a
change in the build directory or the Dockerfile, removal of the old
build, fallback while GitHub is down, a failing build, the size cap, and
a devcontainer `image`. It also caught the token going to the archive's
redirect origin, which is fixed.
- **Web:** Vitest covers the image chooser's state and the runner
descriptions.
- **Dev stack** (`cargo xtask dev --no-host`, two hosts I registered
with the new flags: a process-driver host allowing `ghcr.io/acme/*`, and
a Docker-driver host behind a stand-in `docker` that builds from
`acme/*`):
- a project image no runner allows waited with the reason above; after
switching to an allowed image, the waiting session started on the right
host and finished its turn;
- a build from `other/tool` waited ("No runner builds this project's
image from other/tool"); a build from `acme/app` went to the building
host, and stopped there only because the dev stack has no GitHub App;
- stopping and resuming a session merged seeded "image" settings:
plugins, hooks and `RUST_LOG` stayed, `apiKeyHelper` and
`ANTHROPIC_BASE_URL` were gone;
- the CLI output, `fctl admin runners` and `fctl host status` read as
described.
- **The .NET example's steps** ran in this sandbox, which runs the
default image: SDK 8.0.426 installed, a console app built and ran, and
the `profile.d` snippet set the PATH for login shells.
- **Docs:** hyperstructure/fctl-site built with these docs, and every
link and anchor between pages resolves.
- **Independent review:** a separate agent reviewed the whole diff, and
b59b708 fixes what it found, with tests for each:
  - a tag in `allowed_images` allowed that tag pinned to any digest;
  - reading a large `.json` build file was quadratic;
- the host fell back to the last build after any error, not only when
GitHub was unreachable;
  - a Dockerfile that is a symlink would have been read on the host;
  - Docker Hub prefixes written with `docker.io` matched nothing;
- smaller ones: a stale build directory in the CLI, leftover build files
after a stop, a signed download link in error messages, a few gaps in
reference parsing, and the chooser forgetting typed values.

**Not verified:** a real `docker build` or Apple `container build`. This
sandbox has neither runtime, so the builder commands, `container image
inspect` sizes and `container image delete` are untested on real
runtimes. A first build on a Docker host and on a Mac host is worth
doing before relying on it.

## Not in this change

For a person to decide; I can open issues for them:

- Hosts that run only their own image unless their owner says otherwise.
- Registry credentials from project secrets.
- Dev container features, which need the Dev Container CLI.

## Other repositories

- hyperstructure/fctl-site: hyperstructure/fctl-site#16 updates the
privacy policy. Hosts now pull the images projects choose, download
repositories from GitHub to build them, and report the images and
repositories they allow.
- hyperstructure/sandbox needs no change: the image's layout is the
same, and it ships the newest Claude Code.

## Screenshots

Taken at 809b7b2 against `cargo xtask dev` with two dev hosts, 1440 px
wide unless noted. Later commits don't change these screens.

**Settings → Sandbox, a project that builds its image.** Before, the
form has no way to show a build; after, the Image choice, the file and
the directory it builds in.

| Before | After |
|---|---|
| ![Before: the Sandbox section has only an image
field](https://app.fctl.sh/api/v1/published/F0fZAliXtluplWuc4dCjpJNLEoojVcEu2SVzZeqgxK7/before-build-light.png)
| ![After: Image set to "Built from the repository", with File and
Directory](https://app.fctl.sh/api/v1/published/Ud5Ese2ZT9AXSkc9YDlJuJ4GN6Xytp7LrdVpn8iBSLj/after-build-light.png)
|

The three choices, an image from a registry with its hint, and the dark
theme at 390 px:

| Choices | An image | Dark, 390 px |
|---|---|---|
| ![The Image select open: each runner's own, an image from a registry,
built from the
repository](https://app.fctl.sh/api/v1/published/8AvkYMocuajdDoZcHKYjJEFF5b1lzSZ4J1I1xs7dhlq/after-select-light.png)
| ![An image from a registry, with the reference
ghcr.io/acme/agent:1](https://app.fctl.sh/api/v1/published/jLYdeDvohoEEgZXaevLCJ1ke10ybddTY4wzUw7OMiPR/after-image-light.png)
| ![The build fields stacked at phone width, dark
theme](https://app.fctl.sh/api/v1/published/BQPzSA5mKwJjvk7MxcpT77nlrZxmRvP68yK9d4rV68c/after-build-dark-390.png)
|

**Admin → Runners.** Each runner now says its image, what else it runs
and what it builds.

| Before | After |
|---|---|
| ![Before: runners without their
images](https://app.fctl.sh/api/v1/published/ixPw7Ofey4MARsV7gMKq6pfbcN89LVOFw582axFUMkc/before-runners-light.png)
| ![After: "Image ghcr.io/hyperstructure/sandbox:0.5.0 · runs its own
image and ghcr.io/acme/* · builds no
images"](https://app.fctl.sh/api/v1/published/BYvDAQnO0mViAWoWlcMNeCsr4NVVFBiMe5osLj6E4Xn/after-runners-light.png)
|

![After, dark
theme](https://app.fctl.sh/api/v1/published/qCUw03CLKR5nNr6EpWO33yF19SE4AXvsQyZewLpx8dT/after-runners-dark.png)

Session: https://app.fctl.sh/s/ses_01M57YPG1ATK9ARAXTZG43G51J

---------

Co-authored-by: fctl-sh[bot] <343971779+fctl-sh[bot]@users.noreply.github.com>
Co-authored-by: fctl-sh[bot] <fctl-sh[bot]@users.noreply.github.com>
@kalaspuff
kalaspuff merged commit fbd35fc into main Oct 18, 2026
3 checks passed
@kalaspuff
kalaspuff deleted the fctl/39-can-you-check-if-something-is-left-to-do branch October 18, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant