For coding agents
Give a coding agent what it needs to change an Alvo backend safely, from llms.txt and the JSON Schema to a checked, previewed and idempotent apply.
Alvo is built to be configured by an agent. The whole backend is one JSON file with a published schema, every refusal says what to fix, and every write to the configuration can be previewed and retried safely.
Before you start
Section titled “Before you start”- A stack serving your descriptor: Run your own descriptor.
curlandjqfor the shell recipe. - An API key whose roles reach a management level in the descriptor’s
accessblock. This page usesadmin(rolesadmin,authenticated; secretALVO_ADMIN_KEY_SECRET) and grants that role thedeveloperlevel.
1. Point your agent at the docs
Section titled “1. Point your agent at the docs”Two plain-text files describe this site for a model:
https://alvo.burgyn.online/llms.txt: an index of every page, one line each.https://alvo.burgyn.online/llms-full.txt: the pages themselves as Markdown, with every descriptor, request and captured response inline.
Give your agent the first one in its instructions, and let it fetch the second when it needs the detail.
2. Give the editor the schema
Section titled “2. Give the editor the schema”The descriptor format is a JSON Schema with a description on every key, served at
https://alvo.burgyn.online/schema/v1/project.json. A descriptor’s own $schema value,
https://alvo.dev/schema/v1/project.json, does not resolve yet, so an editor does not find the schema from it. Map the
file pattern to the served URL instead. In VS Code, add this to .vscode/settings.json:
{ "json.schemas": [ { "fileMatch": ["*.alvo.json"], "url": "https://alvo.burgyn.online/schema/v1/project.json" } ]}Every *.alvo.json file then gets completion, hover text and errors while you or your agent type. The same URL works
in any editor that maps JSON files to a schema. The schema is also published key by key in the
descriptor reference.
3. Load the shared skills
Section titled “3. Load the shared skills”The dashboard’s schema assistant learns the descriptor from nine skills, and they are plain Markdown your agent can load too. Each one teaches one part of the format and the mistakes Alvo refuses.
In Claude Code, install them as a plugin. This repository is a plugin marketplace, and its alvo plugin carries
the nine skills. In a session:
/plugin marketplace add Burgyn/MMLib.Alvo/plugin install alvo@mmlib-alvoOr from your shell:
claude plugin marketplace add Burgyn/MMLib.Alvo && claude plugin install alvo@mmlib-alvoClaude Code then loads a skill by itself when a task matches its description, such as adding a rule or a hook, and you
can call one directly as /alvo:alvo-descriptor-rules-and-cel. /plugin updates the plugin like any other.
For another agent, or a folder of your choice, a script downloads the same nine files, no clone needed:
curl -fsSL https://raw.githubusercontent.com/Burgyn/MMLib.Alvo/main/scripts/install-agent-skills | shIt writes them to .claude/skills/alvo-descriptor-*/SKILL.md under the current directory and lists what it installed;
any failed download stops it with an error. To put them elsewhere, such as your agent’s own skills folder, pass the
folder: curl -fsSL https://raw.githubusercontent.com/Burgyn/MMLib.Alvo/main/scripts/install-agent-skills | sh -s -- ~/.claude/skills.
Run it again to update them.
To load them by hand instead, each skill is one file:
- Entities and fields
- Field types and formats
- Rules and CEL
- Hooks
- Computed fields and rollups
- Indexes
- Traits and tenancy
- Project access
- Capabilities and limits
In the repository they live in plugins/alvo/skills/.
4. Read errors by their type
Section titled “4. Read errors by their type”Every refusal, from the Data API and the Management API alike, is an RFC 9457 problem document. Its type is
https://alvo.dev/errors/<slug>; branch on the slug, never on detail, which is prose. Those URIs do not resolve yet:
the slug is the anchor on the Problem types page, so https://alvo.dev/errors/forbidden
is documented at /reference/problem-types/#forbidden. A violation adds a JSON pointer, a stable code
and a fixSuggestion an agent can act on.
5. Change a running backend
Section titled “5. Change a running backend”The Management API changes what the project is. Every route is closed to every
caller except the bootstrap administrator until the descriptor’s access block grants a level, so this descriptor, the help desk from the
Tutorial, adds one:
"access": { "developer": "'admin' in @user.roles"}Add it to your own descriptor and recreate the container before you call the routes below; without a level, every
management route answers 403 forbidden. On the Tutorial’s stack, in its
alvo-help-desk directory, this downloads the descriptor with the block in place and applies it:
curl -fsSL -o help-desk.alvo.json https://raw.githubusercontent.com/Burgyn/MMLib.Alvo/main/website/src/snippets/coding-agents/01-base.alvo.jsondocker compose up -d --wait --force-recreate alvoThe agent’s change adds one field, due_on:
{ "$schema": "https://alvo.dev/schema/v1/project.json", "apiVersion": "alvo.dev/v1", "name": "help-desk", "auth": { "providers": ["local"], "roles": ["admin", "agent"] }, "access": { "developer": "'admin' in @user.roles" }, "entities": { "tickets": { "audit": true, "fields": { "title": { "type": "string", "required": true, "maxLength": 120 }, "body": { "type": "text" }, "priority": { "type": "enum", "values": ["low", "normal", "high"], "default": "normal" }, "status": { "type": "enum", "values": ["open", "closed"], "default": "open" }, "estimate_hours": { "type": "decimal", "precision": 5, "scale": 1 }, "hourly_rate": { "type": "decimal", "precision": 6, "scale": 2 }, "estimate_cost": { "type": "decimal", "precision": 12, "scale": 2, "computed": "estimate_hours * hourly_rate" }, "due_on": { "type": "date" } }, "rules": { "list": "'authenticated' in @user.roles", "get": "'authenticated' in @user.roles", "create": "'agent' in @user.roles || 'admin' in @user.roles", "update": "'agent' in @user.roles || 'admin' in @user.roles", "delete": "'admin' in @user.roles" }, "hooks": { "beforeCreate": [ { "action": { "mutate": { "title": { "$cel": "trim(new.title)" } } } }, { "condition": "new.priority == 'high' && !has(new.body)", "action": { "reject": "A high-priority ticket needs a body." } } ] } } }}-
Check an expression. While it edits, the agent can ask what the apply would say about one expression, without applying anything. The body carries the working copy as
descriptorJson, the JSON pointer of the slot (path, here/entities/tickets/rules/update) and the candidate (source). A typo in a role:Request POST /management/projects/help-desk/cel/check HTTP/1.1X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRETContent-Type: application/json200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"findings": [{"path": "/entities/tickets/rules/update","message": "'agnet' is not a declared role, so this membership test can never match.","fixSuggestion": "Did you mean 'agent'? Declared roles: admin, agent, anon, authenticated.","severity": 0}],"isValid": false}An expression with a problem is a 200 with findings, not an error status. The corrected expression comes back clean:
200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"findings": [],"isValid": true} -
Preview the apply. Read the current revision, then send the whole descriptor with
?dryRun=true. The answer is the migration plan, and nothing is applied:Request GET /management/projects/help-desk/descriptor HTTP/1.1X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRET200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"project": "help-desk","revision": 1}Request PUT /management/projects/help-desk/descriptor?dryRun=true HTTP/1.1X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRETIf-Match: "1"Content-Type: application/json200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"applied": false,"revision": 1,"plan": {"isEmpty": false,"hasDestructiveChanges": false,"steps": ["AddField tickets.due_on"]},"replayed": false}The body is
{"descriptorJson": "<the whole file as one string>"}. From a shell,jqbuilds it from the file, and the current revision goes intoIf-Match:Terminal curl -fsSL -o help-desk.alvo.json https://raw.githubusercontent.com/Burgyn/MMLib.Alvo/main/website/src/snippets/coding-agents/02-add-field.alvo.jsonREVISION="$(curl -sS localhost:8080/management/projects/help-desk/descriptor \-H "X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRET" | jq -r .revision)"jq -n --rawfile d help-desk.alvo.json '{descriptorJson: $d}' \| curl -sS -X PUT "localhost:8080/management/projects/help-desk/descriptor?dryRun=true" \-H "X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRET" \-H "If-Match: \"$REVISION\"" \-H "Content-Type: application/json" \-d @-Drop
?dryRun=trueand add anIdempotency-Keyheader to apply it for real, as the next step does. -
Apply it. A write must say which revision it replaces, in
If-Match. Without it, the API refuses rather than risk a lost update:Request PUT /management/projects/help-desk/descriptor HTTP/1.1X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRETContent-Type: application/json428 Precondition Required
Response HTTP/1.1 428 Precondition RequiredContent-Type: application/problem+json{"type": "https://alvo.dev/errors/precondition-required","title": "Precondition Required","status": 428,"detail": "This write requires 'If-Match' carrying the descriptor's current revision, e.g. If-Match: \"3\". Read that revision from GET the same path. Applying without one is a lost update nothing would detect."}With
If-Matchand anIdempotency-Key, the change is applied and the revision advances:Request PUT /management/projects/help-desk/descriptor HTTP/1.1X-Alvo-Api-Key: admin.$ALVO_ADMIN_KEY_SECRETIf-Match: "1"Idempotency-Key: help-desk-add-due-onContent-Type: application/json200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"applied": true,"revision": 2,"plan": {"isEmpty": false,"hasDestructiveChanges": false,"steps": ["AddField tickets.due_on"]},"replayed": false} -
Retry safely. If the response is lost and the agent sends the same request again, with the same key, the API does not apply it twice. It answers with the revision the first request appended, and
replayed: truesays this is that earlier result:200 OK
Response HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"applied": true,"revision": 2,"plan": {"isEmpty": true,"hasDestructiveChanges": false,"steps": []},"replayed": true}On a replay the plan is empty: it describes what this request did, which is nothing. Read what a revision applied from
GET …/revisions/{n}.
Rollback, destructive changes and revision history are in Apply and evolve your descriptor.
Access levels
Section titled “Access levels”The access block maps the caller’s roles to three levels; the highest one that matches wins. A descriptor without
access admits nobody but the deployment’s bootstrap administrator.
| Level | May |
|---|---|
viewer | read the descriptor, its revisions, the resolved schema, the capabilities and the CEL function catalog, and simulate a policy |
developer | everything a viewer may, plus check an expression, apply a descriptor and roll back, as long as the access block itself stays the same |
admin | everything, including a change to the access block, the AI connection and the people who may sign in |
An API key’s scopes limit only the Data API; on the Management API, a key reaches whatever its roles reach. The
per-route table is in docs/architecture/management-api.md.
What can go wrong
Section titled “What can go wrong”| Status | Problem type | When | Fix | Returned by |
|---|---|---|---|---|
| 403 | forbidden | The key’s roles reach no level the route requires, or a developer changed the access block. | Grant the role a level in access; have an admin make access changes. | every host |
| 409 | idempotency-conflict | The Idempotency-Key was already used by this caller for a different request. | Use a fresh key for a new request. | every host |
| 412 | precondition-failed | If-Match names a revision that is no longer current. | Read the descriptor again, reapply your change to it, and send the new revision. | every host |
| 422 | validation | The descriptor is refused, for example an expression that does not compile; the violation’s pointer names the place. | Fix what the violation’s fixSuggestion says, and check the slot with cel/check first. | every host |
| 428 | precondition-required | The write carried no If-Match. | Read the current revision and send it as If-Match: "<revision>". | every host |
Reference
Section titled “Reference”- Management API: every route and its body.
- Descriptor keys:
access, and the whole schema. - CEL functions and Capabilities in this build, which an agent should read before proposing a key this build does not run.
- Problem types:
forbidden,idempotency-conflict,precondition-failed,precondition-required,validation.
Apply and evolve your descriptor: the full lifecycle of a change, from a dry run to a rollback.