vetta and naive.
The name selects nothing — every command works under either — but a cloned blueprint repository
documents the naive spellings below. naive claim is vetta naive claim, and so on.
The
naive binary exists from @usenaive-sdk/vetta-cli 0.4.0. Earlier releases install only
vetta, so on those every command below has to be spelled vetta naive …. A blueprint repo pins
^0.4.0 and gets the binary from its own pnpm install — nothing global is required:
pnpm exec naive up. To install it yourself, npm i -g @usenaive-sdk/vetta-cli@^0.4.0.Commands
claim
The key is verified with one authenticated read before anything is written; a refused key stores
nothing. On success the profile is saved owner-only (
0600) and the output confirms the
organization and the key’s 12-character display prefix — never the key itself.
The binding is the key: API keys are organization-scoped, so everything a later naive up
creates lands in that key’s organization. The profile lives on this machine only — there is no
server-side record tying a repository or project name to an organization, so the same clone
claimed on another machine with another key deploys to another org. To move a project, claim
again with the other organization’s key.
up
The report names the
blueprint and the template it applied, when the config declares them: a
crew provisioned into your organization is money, so which crew it was is not left to be inferred
from the agent list.
Loads the config, then reconciles each declared skill, identity, vault, app and agent by name — in
that order, so agents can reference the rest. The report lists every resource as created,
updated, unchanged, deleted or refused (with the reason), and any refusal makes the exit
code non-zero. Re-running is always safe: every resource is keyed by its config name, and nothing
is deleted by omission — only a removed name, or a cron missing from an agent’s schedules.
A live row the CLI cannot read — one written before a field’s rules tightened, say — is listed
under skipped in the report and stepped over: it is neither reconciled nor deleted, and the rest
of the apply runs normally. A row in skipped is a row to fix or delete by hand.
Config surface
Every cross-reference —
allowed_apps, identity, a vault’s identity — names a resource declared
in the same config, and a config that names one that is not declared (or declares a name twice) is
refused before anything is read. An agent’s system, skills and mcp_servers pass through to
the platform verbatim; allowed_apps names apps and is written as their ids into the apps tool’s
config. A field the config leaves out is never drift: the live value stays (an app’s mcp is the
one exception — see below).
questions is the one key up does not reconcile, because it is not a resource: it is what a
person is asked before the crew is provisioned. They use the same shape an agent asks a question
with mid-run, so one form renders both, and the answers a person gives are recorded on the install
rather than written into the tree — the tree is identical for every organization running the same
template. Declaring them is what lets a published blueprint be set up before anything exists to
configure.
file (resolved against the config’s directory) is pushed by slug; skills are
content-addressed, so an unchanged file is unchanged and a changed one mints a new version
(updated). An agent’s identity names a declared identity, which up attaches after the agent
upsert; the grant is never revoked by up.
An agent’s schedules are its cron deployments, owned as a complete set:
cron string (which must be unique per agent): a match with other fields drifted is patched, a
missing cron is created, and any live deployment of that agent whose cron is not declared is
deleted. The set is bounded to agents declared in this config — an agent without schedules keeps
whatever it has, and other agents’ deployments are never touched. Each outcome is a report line
named <agent> @ <cron> under schedules; a schedule identity names a declared identity.
The intake — the first thing a new agent reads
An agent may declare anintake: the message it is sent once, on the apply that creates it.
system (that is standing instruction, re-read every turn):
it is a session that is created and started with message as its first turn, reported under
intake as one line named after the agent whose id is the ses_ that was opened.
Nothing marks an agent as briefed, because nothing needs to: an agent is created exactly once, so a
second naive up reports it unchanged and starts nothing. An agent that was merely patched is not
re-briefed. Delete the agent and apply again and it intakes again, which is the correct reading of
“this agent is new”. budget_micro_usd is optional and caps that one session; leave it out and the
agent’s own budget governs. Intakes are opened last, after every other write in the apply, so
an agent that delegates or reaches for an app finds the rest of the crew already there.
Blueprints and templates
A blueprint is the machine: the screens, the/api/* routes, the store, the build and the
approval flow. It is one repository. A template is data inside that repository — the crew and
its prompts, the tool allow-lists, the kinds of work it files, its schedules, its demo seed and the
words its screens print. A config names both:
media has no default on purpose: its two crews spend money in different ways, so defaulting one
of them would be a bill you did not choose. An unknown blueprint, an unknown template, or media
with no template at all is refused when the config is loaded — before anything is read or written.
The chosen template’s agents become the project’s crew, and the config may declare agents of its
own alongside them. Because one repository carries every template its blueprint has, switching
is an edit plus naive up — never a re-clone, and never a new app: edit template, re-run, and
the machine, the app, its URL and its data stay exactly where they are.
Switching widens and never narrows. The new template’s agents are created; an agent only the
previous template declared is reported and left alone — defineProject lists those names under
kept, and up reports each one that is live as unchanged with not declared by template "…"; kept, never deleted. Your own rows — clients, posts, connections, the app, its URL, its token —
are never touched by a switch. To retire an agent the old crew left behind, name it under
removed yourself, which is the only thing that ever deletes.
App ownership
Every app records the project that created it, andup compares that stamp before it writes
anything. An app whose row names a different project — or no project, because it was made by hand
through the dashboard or the API — is refused, with the exit code non-zero and nothing written:
env write overwrites a secret value nothing
can read back, and a removed name tears down its hosting and its database. So a name collision
stops rather than converges. Fix it by renaming the app in your config, or, if the app really is
this project’s, run naive up --adopt once: it takes every app the run touches, stamps each with
this project’s name, and reports them updated with adopted from project "…" as the reason.
From then on ordinary naive up converges them and the flag is not needed again. --adopt covers
removed names too, since deleting another project’s app is worse than overwriting it, and under
--dry-run it only says will adopt ….
An app’s env is written as its secrets — a value is a literal or { from_env: "LOCAL_VAR" },
read from your shell at apply time so a secret never sits in the config file:
up can tell an unchanged secret from a changed
one without ever seeing a value: a secret whose live hash matches is skipped, and an app whose
secrets all match reports unchanged. Changing a from_env variable’s value and re-running up
rewrites just that secret — that is how rotation works for app secrets. A secret set before
hashes existed (or by hand, through the dashboard or SDK) has no hash and is rewritten once. An
unset from_env variable refuses the app before any write, naming the variable only. Secrets that
exist on the app but are absent from env are listed in the report as unmanaged secrets: … and
left alone — nothing is deleted by omission. With --dry-run the report says will set … for
exactly the secrets an apply would write.
An app’s deploy_dir ships as one file-map deployment carrying a hash of its contents. On the
next apply, up hashes the directory again and compares it with the app’s newest deployment: equal
means nothing is uploaded and the app reports unchanged (unless env or description made it
updated); different, or a deployment made before hashes existed, deploys again. With --dry-run
the report says will deploy <dir> or <dir> unchanged.
A fullstack app’s mcp is the path it serves an MCP endpoint on ("/mcp"): agents connected to
the app get its MCP tools automatically, and the platform mints a bearer token for that endpoint into
the app’s VETTA_MCP_TOKEN secret, which the app should require. Declaring mcp on a
frontend_only app is refused at parse time. An unchanged path is unchanged (the token is never
rotated by a no-op apply); dropping mcp from the config clears the endpoint and forgets the token.
With --dry-run the report says will set mcp <path> or will clear mcp.
A vaults entry provisions a credential vault by name, optionally held by a declared identity.
Credential values are real secrets, so they are { from_env } only — never a literal — and are
read from your shell at apply time:
up reconciles their
presence, not their value: a credential is identified by (kind, key, host / mcp_server_url).
One that is live is unchanged, one that is missing is created (an unset from_env variable refuses
that one credential, naming the variable only, and never echoes a value), and credentials on the
vault that are not declared are never deleted or overwritten. Because a live value cannot be read or
replaced through up, rotation is a manual operation: create the new credential and delete the
old one on the platform (or with the SDK) — changing a from_env variable’s value and re-running
up changes nothing. Each credential is a report line named <vault> / <key> under vaults; a
vault’s identity cannot change after creation, so a live vault held by another identity is refused.
env_var credentials are refused at parse time: they need substitution at the sandbox egress
boundary, which the platform does not have, so the API would refuse them anyway — declare a
static_bearer or mcp_oauth credential with an mcp_server_url instead, which is injected
server-side before the sandbox sees it.
To delete a resource, move its name from the declaration into the config’s removed block:
up deletes each removed name it finds live and reports it as deleted; a name that is already
absent is unchanged (already absent), so removed entries can stay in the config and re-running
stays safe. A name cannot be both declared and removed. Removing a vault deletes its credentials
with it.
Renaming is a delete plus a create: resources are keyed by name, so changing a declared name
mints a new resource and leaves the old one live — put the old name under removed in the same
apply, or it stays behind. The config is the source of truth: a field edited by hand in the
dashboard is drift, and the next naive up converges it back to what the config declares (a field
the config leaves out is never touched).
Two load failures stay distinct: a config that loaded but is wrong is reported as config invalid
(exit 2 — fix the file), while a config the toolchain could not evaluate (a missing install) is a
config toolchain failure (exit 1). A .ts config is evaluated with the CLI’s own loader and
needs only the blueprint repo’s install (defineProject comes from @usenaive-sdk/blueprints); a
.json config needs nothing installed at all.
init
template
Clones a blueprint into a directory of its own. A blueprint is a public repository with anaive.config.ts in it, so what you get is exactly what is on the branch — its screens, its server
and its tests — not a scaffold generated by string substitution. The git history is dropped: the
first commit of your company should be yours.
--template writes your choice into the clone’s own naive.config.ts, so the next naive up
provisions that crew. Nothing else is fetched — the repository already carries every template its
blueprint has — and a template the blueprint does not have is refused before the clone.
catalog
The published catalogue: one row per blueprint × template × version, carrying the setup questions to ask first, the crew and apps that pair provisions, and the timers it runs.
Requires the
agents:read scope. It reads and never applies — a published row becomes an install
when you clone the blueprint (naive template) and run naive up, which puts the tree in front of
you before it provisions a crew that costs money.
It answers the question naive template cannot: which templates exist at which versions, right
now, rather than which ones this binary was released knowing about. There is no catalogue inside
the CLI, deliberately — publishing a blueprint is publishing a repository and a row, not shipping a
new binary — so this is where the two meet.
installs
One row per project this organization has applied: which blueprint and template, at which artifact version, at which revision, and by which writer.naive up reports the apply it just ran and nothing else. This reports what anyone applied,
which is the thing to read before an apply that expects a particular revision: an install the
dashboard advanced since you last ran up shows a higher revision here, and the row’s report
carries what that apply did.
The repository is <org>/<blueprint>-blueprint — naive template agency clones
usenaive/agency-blueprint — so publishing a new blueprint is publishing a repository: there is no
catalogue inside this binary to drift from the blueprints that actually exist, and no release of the
CLI is needed to add one. The suffix is the convention a fork follows too, so --org acme looks for
acme/agency-blueprint. No API key is required: a blueprint is public, and claiming it comes after.