@cavelon/cli
v0.2.11
Published
The cavelon binary: CLI and local MCP server for building Cavelon solutions with a coding agent.
Readme
cavelon: the Cavelon CLI and MCP server
cavelon lets a developer, or the coding agent working for them, build, seed and
test Cavelon solutions from a repository instead of the Admin. It is generic: it
learns each instance's API, features and docs from what the instance publishes
(its OpenAPI, /api/v1/meta/capabilities, /api/v1/meta/error-catalog and
/llms.txt), so a new core release needs no new cavelon release.
Node.js 20.3 or newer. Run it through npx, which needs no install; to type
cavelon, install it globally (or define an alias for the npx command):
npx -y @cavelon/cli --help
npm i -g @cavelon/cliWithout Node.js, one line installs a standalone cavelon into your user
folder: curl -fsSL https://github.com/goodguys-gmbh/cavelon-dev-kit/releases/latest/download/install.sh | sh
on macOS and Linux, irm https://github.com/goodguys-gmbh/cavelon-dev-kit/releases/latest/download/install.ps1 | iex
in Windows PowerShell. With Python, uvx cavelon runs the same executable from
PyPI, and uv tool install cavelon or pipx install cavelon installs it. Then
cavelon setup sets up your coding agents and logs you in.
Running the one-line installer again updates the CLI, then asks separately
whether to refresh Cavelon plugins/skills and enable YOLO for new MCP sessions.
Enter at the YOLO question keeps your choice. Restart your agent afterwards;
it launches the tools with that personal mode automatically. Critical changes
still require approval. Other package managers: update the CLI, then run
cavelon setup --update. Change just the mode with
cavelon setup --mcp-mode yolo --yes or --mcp-mode standard --yes.
Interactive setup and setup --update offer the same YOLO choice, so
Homebrew, npm, uv and pipx users do not need to run the installer script.
Version 0.1.17 adds Cline, Kilo, Goose and OMP to the OpenCode, Pi and Qwen Code integrations. Each has native settings and four skills; guarded changes use the qualified client dialog or the person’s own terminal:
cavelon setup --agents opencode,pi,qwen,cline,kilo,goose,ompSee the client install pages and qualification matrix for pinned versions, tested platforms, updates and removal.
The full documentation is in the repository's
docs/:
installation
(with the plugin for Claude Code and Codex),
getting started,
concepts,
the command reference,
troubleshooting
and security.
This page is a compact reference.
Log in (a person does this once per instance)
Create a personal access token on
/account/access-tokensin the Admin. It starts withcvpat_. A tenant API key (cbp_…) works too, for one tenant. An instance with personal access tokens turned off refuses everycvpat_token;loginand every later command then answerpersonal_access_tokens_disabled(exit 7) and name the operator's setting,PERSONAL_ACCESS_TOKENS_ENABLED.Run, in a terminal:
cavelon login --instance https://cavelon.example.comhttps://cavelon.example.comstands for your instance's address, the URL you open Cavelon at in the browser.It asks for the token without echoing it.
--token-stdinreads it from standard input instead.cavelonnever takes a token as an argument, so it stays out of shell history. Run it in a terminal of your own: an agent's shell (!in Claude Code) has no terminal to type the token into.loginfinds the tenants the token reaches: it uses the only one, or asks you to choose by number or part of a name. Without a terminal it stores the token and prints onecavelon use <tenant>line per tenant (exit 2).cavelon whoamishows who the token acts as, in which tenant (name, slug and id), its name and when it expires (it warns from seven days before), and whether it may activate solutions.cavelon usechooses another tenant from the same list.
The token goes into the operating system's credential store (Keychain, Windows
Credential Manager, Secret Service). Where there is none, it goes into
~/.config/cavelon/credentials.json, readable only by you (0600). logout
deletes it. The coding agent never sees the token: it calls cavelon, and
cavelon sends it.
login reads the instance's capabilities and warns when its contract versions
are newer than this cavelon understands. It also caches the OpenAPI and the
error catalog under ~/.cache/cavelon/<instance>/<version>/, which only you
can read.
CI, scripts and cloud agents
Environment variables replace the login:
| Variable | Holds |
|---|---|
| CAVELON_URL | the instance URL |
| CAVELON_TOKEN | a personal access token (cvpat_…) or a tenant API key (cbp_…); needs CAVELON_URL |
| CAVELON_TENANT | the tenant's name, slug or id, for a personal token that reaches several tenants |
Which instance, token and tenant
Highest first:
- command-line options:
--instance <url>,--tenant <name, slug or id>(there is no token option); CAVELON_URL,CAVELON_TOKEN,CAVELON_TENANT;- the nearest
cavelon.yamlfrom the current directory upwards (instance URL and tenant, never a token); - the stored login, and the tenant chosen with
cavelon use.
CAVELON_TOKEN is used only together with CAVELON_URL, and only for that
instance: an option or a cavelon.yaml (perhaps from a cloned repository) that
names another instance never receives it. Plain http:// is refused except for
localhost (or with CAVELON_ALLOW_HTTP=1).
cavelon.yaml is written by cavelon init (see "Solution as code"). Keys the
binary does not know are kept and ignored. With --env <name>, env/<name>.yaml
names the tenant between CAVELON_TENANT and cavelon.yaml.
Commands
Every command takes --json (one JSON document on stdout, errors included),
--instance and --tenant, and --help. Each is marked read-only or changing
(cavelon commands lists them; cavelon <command> --help says it).
| Command | Marked | What it does |
|---|---|---|
| setup [--agents] [--yes] [--check] [--remove] | changing (destructive) | Set up supported coding agents with their plugin or native MCP configuration and all four skills, then log in. --check reports what works, --remove undoes it. A person runs this; not an MCP tool. |
| login [--token-stdin] | changing | Store a token for an instance. A person runs this. |
| logout [--all] | changing | Delete the stored token. |
| whoami | read-only | Owner or key, tenant (name, slug and id), role, where the credential came from, whether it may enter Platform mode and which tenants it reaches. |
| use [<tenant>] / use --clear | changing | Choose the tenant for this instance, by name, slug or id, or from a list. |
| status [--offline] | read-only | Instance, credential, tenant, solution folder and the solution's state (draft or active, ready to activate, latest test run), open previews, running operations, quotas close to full or why they cannot be read. |
| limits [--key <key>] [--source <source>] | read-only | The instance's limits for this tenant by source, who changes each and how; the tenant's quotas with their use (the monthly Processing Step cap among them); branch concurrency. |
| limits set <key> <value> [--confirm] | changing (destructive) | Change a limit through the operation the instance names: a tenant admin's, a quota of the Tenant Owner's (inference budget, Processing Step cap), or an operator's run cap with a Platform-mode token; without --confirm, shows the old and new value. |
| models list | read-only | The tenant's Model Registry rows with their endpoint and max_concurrent_requests; never a key. |
| models set-limit <model> <n\|none> [--confirm] | changing (destructive) | Set or clear a row's max_concurrent_requests; without --confirm, shows the old and new value. |
| tenant create <slug> [--name] [--use] [--confirm] | changing | Create a tenant (personal token in Platform mode with tenants.manage); refused before sending when the token cannot; without --confirm, shows the tenant it would create. |
| tenant list [--search] | read-only | Tenants the token can see, with name, slug, role and id. |
| harness list [--readiness] | read-only | The tenant's solutions, with slug, name, status and id. |
| harness new <slug> [--name] [--description] | changing | Create an empty draft solution. |
| harness clone <source> [--slug] [--name] [--no-tests] [--no-triggers] | changing | Copy a solution into a new draft. |
| activate [--harness] [--env] [--make-default] [--confirm [<token>]] | changing | Activate through the readiness gate only, never by force; a solution a channel or trigger reaches previews first. |
| chat <message> [--harness] [--env] [--session <id>] [--timeout 90s] | changing | Send one message to a solution and print its answer, with the session to continue and the conversation to trace; tries a draft or an active solution that is not the default route. |
| deactivate [--harness] [--env] [--confirm [<token>]] | changing (destructive) | Take an active solution out of live traffic: its status becomes inactive (not draft); previews first. |
| init [--harness [--new]] [--agents <list>] [--hook] [--update] [--from <file> [--force]] | changing (files) | Make this folder a solution: cavelon.yaml, package/, tests/, env/, .cavelon/; on a terminal it asks for the tenant and the solution (or a new one by name); a name close to an existing solution's is refused unless --new; --from writes a package file into it. |
| pull [--harness] [--force] [--include-tenant-wide] | changing (files) | Write the instance's package into package/ and tests/, the inventory into .cavelon/. A solution's tenant-wide sections only with --include-tenant-wide, which says when the export carries none. |
| validate [--offline] [--strict] | read-only | Check the package files against the cached package schema, their references, unknown fields and models; --strict fails on warnings. |
| schema [<section>[.<field>…] \| <type>] [--offline] | read-only | The package schema's sections, or the fields of a section, a nested field (agents.handoffs) or a type, with a minimal example. |
| apply [--env] [--harness] [--mode] [--include-tenant-wide] | changing | Preview the files against the instance; prints and stores a preview id. A solution's tenant-wide sections go along only with --include-tenant-wide, for the whole tenant. |
| apply --confirm <preview-id> | changing | Import exactly that preview; a stale one, or one the import's own check refuses (it names the blockers), exits 4. |
| explain <code> | read-only | Look a code up in the instance's error catalog. |
| variables list / variables get <name> | read-only | The tenant's plain-text variables ({{var:…}}) with their values. |
| variables set <name> <value> [--stdin] [--confirm] | changing | Create or replace a variable. Never a credential. Replacing another value needs --confirm; without it, shows the old and new value. |
| variables delete <name> [--confirm] | changing (destructive) | Delete a variable; without --confirm, shows it. |
| secrets list [--missing] | read-only | The tenant's secret names ({{secret:…}}): set or not, declared, changed when. Never a value. |
| secrets set <name> | changing | Set a secret's value from the terminal or stdin. A person runs this. |
| secrets delete <name> [--confirm] | changing (destructive) | Delete a secret's value; without --confirm, shows its status. A person runs this. |
| api <operation> [name=value…] [--body <body>] | changing | Call any operation of the instance's OpenAPI. |
| api list [--tag] [--search] [--method] [--tags] | read-only | The operations the instance publishes. |
| api describe <operation> | read-only | One operation's parameters, body and responses. |
| docs search <query> | read-only | Search the instance's own docs. |
| docs get <page> [--max-chars] [--cursor] | read-only | One docs page as markdown. |
| wait <operation…> [--timeout 90s] | read-only | Wait for operations; resumable, and reports waited_ms. A test run with failed cases is a failure. |
| watch <operation> [--timeout 10m] | read-only | Stream an operation's changes (server-sent events). |
| kb upload <dir> --kb <kb> [-r] [--ext pdf] [--replace] [--dry-run] [--wait] | changing | Upload documents; returns operation ids. Names files that match an active document; --replace replaces those. --dry-run also names files identical to an active document. |
| test run [--suite <s>] [--harness <h>] [--wait] | changing | Start test-suite runs; returns operation ids. |
| trace <run> [--trace <id>] [--span <id>] | read-only | Summarise a run's traces (or a test run's results, with why a case did not pass), then one trace's spans, then one span. A database query call that failed shows its code. |
| db instance | read-only | The dialects the instance runs and the addresses it connects to databases from, which a database's firewall lets in. |
| db connections / db queries [<query>] [--connection] / db runs <query> | read-only | The tenant's database connections (with their CA certificates' expiry), saved queries (one in full with its SQL and parameters) and a query's runs (no values, no rows). |
| db test <connection> | changing | The tenant Owner tests a connection step by step; exit 3 when a step fails. |
| db test-run <query> [--draft] [--value name=value…] | changing | Test a saved query or validate and test edited package SQL before apply. Writes dry-run and report rollback evidence; exit 3 when it fails. Drafts need the Owner's managing PAT. |
| db queries delete\|enable\|disable\|rename <query> [<new-slug>] [--confirm] | changing | Preview a saved-query change, then obtain the person's approval. Delete reports removed assignments; pull follows the change. |
| loop start <trigger> [--input <json>] [--wait] [--confirm] | changing | Start a loop through its trigger, as you; without --confirm, shows the trigger, its solution and the payload; with it, returns the run and operation ids. |
| loop cancel <run> [--confirm] | changing (destructive) | Stop a trigger run and its loops; without --confirm, shows what would stop. |
| loop watch <run> [--loop] [--timeout 10m] | read-only | One line per decided iteration and per state change, then the loop's outcome; returns when it ends or pauses. |
| loop iterations <run> [--loop] [--limit] [--cursor] | read-only | A loop's state, budget and iterations. |
| loop pause <run> / loop resume <run> [--reason <pause_reason>] | changing | Pause at the next safe point; resume a paused loop (a verdict on the task names its pause reason as reviewed). |
| trigger identity <trigger> [<key> \| --clear] [--confirm] | changing (destructive) | Show, bind or clear the API key a trigger's unattended runs act as. |
| sandbox list | read-only | The tenant's Sandboxes: mode, state, revision, what each mode offers. |
| sandbox validate <sandbox> | changing | Run the Sandbox's readiness checks; exit 3 when one fails. |
| sandbox files <sandbox> [<path>] / sandbox cat <sandbox> <path> | read-only | List a workspace folder; print a file a page at a time. |
| sandbox activity <sandbox> [<id>] / sandbox logs / sandbox receipt | read-only | What ran in the Sandbox, one activity's log, its validation receipt. |
| sandbox seed <sandbox> <folder\|tar> [--confirm] | changing (destructive) | Replace an isolated container's workspace with an archive. |
| sandbox refresh <sandbox> | changing | Accept a customer VM's files as the new baseline. |
| artifacts export <sandbox> [--path] [--out] [--wait \| --job <id>] | changing | Take files out of an isolated container as a tar. |
| commands | read-only | Every command with its marking and MCP tool. |
| mcp | changing | Serve the commands as MCP tools on stdio. |
cavelon api
cavelon api list --search harness
cavelon api describe create_harness
cavelon api get_harness_by_slug slug=support
cavelon api create_knowledge_base --body '{"name": "FAQ"}'
cavelon api create_knowledge_base --body @kb.json # or --body - for stdinAn operation is named by its operationId or by its short name (the part before
FastAPI's path suffix, list_harnesses for list_harnesses_api_v1_harnesses_get);
a looser spelling such as createTenant finds create_tenant and says so.
Parameters are name=value or -p name=value, never --name value; the body
(--body) is checked against the operation's schema before it is sent. --json
is the output switch, as everywhere else. --json <body> still sends the body
for now, with a warning: it is deprecated and will be removed in a later
release.
Limits
cavelon db instance and cavelon limits show published database connection
and query caps, current counts, their source and platform defaults. Missing
counts on older instances are left out. An operator with limits.manage
uses a Platform-mode personal access token to preview an override with
cavelon limits set database_queries_per_tenant 250 --tenant acme and sends
it after the person's confirmation. none resets the override. Tenant Owners
cannot raise these caps. See Database limits.
cavelon limits shows what the instance allows this tenant, from the limits
it publishes in /api/v1/meta/capabilities: upload size and file types, agent
turns and tool calls, timeouts, rate limits, the licence's cap on solutions.
They are grouped by where each value comes from (tenant, platform,
licence), and each names who changes it (a tenant admin or the instance
operator), the setting, and the docs page (cavelon docs get <page>). The
tenant's quotas follow with their current use, read from the path the limits
link to (/api/v1/tenants/current/quota-usage); status names the ones at 80 %
or more (or at the quota's own warning_ratio).
Two commands check them before they send anything:
kb uploadrefuses a file larger thankb_upload_max_file_size_mbor of a type outsidekb_upload_allowed_extensions(exit 3, with the instance's own code,upload_file_too_largeorupload_file_type_unsupported), naming the files, the limit, its source and who changes it.--dry-runchecks the same. A.zipis sent only while the tenant has archive uploads on (kb_upload_archive_enabled;archive_uploads_disabledotherwise, whatever the formats list) andkb_upload_archive_formatslistszip(archive_format_disabled), and only when the files its directory declares stay withinkb_upload_archive_max_entries,kb_upload_archive_max_total_uncompressed_mbandkb_upload_archive_max_compression_ratio(per file), as the instance counts them (directories do not count). A limit whosebinds_whennames an on/off limit that is off does not bind and is not checked. On an instance without the switch, an empty list of formats means archives are off. The instance checks each file inside against the types itself. On an instance that publishes no archive entries, the zip is sent and the instance decides. A knowledge base may set lower limits of its own, which the instance checks.harness newrefuses when the tenant's quotas list a solution quota that is used up (exit 3,tenant_quota_reached), or when the tenant alone already has as many solutions that are not archived aslicence_max_harnessesallows (exit 7,license_limit_reached, as the instance answers). The licence counts every tenant's solutions, so below the cap the instance decides.
A limit the instance does not publish is never assumed: on an instance older
than the published limits, limits says so and the commands send as before.
Limits that bind only while another is on.
The archive caps are published whether archive uploads are on or off, with
binds_when: "kb_upload_archive_enabled". limits lists them under "Only while
another limit is on" ("applies while archive uploads are on", and whether they
bind now), and --json adds binds_now to each. A tenant admin may set a cap
before turning archives on, and turns them on with
limits set kb_upload_archive_enabled true. The formats are the platform's
(changeable_by: operator); limits set kb_upload_archive_formats refuses and
names the switch.
The monthly Processing Step cap. The instance
publishes it in tenant_quotas.values, with this billing month's use, its
state (none, ok, reached, unavailable) and its change. limits lists
it under the tenant quotas ("close to the cap" from 80 %, "cap reached"), and
leaves out the quota-usage row that names the same change. A Tenant Owner
(settings.manage) sets it with limits set monthly_processing_step_cap <n>
and removes it with none. cavelon explain processing_step_cap explains the
refusal of new work at the cap (PROCESSING_STEP_CAP_REACHED), with the cap,
its use, when it resets and who raises it.
Branch concurrency. limits shows the width
per node (orchestration_max_branch_concurrency: a fan-out's or Map loop's
max_concurrency above it is capped), the branch slots one process shares
(orchestration_process_max_branch_inflight), and whether this tenant's
branches run concurrently (orchestration_parallel_branches). With branches
off, it names the switch that turned them off (the platform's
ORCHESTRATION_PARALLEL_FANOUT_ENABLED, or the tenant's feature flag) and who
turns it on. validate warns, never fails, when a fan-out or Map loop in the
package (an orchestration node's or graph edge's config, or a handoff's
orchestration_config) sets max_concurrency above the width
(branch_width_capped), and when the tenant runs fan-outs and Map loops in
sequence (branches_run_in_sequence). It reads the limits the instance last
published (cached by any command that read them), so it stays offline.
Run capacity. The run caps
(max_concurrent_agent_runs_per_tenant, max_concurrent_agent_runs_global)
also name their origin where the instance publishes one: a platform setting
the operator changes in the Admin (Platform › Operations › Rate limits), the
environment, or the default; limits then adds an origin column and says where
each cap is set. A tenant's own cap shows under
max_concurrent_agent_runs_per_tenant with source tenant; the operator sets
it with PATCH /api/v1/tenants/{tenant_id}/limits. status prints the run caps
with source and origin, and the slot waits (agent_run_slot_wait_seconds,
model_endpoint_slot_wait_seconds). An entry without origin shows as before.
Changing a limit. Each limit a tenant admin
may change publishes its change: the operation (operationId, method, path),
the body field that carries the value, the permissions of which the caller
needs one, and its bounds (minimum, maximum; a rate limit's maximum is the
operator's ceiling, named in maximum_setting; the per-visitor rate limits
rate_limit_chat_visitor_rpm and rate_limit_widget_visitor_rpm among them).
The inference budget, a quota,
is listed in tenant_quotas.changes. limits set <key> <value> reads that and
checks the value offline: a whole number in the limit's unit (50 or 50MB,
100rpm), a comma-separated list of file types, true/false for a switch,
or none to clear the tenant's own value so the platform's applies again.
Without --confirm it shows the old and the new value, the operation with its
body and who may run it, and changes nothing; with --confirm it sends exactly
that operation and prints the new value from the instance's answer. Where the
tenant's quota row names the change, the preview
of a quota shows its current value from the row's change.field.
cavelon limits set kb_upload_max_file_size_mb 50 # shows the change
cavelon limits set kb_upload_max_file_size_mb 50 --confirm # PATCH /api/v1/tenants/current/upload-defaults
cavelon limits set rate_limit_chat_rpm none --confirm # back to the platform's value
cavelon limits set monthly_inference_token_budget 2000000 # PATCH /api/v1/tenants/{tenant_id}/limits, this tenant
cavelon limits set monthly_processing_step_cap none --confirm # a Tenant Owner removes the capAn operator's change. The run caps publish a
change too, with requires_role (the global roles, any one of which the
caller needs) and mode: platform. limits set sends it only when
/api/v1/meta/principal, asked without X-Tenant-Id, answers in Platform mode
for a personal access token whose ceiling role is one of those roles (and the
person's global role from /api/v1/auth/me too), and sends the change without
X-Tenant-Id. Otherwise it refuses before sending (exit 7,
platform_role_required), naming the role and the Admin page (Platform ›
Operations › Rate limits › Run caps, a tenant's Limits section, or Configure ›
Feature Flags). The limits are still read for a tenant, so name one with
--tenant or cavelon use. With --tenant <id|slug>, a limit's
tenant_change applies instead: max_concurrent_agent_runs_per_tenant then
sets that tenant's own cap, with the tenant's id in the path. The tenant's flag
behind concurrent branches is the change on orchestration_parallel_branches:
a PUT to
/api/v1/admin/feature-flags/{tenant_id}/ORCHESTRATION_PARALLEL_FANOUT_ENABLED,
the tenant's id from --tenant and the flag from the published path (the
OpenAPI's {flag_key}); on/off (or true/false), and the preview shows
the flag's own state. An operator limit without a change cannot be changed at
runtime, and limits set says so. A tenant on Processing-Step terms is not
offered the inference budget: the instance leaves its change out, so limits set refuses it as not listed.
cavelon limits set max_concurrent_agent_runs_global 150 --tenant acme --confirm # superadmin, Platform mode
cavelon limits set max_concurrent_agent_runs_per_tenant 6 --tenant acme --confirm # acme's own cap
cavelon limits set orchestration_parallel_branches false --tenant acme --confirm # acme's flag, platform adminIt refuses before sending anything:
- an
operatoror licence limit without achange(exit 7,limit_changed_by_operator), naming who changes it and where (itssetting,originand docs page); - an operator's change without a Platform-mode token of its role (exit 7,
platform_role_required); - a value outside the published bounds (exit 3,
request_invalid, as the instance answers); a rate limit above the ceiling names the operator's setting that raises it, with the instance'slimit_above_platform_ceilingwhere its error catalog lists the code (its fieldssetting,value,maximumandmaximum_settingin the details). When the instance itself refuses with that code (its ceiling changed since it published), the error names the ceiling, the setting and that only the operator raises it;cavelon explain limit_above_platform_ceilinglists today's ceilings; - an entry without
change, on an older instance (exit 1,operation_unavailable: this instance does not publish how to change it); - a credential whose
permissions, as/api/v1/meta/principallists them, share no name with the change's (exit 7,forbidden, naming the permission): a tenant admin's token for the Processing Step cap, for example. On an instance that publishes nopermissions, an API key whose scopes are neitheradminnor one of the permissions is refused, and for a person the instance decides; its 403 exits 7 and names the permissions.
The MCP tool limits_set carries the destructive annotation and takes
confirm like the CLI. cavelon limits names the command under the limits a
tenant admin changes, and the limits an operator changes in Platform mode.
Model endpoint limits. models list shows the
tenant's Model Registry rows with their endpoint (the base_url's scheme, host,
port and path: user info and query are left out) and max_concurrent_requests,
and which rows share an endpoint, and so its count. It never shows a key, only
its kind (api_key_type). models set-limit <model> <n|none> shows the old and
the new value and changes nothing; with --confirm it sends only that field to
the row's update operation (PATCH /api/v1/model-registry/{model_registry_id},
agents.manage_llm_config). The value is checked against the instance's own
schema, and a row without a base_url is refused before anything is sent
(exit 3, model_endpoint_limit_without_base_url). The MCP tool
models_set_limit carries the destructive annotation, so the person confirms it
in the client.
Variables and secrets
Prompts, tools, guardrails and test suites name tenant variables as
{{var:<name>}} and secrets as {{secret:<name>}}. A package carries only
their names: package/required_variables.yaml and
package/required_secrets.yaml declare what the solution needs (a name and a
description each), and pull and apply keep them. apply lists every
name the target has no value for, with the command that sets it (--json:
set_commands), and reminds of them after --confirm.
cavelon variables set crm_base_url https://crm.example.com
cavelon secrets list --missing
cavelon secrets set crm_api_token # asks for the value, without echo
op read op://dev/crm/token | cavelon secrets set crm_api_token- A variable is plain text: anyone who may view the tenant's settings
reads it, and the agent may set it (MCP:
variables_list,variables_get,variables_set).variables setrefuses a value that looks like a token. A new variable is set at once; replacing another value shows the old and the new one and changes it only with--confirm, because every solution of the tenant reads it. - A secret's value is never an argument, never printed, never written to a
file and never read back.
secrets setreads it from the terminal without echo, or from standard input when that is piped (one trailing line break is dropped), and refuses a value given as an argument without repeating it. Setting or deleting one needs a person (a session or a personal access token): with a tenant API key,cavelonrefuses before it asks for the value or sends anything (secret_needs_a_person, as the instance would answer). Where the instance lets only a person signed in to the Admin set or delete one (/meta/principallists them inneeds_a_person), every token is refused that way (exit 5; a key on an older instance exits 7), and the hints name the Admin's Settings › Secrets page. MCP offers onlysecrets_list; an agent tells the person the exactcavelon secrets set <name>command, or the Admin page, instead. --env <name>acts in the tenantenv/<name>.yamlnames, likeapply. A name without its env file is refused (exit 2) before anything is sent.
Updating a document
kb upload names each file that matches an active document of the knowledge
base by file name, with what happens to that document, in --dry-run too:
bergbahn-faq.md exists (094e95e9…) and stays activeA file whose bytes are those of an active document, under any name, is not
created again: the instance deduplicates it, and kb upload says
"identical to the active document …; nothing new was created (deduplicated)".
--dry-run says so before the upload ("nothing new would be created") where
the instance publishes each document's file_sha256 (the SHA-256 of the
uploaded bytes, which its deduplication compares): it hashes each local file
the same way, and --json lists those files under identical with the
document each one matches, and sets content_compared. A same-named document
the file is identical to is then neither replaced nor deactivated. On an
instance that does not publish the hash, content_compared is false and
the dry run names no file identical; a document without a hash (not a file,
or uploaded before the instance published it) matches none.
- Without a flag the old document stays active next to the new one, and both
answer, unless the instance replaces same-named documents itself (its upload
offers
replace_existing); there--keep-bothkeeps both. --replacereplaces it. It uses the instance's own replacement (replace_doc_ids) and reads what was replaced from each document'sreplaced_document_ids. On an instance whose upload offers neither, it shows the documents it would deactivate after the upload and sends nothing; with--confirmit uploads, then deactivates them (after the wait, with--wait).
Long-running work
kb upload and test run return operation ids at once. --wait waits for them,
or run wait:
cavelon wait op_test_run_… --timeout 5mwait returns when every operation has finished or needs a person, or when the
timeout passes. It prints the state in every case; when time ran out, the work
goes on, and the same wait again resumes it. watch streams each change.
An operation only says that the work finished: for a test run, wait,
watch and test run --wait also read the run's summary. A run whose cases
failed or errored, or that measured nothing comparable (steps not run,
technical errors, missing results, or no pass rate), exits 1 with the counts,
the failed cases and the reason the instance recorded; a run whose answers
wait for a manual verdict or for a value a case needs exits 5 (--json:
failed_results, each with counts, comparable and exit_code). trace <test run> lists every case that did not pass with
its error, the judge's reasoning and the command that opens its traces (by its
conversation id, or for a trigger case by the run it started), and the judge's
reasoning for each scored pass the instance sent it for. test run --wait ends,
when its wait ran out, with the one wait command that resumes.
A trigger or channel run that meets a full run cap stays queued and the
instance retries it every 5–20 s. The run says so itself
(waiting_for_capacity; for an operation, the
kit reads the run its result_ref names), so wait, watch, loop watch,
trace, status and loop cancel say waiting for run capacity from the
first poll (--json: capacity_waits, or capacity_wait on the run), name
the run caps and where each is set, and keep waiting within the timeout. On an
instance that does not publish the field, they say it once a run has been
queued for more than 30 s, and mark the wait inferred: true.
A run or call refused for capacity (run_capacity_busy, model_endpoint_busy)
gets the instance's catalog entry plus which limit to raise and who can
(--json: capacity_refusals; explain: kit_hint), and that retrying later
is fine.
Loops and Sandboxes
A Masterloop loop runs a solution in bounded iterations; it starts only through its trigger, and stopping the trigger run stops it. A Sandbox is the workspace a loop works in, in one of two modes, which offer different things:
| | isolated_container | customer_vm |
|---|---|---|
| Put files in | sandbox seed <sandbox> <folder> (archive import) | put them on the VM, then sandbox refresh <sandbox> |
| Take results out | artifacts export <sandbox> (archive export) | sandbox cat <sandbox> <path> |
| Validation receipt | sandbox receipt | none: completion is agent-reported |
cavelon reads each Sandbox's mode and refuses a command the mode does not
offer before sending anything, naming the mode (exit 3, with the instance's
own code: sandbox_capability_unavailable, sandbox_workspace_refresh_unavailable
or sandbox_validation_receipt_unavailable). It also refuses Sandbox and loop
commands when the instance's capabilities say the feature is off.
cavelon sandbox validate orders-test
cavelon sandbox seed orders-test seeds/orders # shows what it would send
cavelon sandbox seed orders-test seeds/orders --confirm # the archive becomes the workspace
cavelon trigger identity orders loop-runner # shows the change
cavelon trigger identity orders loop-runner --confirm # binds the API key
cavelon loop start orders --input @request.json # shows what it would start
cavelon loop start orders --input @request.json --confirm # starts the run
cavelon loop watch <run>
cavelon sandbox cat orders-test output/summary.json
cavelon artifacts export orders-test --path output --wait --out results.tarloop startcalls the trigger's run-now route; the run acts as you (with a personal access token, the person) and spends the tenant's budget, so it first shows the trigger, its solution and the payload, and starts only with--confirm. It returns the run id and the run's operation id, sowaitandwatchfollow it (andtrace <operation>reads the run's traces); a paused loop makeswaitexit 5. It sends anIdempotency-Key(--idempotency-key, a new UUID by default;--json:idempotency_key). When the start ends in exit 8 (a timeout, a cut connection, a 5xx), the run may have started: the error's hint names the key, and the same command with--idempotency-key <key>starts no second run. With--wait, a run that ended without starting a loop exits 1 and names the stage that recorded an error.loop watchprints each iteration once the loop decided on it: accepted (with the outcome of itsloop.continuation.v1result), rejected, or failed when its child failed, with the reason. Usage is the iteration's actual usage; until the instance reports it, the line says what the iteration reserved. Then each state change, and at the end the loop's outcome: its reason, what it charged and, for a paused loop, how it goes on (go_on): theloop resumea person runs after fixing the cause, with--reason <the pause reason>when the pause is a verdict on the task; or, when it cannot be resumed, why not,loop cancel <run> --confirmandloop start <trigger>. The kit follows the loop'sresumewhere the instance publishes it; until then it rules a resume out only from the loop's published state (its run stopping, its deadline,resume_enabled, unknown operations), and names the--reasoncommand for when the instance asks for a review. The catalog's sentence for the pause reason is shown when it lists one.--jsongives one object per line (iteration,loop,outcome). Exit 0 completed, 1 failed or cancelled, 5 at once when the loop pauses, 6 when the timeout came first. A run that ended without a loop names the stage that recorded an error.loop iterationsreads the same a page at a time;trace <child_run_id>reads one iteration's run.loop pause,loop resumesend the loop's version and anIdempotency-Key(--idempotency-keymakes a retry safe; an exit-8 error names the key it sent); a loop that changed in between answers 409 (exit 4).loop resume --reasonmust be the loop's pause reason (exit 2 otherwise, before anything is sent). A pause that needs a review and has none exits 5 (loop_resume_review_required) with the command to run; one that cannot be resumed exits 4 with what to do instead. Without--loop, they act on the run's only loop that can be paused or resumed.loop cancelandsandbox seedchange nothing without--confirm; they show what would happen.trigger identitybinds through the trigger's versionedexecution-identityroute, with the version it read;applynever binds one. The key is named by its name or id, never its value (a value that looks like a key or token is refused and not echoed). Without--confirmit shows the change. It warns when the key does not reach the solutions the trigger needs or a Sandbox's Access does not list it; both are fixed in the Admin.- Sandbox reads (
files,cat,activity,logs,receipt) and jobs go through a solution the Sandbox allows:--harness, elsecavelon.yaml's, else the Sandbox's only allowed solution. One it does not allow exits 7. sandbox seedsends a folder as an uncompressed USTAR archive (files and folders only; links and special files are refused), a.taras it is, or any other file under its own name, up to the size the instance's OpenAPI allows (64 MiB). Names are checked before they go in, and the archive's bytes depend only on the files, so the same seed again is the same job: running it again resumes it. The archive goes up once the job has taken the workspace;sandbox seedwaits up to 30 seconds for that (over MCP too), and otherwise exits 6 with the command that resumes it.artifacts exportstarts an export job and returns its operation id; with--wait, or later with--job <id>, it writes the tar to--out(defaultsandbox-<job>.tar) after checking its SHA-256, and never over an existing file (exit 4).
Solution as code
A solution is a folder with a cavelon.yaml; cavelon finds the nearest one
from the working directory upwards, so a monorepo can hold several.
solution/
cavelon.yaml instance, tenant, solution (harness), package format, layout; never a token
package/ one file per top-level section of the instance's package schema
tests/ one file per test suite (the schema's test_suites section)
seeds/ your seed scripts and data manifests
env/test.yaml where `apply --env test` goes: tenant, harness, mode, runtime_bindings
env/prod.yaml
.cavelon/ local state, never committed: inventory.md, pull.json, previews/
AGENTS.md a short Cavelon block between markerscavelon init --instance https://cavelon.example.com --tenant acme --harness support
cavelon pull # the live solution into package/ and tests/
$EDITOR package/agents.yaml
cavelon validate # offline, against the cached package schema
cavelon apply --env test # preview: prints and stores a preview id
cavelon apply --confirm <id> --env test
cavelon activate --harness supportinitcreates its own files (cavelon.yaml,package/,tests/,seeds/,env/test.yaml,env/prod.yaml,.cavelon/) and never overwrites them.AGENTS.md,.gitignoreand an existingCLAUDE.md(with an@AGENTS.mdimport) get at most one block betweencavelon:beginandcavelon:endmarkers; a file whose markers are broken is left alone with a warning.--hookadds a git pre-commit hook that runscavelon validate, as a marked block after a hook's shebang. Only an invalid package (exit 3) stops the commit; when the check cannot run, the hook warns and lets the commit through.--updatetouches only the marked blocks and the fallback files a previousinitwrote.init --from <file>brings an existing package file into the solution: a blueprint package, an export from another instance, a package attached to an issue. It reads JSON or YAML and writes it aspullwrites an export, onepackage/<section>.yamlper top-level section and onetests/<suite>.yamlper test suite, sovalidateandapplytake it from there; it never goes through the Admin's import, and the repository stays the source. A file that already holds the same value keeps its bytes. One that would change, or go away because the package lacks its section or suite, stops the import before anything is written (exit 4,package_files_differ, the files indetails.files) unless--force. Sections the instance's package schema does not know are written, reported asignoredand warned about, asvalidatedoes. With no--harness, a package with one harness names the solution in a newcavelon.yamlandenv/test.yaml. A solution the tenant does not have yet,initcreates as a draft, named as in the package, soapply --env testpreviews into it; the MCP toolinitcreates none and names thecavelon harness newcommand instead. The rest ofinitruns as without--from: its own files, the marked blocks.mkdir counter && cd counter cavelon init --instance https://cavelon.example.com --tenant acme --from ../counter-parent.json cavelon validate --offline cavelon apply --env testinit --agents <list>is the fallback for agents without the Cavelon plugin: the skills go into.agents/skills/cavelon-*/and.claude/skills/cavelon-*/, marked as generated, and each named agent gets thecavelon mcpentry:claude(.mcp.json),codex(.codex/config.toml, a marked block),cursor(.cursor/mcp.json),copilot(.vscode/mcp.json),gemini(.gemini/settings.json),kiro(.kiro/settings/mcp.json);piandotherneed onlyAGENTS.md. A JSON file is changed only when rewriting it keeps every other byte; otherwiseinitsays what to add.pullexports the solution (--harness, orcavelon.yaml's), or without one the tenant's full configuration, and splits it along the top-level sections of the instance's published package schema, so no entity type is built into the binary. A file whose content did not change keeps its bytes, sogit diffshows what changed on the instance; a field the export spells out and the file leaves out (an empty list or object,null, a default) is no change. A test suite goes back to the file it came from (.cavelon/pulled-files.jsonrecords which). Files of sections the schema does not know are kept byte for byte. A solution's pull leaves the tenant-wide sections out of the folder (those the schema marksx-cavelon-scope: tenant; on an instance that marks none,tenant_settingsandmodel_registry) unless--include-tenant-wide, which asks the export for them (include_tenant_wide) where the instance takes it, and says when the export carries none. A file of one already there is kept (listed underkept). It refuses when package files have uncommitted changes (exit 4) unless--force, and writes the tenant's solutions, knowledge bases, tools, test suites and sandboxes to.cavelon/inventory.md.validatechecks the files against the package schema thatinit,pullorapplycached, and the package version against those the instance accepts. It contacts the instance only when nothing is cached, or to read a tenant's list (models, skills, tools, knowledge bases, solutions) the package refers to and no command has read yet; never with--offline, which names the checks it skipped instead. Each finding has a file, line, path and code; exit 3 on errors, and with--stricton warnings too. A reference the import preview will block (package_reference_unknown,package_model_unknown) keeps the summary from saying "Valid". A Model Registry row'smax_concurrent_requestsneeds abase_url(model_endpoint_limit_without_base_url); where the package schema does not carry the field, a warning says the import ignores it.applyvalidates, then calls the import preview and prints what changes, which active solutions it reaches, what the target still needs (secret values, OAuth grants, runtime bindings, trigger execution identities), loop budgets and ignored sections, with the preview id. It stores the exact request in.cavelon/previews/, soapply --confirm <id>imports exactly what was previewed, even if the files changed since (it warns). A target that changed since the preview answers409 import_preview_stale: exit 4, and the hint says how to preview again. An import whose own check, re-run when it applies, finds what the preview did not answers409 package_requirements_changedwithblockers: exit 4, each blocker on its own line (in--json, the error'sblockers), "nothing was imported; preview again", and for a code the kit knows (such asruntime_draft_iteration_needs_draft_parent) its hint.applypreviews into an existing solution and never creates one: when the env file,cavelon.yamlor--harnessnames a solution the tenant does not have, it stops withsolution_not_found(exit 1) before anything is sent, and names thecavelon harness new <slug>command that creates the draft (with--nameaspackage/harnesses.yamlnames that harness); preview again after it.runtime_bindingsfrom the env file bind the package's runtime requirements. A solution's import leaves the package's tenant-wide sections out;--include-tenant-widesends them (include_tenant_wide), for every solution of the tenant, and the confirm replays the flag with the preview it stored. The preview shows the instance'stenant_widereport: the sections it left out, or the active solutions they reach when applied. An instance that does not publish that flag imports them with every solution's package, andapplysays so. (--tenant-wide, the flag's name in 0.1.7, is still taken with a warning.) A preview that reaches an active solution, deletes, changes tenant-wide sections or goes toenv/prodsays to show it to a person first.explain <code>looks a rule code or API error code up in the instance's error catalog (and the codesvalidatereports itself): meaning, fix and docs link. Forrun_capacity_busyandmodel_endpoint_busyit adds which limit to raise, who can, and today's values fromlimits, and the instance's pages on capacity (concepts/capacity-and-concurrency,tutorials/plan-model-capacity) where its docs index lists them (--json:read).activatereads the readiness gate and activates only when it passes, never withforce. A personal access token without "may activate" is refused before anything is sent (exit 7); a solution that is not ready exits 3 with its blockers. A solution a channel or an active trigger reaches (the solution'schannel_count, the active triggers bound to it) goes live for them at once, so there it previews and activates only with--confirm; so it does where the instance does not say what reaches a solution.A Masterloop parent and its iteration solution activate without the override in one order: apply the iteration, apply the parent, run the parent's loop suite, activate the iteration, activate the parent. When
applymeetsruntime_draft_iteration_needs_draft_parentorruntime_external_iteration_harness_unavailable, oractivatemeets the parent'smasterloop_iteration_harness_draft, itshintgives the catalog's sentence and hint, the step it points at, and the order;explainadds the same underorder.
Import needs a personal access token (or an Admin session): an instance refuses it to a tenant API key.
Built for agents
- No prompts. Only
loginasks, and only on a terminal; everywhere else it needs--token-stdin. --jsoneverywhere; no colours or progress lines without a terminal, or withNO_COLORset.- Bounded waits:
waitdefaults to 90 seconds,watchandloop watchto 10 minutes. - Commands to copy: a command printed in a hint quotes every name for the
shell cavelon runs in (POSIX shells, PowerShell, cmd), so a Sandbox named
Lab VM 4073stays one argument.CAVELON_SHELL=posix|powershell|cmdnames the shell when the guess is wrong. - Confirm what is destructive:
apply,loop cancel,sandbox seedandtrigger identityshow what they would do, and act only with--confirm. - Bounded output: lists take
--limitand--cursorand print the next cursor; traces are summarised, with the command for each span's detail. - Read-only or changing, on every command and MCP tool.
Exit codes
| Code | Meaning |
|---|---|
| 0 | ok |
| 1 | anything else (not found, an operation failed or was cancelled, a test case failed) |
| 2 | usage: unknown command or option, a missing argument, no instance chosen |
| 3 | validation failed: the arguments or body do not match the instance's schema, or the instance refused them (400, 422) |
| 4 | conflict or stale preview (409, 412) |
| 5 | needs a person (needs_action): the reason and the Admin link are printed |
| 6 | timed out: the operation is still running; wait again to resume |
| 7 | not authorised: no token, or the instance refused it (401, 403) |
| 8 | server or network error (5xx, 429, unreachable, request timeout) |
With --json, an error is {"error": {"code", "message", "hint", "docs", "status", "exit_code"}},
carrying the instance's own error code where it sent one.
cavelon mcp
The same commands as MCP tools over stdio, for agents that prefer tools to a
shell. The tools are coarse, one per workflow command plus api, api_list,
api_describe, docs_search and docs_get, and carry the standard read-only and
destructive annotations. The repository tools (init, pull, validate,
package_schema, apply, explain, activate) and sandbox_seed and
artifacts_export work in the folder the agent started cavelon mcp in;
init and pull change nothing on the instance. Tools that start work never
block: kb_upload, test_run, loop_start, sandbox_seed and
artifacts_export return operation ids, operation_status reads them (at
once, or waiting up to its timeout, at most 50 seconds, with waited_ms in
the answer), and loop_iterations follows a loop
(loop watch and watch stream, so they are no tools). loop_cancel,
sandbox_seed and trigger_identity are marked destructive and change nothing
without confirm: true. limits is read-only; the server's instructions tell
the agent to read it before planning a solution. login and logout are not
tools; a person runs them.
{
"mcpServers": {
"cavelon": { "command": "npx", "args": ["-y", "@cavelon/[email protected]", "mcp"] }
}
}The Cavelon plugin for Claude Code and Codex (plugin/.mcp.json) and
init --agents carry this entry. It runs the released package through npx,
pinned to the kit's minor version: in 0.x a new minor may break, and it reaches
an agent only with a new plugin or init --update. With cavelon installed,
{ "command": "cavelon", "args": ["mcp"] } works as well.
Files on your machine
| Path | Holds |
|---|---|
| ~/.config/cavelon/config.json | the current instance, the tenant chosen with use, which store holds the token. Never a token. |
| ~/.config/cavelon/credentials.json | the token per instance (0600), only where there is no OS credential store |
| ~/.config/cavelon/setup.json | what cavelon setup changed in your coding agents, so setup --remove undoes exactly that (0600) |
| ~/.cache/cavelon/<instance>/<version>/ | the instance's OpenAPI, error catalog and package schema, its capabilities once per tenant (they carry the tenant's limits) and its docs index once per token and tenant; files 0600, folders 0700 |
| ~/.cache/cavelon/update-check.json | when the latest release was last looked up and announced; CAVELON_NO_UPDATE_CHECK=1 turns the check off |
CAVELON_CONFIG_DIR and CAVELON_CACHE_DIR move them; XDG_CONFIG_HOME and
XDG_CACHE_HOME are honoured. CAVELON_CREDENTIAL_STORE=file skips the OS store.
A release's cached copies are kept until the instance reports another version.
A development build (a version with dev, snapshot or local in it) keeps
one version while what it publishes changes, so its copies are read again after
a minute, or checked with the ETag the instance sent with them;
CAVELON_CONTRACT_TTL_SECONDS sets that time. cavelon validate --verbose says
which copy of the package schema it used.
In the working directory, only init, pull and apply write, as described
in "Solution as code", and artifacts export, which writes the tar it
downloads to a new file.
Version 0.1.17: cavelon setup --agents cline writes shared native user settings
and skills; cavelon init --agents cline copies project skills only. Guarded
changes use the person’s own terminal. See Cline
for guarded launch, custom CLI paths and separate editor qualification.
Version 0.1.17: cavelon setup --agents kilo installs Kilo-native settings, skills
and bundled server/TUI plugins. CLI TUI changes ask a fresh native dialog;
editor/headless changes use the person’s terminal. See Kilo
for preserved configuration precedence, guarded launch and lifecycle.
Version 0.1.17: cavelon setup --agents goose writes Goose's native user YAML
extension and skills; project init copies shared skills only. The interactive
CLI uses its built-in person form. Headless runs cancel that form without a
change. System/additional config bindings and personal YAML are preserved.
See Goose for paths, lifecycle and approval limits.
Version 0.1.17: cavelon setup --agents omp installs OMP's own native profile,
MCP entry, skills and bundled autoload extension. It preserves personal YAML
and supports fresh TUI approval; print/RPC calls use the person's terminal.
Use matching profile selection and a guarded launch. See
OMP for lifecycle and qualification limits.
