Skip to main content
Exactly one key is required: an identity, as auth. createVendo({ auth }) is day one. createVendo() is a type error, and createVendo({}) throws VendoError("validation") naming the identity it is missing.

Composition

Passing a key that was removed throws at boot naming its new home: model became models.default, paint split into models.apps and apps: false, brief became instructions, policy / judge / approvals moved inside guard({ … }), and connectorApps became connectedAccounts.

models

One seat per job that actually runs. A seat is a job, not a model, so the same model may fill several and swapping one never renames the others. Each seat takes a model-name string, resolved through the credential ladder, or an explicit AI SDK LanguageModel object, which wins as-is. A name string is pinnable per seat with VENDO_MODEL, VENDO_MODEL_APPS, VENDO_MODEL_REVIEW, and VENDO_MODEL_JUDGE. A key that is not a seat throws models.<key> is not a model seat.

guard

guard({ policy, judge, approvals, breakers }) declares the rules and this composition completes them with the store, risk grading, and the org-policy layer. A VendoGuard built by createGuard({ store, … }) is taken verbatim instead. Named presets: "cautious" lets reads run and asks before writes or destructive calls, "readonly" lets reads run and blocks everything else, "autopilot" runs everything. "cautious" makes one exception to that prompt: the agent’s bash runs without asking. It is still graded write, so it takes an audit row per call and answers to your own rules and the kill switch — but an automation has nobody to answer an approval card, and a prompt here would mean the shell could never run unattended. Add your own rule for the bash tool to put the prompt back. guard({ policy: {} }) reads the default .vendo/policy.json, which is what vendo init scaffolds. It reads fail-soft, so a missing file also auto-runs with no notice; keep the file in version control. vendo doctor is what tells you it is gone — config/policy.json fails with E-CFG-001. Omitting the guard key entirely surfaces an unconfigured-policy notice in the shipped chrome. Past either breaker, a would-be auto-run parks for approval until the window clears.

limits

Vendo counts, you decide. The callback is asked once before each metered action.
count(action, window?) is already bound to that user. window takes days, hours, and minutes, ANDed into one lookback, a since instant, or a pool name to count a shared meter instead. Return false, or { allow: false, message } to say why in your own words, and the action is refused and never counted. Anything else allows it and the meter records it. A policy against a store with no usage meter is refused at composition. A callback that throws denies and logs limits.callback_error. Limits walks the per-user cap, the per-org pool, windows, and what a blocked user sees.

mcp

true takes the defaults. The object form carries door-specific settings through the umbrella. A key is any opaque string (openssl rand -hex 32), the door never writes one down, and rotation is listing both keys until the old one is out of use. serviceAuth is itself a local authorization-server choice, so it fills the same slot VENDO_MCP_BROKER_URL fills and the environment default never displaces it.

oauth

apps

false unmounts app generation entirely: vendo_make and the vendo_apps_* tools are absent, the building-apps skill is absent, and /apps answers not-found. A check is { name, run(input) } returning Finding[], or { name, kind: "judgment", rule } whose sentence joins the AI reviewer’s rubric. A block finding stops the ship and a warn rides along. A check that throws degrades to a warn naming it, so a broken check never takes an app down. Built apps have no flag. They are gated by a configured sandbox adapter alone.

profile

The .vendo/ pieces as in-memory compose-time inputs, for venues with no filesystem. Each is independent and each wins over its file. overrides and tools are validated lazily, so a malformed piece throws on the first actions use rather than at createVendo.

sweep

{ intervalMs }, default 60 seconds. One pass expires orphaned parked calls and stranded approvals, driving both the amortized on-request sweep and the background timer.

Turn context

Two owners, and the split is the point. What the deployment curates sits at the top level; what the thinker decides is set where the thinker is named.
There is no agent: { … } options bag. It was one bag holding both owners, so a host configured the thinker through a key the thinker never saw. maxInitialTools bounds how many host tools the model sees at the start of a turn. Past the cap Vendo picks a deterministic read-first loadout, safest risk then name, and the rest stay reachable through find_tools. Vendo’s own vendo_* tools are always active and do not count toward the cap. Every tool, initial or searched-in, executes through the same guard binding. toolOutputCap truncates only the copy sent to the model. Persistence and the streamed thread are unaffected. A harness you named yourself carries its own equivalents. These four are vendo()’s.

Turn cancellation

Vendo cancels an in-flight turn when the client disconnects. The wire forwards the request’s AbortSignal into the agent loop, so closing the tab aborts the provider call and stops further steps. The thread is left consistent and resumable. No configuration is required.

Base path

The server’s base path is the fixed constant /api/vendo. It is not configurable: the handler answers only paths under it and returns not-found for anything else. createVendoClient({ baseUrl }) configures the client side. Point it at a full origin when the browser talks to a Vendo deployment on another host.

Identity

auth is one door with two spellings — a preset’s result, or an object you write. A preset is a function returning that object, so nothing is reserved to the preset path:
principal is the only required member; each other one absent means the documented “not asserted” behavior, never an error. Vendo mints no principals of its own, so a config with no principal at all refuses to compose. A hard-wired subject is a development stand-in, never a deployment:
principal(req) must return kind: "user" or null. Returning kind: "org", or a subject starting with vendo:, is rejected at the wire because those identities are Vendo-owned. null refuses the request with forbidden. If you want logged-out visitors served, resolve them to a principal of your own choosing. Marking it ephemeral: true keeps it out of org membership and blocks it from connecting external accounts. The seams facts, pools, and memberships run once per context resolution. facts becomes the prompt’s [User] block, pools feeds the limits policy, and memberships answers can(). Every asserted membership is already a pool named org:<orgId>, so an org cap needs no pools seam of its own. The top-level principal, actAs, and oauth keys are deprecated aliases for the members of the same name. They keep working for this whole major, but each is one seam with nowhere to grow — facts and pools never had a top-level twin, which is the reason the door consolidated. Passing auth beside any of the three — or beside the top-level memberships seam, which fills the same slot — throws VendoError("validation") naming the mixed keys. See Your users for facts and Orgs & memberships for memberships.