Skip to content

The Steward

A steward is the machine an environment is operated from, declared. It is one Agent running chant acp on a persistent sandbox, seated on the team as a Teammate so it has a standing conversation, with a Schedule for every op that carries a cadence and an optional Webhook for whoever has to hear about a turn.

The teammate’s thread is that environment’s operational history. Each turn is one chant command line, so scrolling the thread is scrolling what was done to the environment, redacted and searchable, without anyone having kept a separate log.

chant needs six things from a place to run ops. fountain already has a noun for each:

chant needfountain noun
a place to runEnvironment (repo, chant, kubectl) + Vault (creds, or the broker) + a persistent sandbox
a recordthe conversation: turns, events, SSE, search, audit
a cadencea Schedule on the teammate, in-thread, busy means skip
waiting on a humana gate fact on chant’s ledger, resolved by chant approve or a merged PR
waiting on the worldthe next tick’s observation
telling someonea Webhook, or a message to a human teammate
one writer per environmentthe thread

Nothing in that table is new machinery. The steward composite is the declaration that wires the existing nouns together in one place, so an author writes the environment once instead of six resources that have to agree with each other.

import { ConvergeOp, WatchOp, gt, report, when } from "@intentius/chant/op";
import { Environment, Repository, Steward, Vault } from "@intentius/chant-lexicon-fountain";
export const toolchain = new Environment({
name: "prod-toolchain",
repositories: [new Repository({ url: "https://github.com/acme/estate", mount_path: "/workspace/estate", ref: "main" })],
setup_script: "npm ci && npm install -g @intentius/chant",
networking_type: "limited",
networking_config: { allowed_hosts: ["github.com", "registry.npmjs.org"] },
metadata: { "managed-by": "chant" },
});
export const prodCreds = new Vault({ name: "prod-creds", metadata: { "managed-by": "chant" } });
export const { op: prodWatch } = WatchOp({ name: "prod-watch", env: "prod", schedule: "*/15 * * * *" });
export const { op: prodConverge } = ConvergeOp({
name: "prod-converge",
env: "prod",
schedule: "0 * * * *",
rules: [
when(gt("updateCount", 0), report("declared and live state disagree"), {
id: "prod-drift",
why: "An update pending against prod means something changed outside this repo.",
}),
],
});
export const { agent, teammate, schedules } = Steward({
name: "prod-steward",
environment: toolchain,
vault: prodCreds,
ops: [prodWatch, prodConverge],
});

ops takes the Op declarations the composites hand back, or plain OpConfig objects for an Op assembled by hand. Either way the steward reads two fields off each: the name, which becomes the prompt chant run <op>, and the schedule, which becomes the cron.

DefaultEffect
runtime: acp, runtime_command: "chant acp"the sandbox launches chant’s own ACP server, so a prompt is a chant command line
sandbox_mode: persistentone computer per steward: the checkout and the tool cache survive a turn ending
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 on the other end of the protocol chooses
no skillsa steward’s competence is the project’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

Two stores, each authoritative for one kind of thing, and nothing stored twice.

FactStoreRead it with
A run: its turn, its steps, its stdout, when it started and how it endedfountain’s database, as the conversationchant run status|log|list --on fountain, or the thread in the UI
A gate’s resolution, and anything that has to survive across runschant’s ledger branch in gitchant approve, chant run approve <op> <gate>
The declared shape of the estatethe repochant build, chant lifecycle diff --live

A gate is a fact, not a wait. A run that reaches an unapproved gate ends its turn saying so; the resolution is recorded on chant’s ledger by a person or a merged PR, and the next run re-evaluates it. Nothing sits blocked inside a sandbox holding a machine open, which is what lets the steward be one computer rather than one per pending decision.

A teammate runs one turn at a time. That is fountain’s rule, not chant’s, and the steward is built to sit exactly on top of it: a schedule that fires while a turn is in flight is dropped with teammate was busy rather than queued, and a chant run --on fountain against a busy steward is refused with the conversation’s address rather than retried.

The composite refuses three things at construction, so the failure arrives while the author is looking at the declaration:

  • Two stewards on the same environment and vault. That is two processes on one checkout; fountain would accept both and their turns would interleave.
  • An op whose schedule.overlap is anything but skip. Any other policy would be a promise the server does not keep.
  • A webhook url FTN022 would reject: plaintext http, or a loopback, link-local or RFC1918 target.

All six kinds, in dependency order: Environment, Vault, Agent, Teammate, Schedule, Webhook. fountainApply sends the first three through fountain’s bulk POST /api/apply and the other three through their own routes afterwards, matched by name and by url, so a second apply of an unchanged manifest makes no writes.

See Composites for the constructor’s full option list, Runtime for running an op on the steward, and ACP for what the sandbox is actually speaking.