Skip to content

Composites

llms.txt lists every page for an agent

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"],
});
DefaultEffect
networking_type: limited with an empty allowlistdeny-all egress, fountain’s isolation mode
allowed_vault_ids: []no conversation may override the reviewed environment at spawn
managed-by: chant on bothowned-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.

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.

DefaultEffect
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_modeevery 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 modelan acp agent’s model is whatever the command it launches decides to use
no skillsa steward’s competence is chant’s op definitions
allowed_vault_ids scoped to the given vault, [] with noneno conversation may attach a vault the steward was not given
managed-by: chant on the Agentowned-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 webhook option, 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.

DeclaredNameWhat it carries
Environment<name>-envthe 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
DefaultEffectLoosened by
networking_type: limited with an empty allowlistdeny-all egressallowedHosts, or unrestrictedNetworking: true (FTN011 warns)
allowed_vault_ids scoped to the box’s vault, [] with noneno conversation attaches a vault the box was not givenallowedVaults, a list or "any"
sandbox_provider unsetthe instance’s configured default backendsandboxProvider: sprites or e2b
no permission_policy defaultfountain’s unset policy is auto_allow, so the choice is written downpermissionPolicy is required
managed-by: chant on every resourceowned-only reconcile, prune, and drift filtering see themextra 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.

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.

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.

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.

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.

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.

chant