The data plane
fountain reaches the sandboxes its agents run in through one credential and one
base URL. dataPlane=spritzer points that URL at
spritzer, an emulator of the Sprites
API running in the cluster.
The seam is pure configuration. The app is not built differently and cannot tell the difference, so a local run exercises the real control-plane path instead of a stub of it.
| target | default |
|---|---|
k3d | spritzer. Offline there is no Sprites account, and a placeholder token against the real API is a 401 nobody sees until they talk to an agent |
kubernetes | sprites |
Provisioning works
Section titled “Provisioning works”Creating a conversation creates a sprite and populates it. fountain writes
its fountain skill and a /home/sprite/.env carrying a scoped token and the
conversation id into the sprite’s filesystem, and spritzer then reports that
sprite running with both files present.
Turns complete
Section titled “Turns complete”At fountain v0.7.0 and spritzer 0.5.0, the turn gate holds. The “every
time” evidence below is v0.6.1’s — 34 of 34, batched — and v0.7.0 has been
through the single conversation just e2e runs, not a rerun of that batch;
nothing in v0.7.0 touches this path. fountain opens
an exec session for the runtime command and writes the prompt into it as stdin;
spritzer holds the session open, echoes the prompt back on stdout, and exits 0
on EOF:
event: output claude --dangerously-skip-permissions --print --verbose --output-format stream-json …event: output Reply with the single word: fountainevent: stage turn done {"exit_code":0}That second line is the prompt making the round trip. It is the difference between provisioning a sandbox and holding a conversation — and it is still the echo, never a model. See what it will never prove.
Measured: 34 of 34 conversations, including batches fired back to back, which is the pacing that used to fail most.
So the emulated data plane proves the substrate can provision a sandbox, address it, and carry a prompt into it and a reply back out.
What it will never prove
Section titled “What it will never prove”Even once that endpoint lands, three things are absent and no configuration brings them back:
- live model reasoning
- real tool execution in the sandbox
- true VM isolation
spritzer answers exec with a scripted interpreter whose default is to echo the command back. A green local conversation is a plumbing check. It is not somewhere to judge agent behaviour or sandbox security, and single-node Postgres is likewise not somewhere to benchmark durability.
An account and a key, headless
Section titled “An account and a key, headless”POST /api/auth/register registers the same way the form does — under this
deployment’s defaults the account self-verifies at registration, and the
instance’s first account becomes the admin (fountain ADR 0011) — which is what
a script, a CI step or an agent needs:
curl -sX POST http://localhost:4000/api/auth/register \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"..."}'
{"message":"Account created. You can sign in now.","user_id":"60e3c0e6-..."}An API key comes from the same shape of call:
curl -sX POST http://localhost:4000/api/auth/token \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"..."}'
{"prefix":"ftn_75cd","key_id":"39a7161a-...","api_key":"ftn_75cda1c2..."}That key is what the conversation gate below authenticates with, so the whole
path from nothing to a running conversation is reachable without opening a
browser — and without a single kubectl command.
The conversation gate
Section titled “The conversation gate”just verify asks /health. A 200 there says the release booted; it does not
say it reached its database, resolved its secrets, provisioned a sandbox or
streamed anything back. just verify-conversation checks the rest:
export FOUNTAIN_PASSWORD=... # not on the command linejust verify-conversation you@example.com # plumbingjust verify-conversation you@example.com strict # plumbing + a model repliedBoth make a throwaway agent, open one conversation, and tear both down on the way out, including when an assertion fails, which is the case that matters.
plumbing asserts a sandbox was provisioned, a turn ran, events streamed in
order, and the turn exited 0. It catches a broken Secret, an unreachable data
plane, a migration that did not run.
strict additionally asserts a model replied, and refuses to run against
dataPlane=spritzer:
✗ strict needs a real data plane. This deployment runs the emulator, which echoes the runtime command back instead of calling a model, so a green run here would prove nothing about a reply.The emulator satisfies every plumbing assertion with no model in the loop at all, so a gate that cannot tell those apart is worse than no gate. This one fails closed.
Against the local default, plumbing passes: the turn completes and the prompt
comes back. An orphaned turn or a :command_exited fails the build now — both
were outcomes once and are regressions today.
For real conversations
Section titled “For real conversations”Set dataPlane=sprites and put a real SPRITES_TOKEN in the Secret.
Note also that a Fly platform token is not a Sprites token. A valid Fly
credential that api.fly.io/graphql accepts is rejected by api.sprites.dev
with 401 authentication failed.