Composites
ConciergeStack
Section titled “ConciergeStack”An Environment + Agent pair with the secure-by-construction defaults for agents that touch anything sensitive:
import { ConciergeStack } from "@intentius/chant-lexicon-fountain";
export const { environment, agent } = ConciergeStack({ name: "concierge", model: "anthropic/claude-sonnet-4-6", allowedHosts: ["registry.npmjs.org", "github.com"],});| Default | Effect |
|---|---|
networking_type: limited with an empty allowlist | deny-all egress, fountain’s isolation mode |
allowed_vault_ids: [] | no conversation may override the reviewed environment at spawn |
managed-by: chant on both | owned-only reconcile, prune, and drift filtering see them |
Every default is the closed one, so loosening one shows in review: pass an allowlist, pass vault ids, or drop to the raw classes.
Give such a sandbox no cloud credentials of any kind: prompt injection can exfiltrate anything readable inside it. Services the agent needs live outside the sandbox behind their own auth; the sandbox gets at most a conversation-scoped token.
Steward
Section titled “Steward”An environment’s one writer: an Agent that speaks the Agent Client Protocol over chant acp, whose every run lands on one long-lived conversation. chant run <op> --on fountain opens it the first time and resumes it after that by channel_id chant:steward:<name>; its sandbox keeps its disk across park and wake.
import { Steward } from "@intentius/chant-lexicon-fountain";import { watch, converge, apply } from "../ops";
export const { agent, declaration } = Steward({ name: "prod-steward", environment: toolchain, // repo, chant and kubectl in setup_script vault: prodCreds, // omit under the egress broker ops: [watch.op, converge.op, apply.op],});That conversation is the environment’s operational history. Each turn is one chant command line, so scrolling it is scrolling what was done to the environment.
| Default | Effect |
|---|---|
runtime: acp with runtime_command: "chant acp --steward <name>" | the agent launches chant’s own ACP server, so a prompt is a chant command line |
no sandbox_mode | every conversation gets a sandbox of its own; Managoat refuses persistent with 422 |
permission_policy: { default: "auto_allow" } | nobody is at the keyboard to answer a permission card; chant’s gates are where a human belongs |
no model | an acp agent’s model is whatever the command it launches decides to use |
| no skills | a steward’s competence is chant’s op definitions |
allowed_vault_ids scoped to the given vault, [] with none | no conversation may attach a vault the steward was not given |
managed-by: chant on the Agent | owned-only reconcile, prune, and drift filtering see it |
Every op is listed on the steward, which is how chant run <op> --on fountain knows the conversation its run belongs on. An op’s schedule stays on declaration. Managoat has no scheduler, so a clock outside it (a CI schedule:, a CronJob) runs chant run <op> --on fountain on that cadence.
Refused at construction rather than at apply:
- Two stewards on the same environment and vault. That is two writers on one environment, which is what a single conversation exists to prevent. Managoat would accept both, and their turns would interleave.
- The old
webhookoption, from an untyped caller. Managoat removed webhooks (managoat/managoat#2793).
The composite also returns declaration, the steward as core reads it: its name, its Ops, and its form. form is "fountain" by default. A box whose preset has no Fountain in some environment names that environment, as in form: { default: "fountain", environments: { minimal: "local" } }, and there chant operator --steward prod-steward --env minimal runs the same Ops on the same crons. A form that is local in every environment is refused, since none of the Fountain resources would ever run; declare that steward with declareSteward from @intentius/chant/op. Export declaration from an *.op.ts file so the operator and chant workspace status can find it. Behind a broker, name the box capabilities the steward’s Ops use in capabilities and leave vault out; the two together are refused. The Steward has the whole rule.
chant build emits Environment, Vault and Agent, in that order, and fountainApply sends them through Managoat’s bulk POST /api/apply.
A long-lived machine that serves an app repo and its tools: an Environment that runs a provisioning script as its setup_script, an Agent whose conversations are provisioned from it, and optionally a Vault for the secrets every box of this kind shares. An arugula studio box is the case it was written for: box/provision-template.sh is the setup, and the door serves on :8080 with hud behind it.
repo is optional. When given, the Environment clones it in (repositories) before setup_script runs. When omitted (a blank-slate app, such as the one hud’s init makes, has no git remote yet), the Environment declares no repositories, and setup_script runs on an empty sandbox.
The machine is one conversation, not the agent. Managoat gives every conversation a sandbox of its own and refuses sandbox_mode: "persistent", so the Agent declares no sandbox_mode. A box runtime opens one conversation per box and resumes it by channel_id. Its sandbox keeps its disk across park and wake and is destroyed only when the conversation is terminated.
import { readFileSync } from "node:fs";import { Box } from "@intentius/chant-lexicon-fountain";
export const { environment, agent, vault } = Box({ name: "studio-box", repo: { url: "https://github.com/arugula-salad/studio", ref: "main" }, setupScript: readFileSync("box/provision-template.sh", "utf8"), model: "anthropic/claude-sonnet-4-6", permissionPolicy: { default: "auto_allow" }, allowedHosts: ["registry.npmjs.org", "github.com"], vault: { secrets: [{ key: "STUDIO_SECRET", value: "${STUDIO_SECRET}" }] },});setupScript is the script’s text, because fountain’s setup_script is a string it runs, not a path it reads.
model is required. Managoat refuses an Agent with no model for every runtime but acp (model: can't be blank at apply), and a Box’s runtime is never acp. Box throws at build when it is missing, and FTN016 reports the same gap on an Agent declared any other way.
| Declared | Name | What it carries |
|---|---|---|
| Environment | <name>-env | the app repo at /workspace/app (or repo.mountPath) when repo is given, else no repositories; the setup script, packages, env_vars and networking, passed through |
| Agent | <name> | runtime (default claude), model (required), sandbox_provider (unset, so the instance default, unless sandboxProvider names one), the given permission_policy |
Vault (with vault) | <name>-secrets (or vault.name) | the shared secrets, written inline at apply |
| Default | Effect | Loosened by |
|---|---|---|
networking_type: limited with an empty allowlist | deny-all egress | allowedHosts, or unrestrictedNetworking: true (FTN011 warns) |
allowed_vault_ids scoped to the box’s vault, [] with none | no conversation attaches a vault the box was not given | allowedVaults, a list or "any" |
sandbox_provider unset | the instance’s configured default backend | sandboxProvider: sprites or e2b |
no permission_policy default | fountain’s unset policy is auto_allow, so the choice is written down | permissionPolicy is required |
managed-by: chant on every resource | owned-only reconcile, prune, and drift filtering see them | extra metadata merges over it |
allowedVaults: "any" leaves the list unset, which fountain reads as any vault the tenant owns. A runtime that makes a vault per box, as hud’s box runtime does for each box’s identity, needs it.
Credentials
Section titled “Credentials”A box holds no credential (#2726). A vault secret’s value and an envVars value are references that resolve at apply: ${VAR}, or a secret-manager reference such as op://, bws:// or infisical://. Avoid process.env.X too: it writes the secret into the built manifest, and no check sees it in the source. What the box reaches outside itself, such as inference or Fountain’s own API, goes through a broker like a studio’s lobby, which holds the key. Those capabilities and their scopes are declared in the box block of the workspace member that holds the composite. chant workspace check reads that member’s files and fails with box-credential-declared on a literal secret, and with box-capability-unbrokered on a capability that names no broker.
The port
Section titled “The port”fountain’s Environment and Agent have no field for a served port at the pinned spec (v0.29.0). port (default 8080) is recorded as box-port in the metadata of the Environment and the Agent, and returned as port. Whatever builds the box’s URL reads it from there, from the declaration or from fountain’s own record of the agent, rather than assuming 8080. BOX_PORT_METADATA_KEY names the key.
Allowed vaults in the read contract
Section titled “Allowed vaults in the read contract”sandbox_provider is a plain Agent field, so chant workspace graph --json already carries it on the agent’s node like any other declared attribute. allowed_vault_ids is not as plain to read: fountain’s own convention, and this composite’s, is that an unset list means “any vault the tenant owns”, so a graph reader can’t tell “no vaults declared” from “any vault” from the key’s absence alone (#2759). A runtime that creates a vault per box, such as hud’s, needs to know which before it acts. The resolved list, or the literal string "any", is also recorded in the agent’s metadata under box-allowed-vault-ids (BOX_ALLOWED_VAULTS_METADATA_KEY), so a reader sees it on the agent’s node without knowing that convention.
=== It is read-only bookkeeping alongside box-port: the manifest’s real allowed_vault_ids field is what fountain applies.
The callback token
Section titled “The callback token”Managoat gives every sandbox a callback token, FOUNTAIN_TOKEN, in the environment of each exec it makes into it, the setup script’s included. The token is scoped to the owner (sandbox_api_access: owner). The opt-out, sandbox_api_access: none, is a field of a conversation’s launch, not of the Agent, and Managoat accepts it only for a fresh sandbox, never on a channel_id resume. A box’s conversation is resumed, so no Box option can turn it off (managoat/fountain#2497). The Box records it instead. The Agent’s metadata carries box-sandbox-api-access: owner, which the built manifest and chant workspace graph --json show. The member’s box block declares BOX_FOUNTAIN_CALLBACK_CAPABILITY, which is { name: "fountain-callback", broker: "fountain", scope: ["owner"] }. chant workspace check reports a box member that builds a Box and doesn’t declare it (WSP125, box-fountain-callback-undeclared). A setup script that should not pass the token on can unset FOUNTAIN_TOKEN first, but the agent runtime still gets it.
Box and Steward
Section titled “Box and Steward”A Steward is an environment’s one writer: an acp agent whose every turn is a chant command line, on one long-lived conversation. A Box is where people and their agents work on an app, with a conversational runtime, the app repo checked out, a provisioning script, and a served port. No two-writers refusal applies, because each conversation on a box is someone working on the app rather than a turn on an environment’s operational history.
Nothing in a Box starts a conversation. chant build emits Environment, Vault and Agent, and fountainApply applies them through fountain’s bulk POST /api/apply. Starting a box and reaping it stay runtime acts: a conversation launched on the agent provisions the sandbox, which clones the repo and runs the setup script.