← All writing

Give the AI Less to Be Wrong About

ADL, seeds, and guardrails for vibe coding

Picture an internal tool built in an afternoon. A team lead describes what they want to a coding agent: a small app for tracking customer escalations, with a place for support notes and a way to mark tickets resolved. Twenty minutes later it exists. FastAPI backend, React front end, database, auth. It works. Everyone is impressed. Nobody reads the code, because nobody was ever going to read the code; that was the whole point.

Four months later someone notices that the endpoint listing tickets doesn’t check who’s asking. Every customer escalation, every internal note about every customer, readable by anyone with the URL. The agent didn’t fail. It produced exactly what was asked for, and the asking never mentioned authorization, and the human in the loop reviewed the app by clicking around in it, which is the only kind of review the workflow allows.

This is the vibe coding argument in miniature. Not “AI writes bad code.” AI writes plausible code, at volume, and the errors concentrate precisely where clicking around doesn’t reach: permission checks, state transitions, validation edges, the boundaries between what one user may see and another may not. Andrej Karpathy’s original description of vibe coding was honest about this: you “forget that the code exists.” That is a perfectly rational trade for a weekend prototype. It is a catastrophic trade for anything that holds someone else’s data, and the industry is currently making it thousands of times a day.

The asymmetry nobody priced in

Here is the economic shape of the problem. AI collapsed the cost of producing code. It did not collapse the cost of verifying it. Reading code for security correctness is as slow as it ever was, and it now has to keep pace with agents that emit five thousand lines before lunch.

The industry’s proposed fixes are all on the review side: better code-review agents, security scanners in CI, “trust but verify” workflows. These help. They are also structurally losing, because they accept the premise that every generated application gets its own bespoke implementation of authentication, authorization, validation, and lifecycle logic, and then race to audit all of it. A thousand vibe-coded backends means a thousand independently hand-rolled security surfaces, each one novel, each one unaudited by default.

There is another response, and it is older than AI: shrink what has to be verified. Don’t review the AI’s auth code faster. Arrange things so there is no auth code for the AI to write.

Declare the application, don’t generate it

This is the design premise behind ADL, the Application Definition Language that our Kalos runtime interprets. An ADL file is a YAML document that declares a backend application: the data model, the relationships, the permissions, the state machines, the validation rules, the audit events. The runtime does not generate code from it. It interprets it. Submit the file, and the schema, the REST API, the access control enforcement, the lifecycle rules, and the OpenAPI documentation all exist, derived from one artifact.

The division of labor is the point. The agent authors the declaration. The engine supplies the load-bearing semantics: the RBAC enforcement order, the interception of direct writes to state-machine-controlled fields, the validation pipeline ordering, the atomic migrations. Those semantics were implemented once, by humans, tested once, and are shared by every application the engine runs. The agent cannot get them subtly wrong, because the agent never touches them.

Here is roughly what the escalation tool from the opening looks like as a declaration:

domain:
  name: escalations
  version: "1.0.0"

  objects:
    Timestamp:
      createdAt: { type: datetime, autoNow: create }
      updatedAt: { type: datetime, autoNow: update }

  enums:
    TicketStatus:
      values:
        open:     { label: "Open",     color: "#3b82f6" }
        working:  { label: "Working",  color: "#f59e0b" }
        resolved: { label: "Resolved", color: "#10b981", final: true }

  resources:
    Ticket:
      object:
        id:          { type: uuid, primaryKey: true }
        title:       { type: string, required: true, minLength: 3, maxLength: 200 }
        customer:    { type: string, required: true, maxLength: 120 }
        details:     { type: text, required: true }
        status:      { $ref: '#/enums/TicketStatus', default: open }
        resolution:  { type: text }
        createdById: { type: string, required: true, immutable: true, maxLength: 100 }
        $merge: { $ref: '#/objects/Timestamp' }
      indexes:
        - { fields: [status] }
        - { fields: [createdById] }
      stateMachine:
        field: status
        initial: open
        states:
          open:
            transitions:
              start: { to: working, guards: [agent, admin] }
          working:
            transitions:
              resolve:
                to: resolved
                guards: [agent, admin]
                requiredFields: [resolution]
          resolved:
            final: true
      permissions:
        list:   [agent, manager, admin]
        get:    [agent, manager, admin]
        create: [agent, admin]
        update: [owner, manager, admin]
        delete: [admin]

Forty-odd lines. Read the permissions: block. That is the entire authorization surface of the application, and a manager who has never written code can review it in under a minute. The failure mode from the opening story (a list endpoint that forgot to check the caller) is not a mistake this artifact can express. There is no place to put it. An omitted permissions block doesn’t default to open; it defaults to admin-only, because the convention was chosen by a human thinking about failure modes, once, for every application.

Now compare the audit problem. The generated-code version of this app is a few thousand lines of Python: route handlers, ORM models, Pydantic schemas, JWT middleware, dependency-injected permission checks scattered across files. Every one of those lines is a place the agent could have been confidently, silently wrong. Which artifact would you rather be responsible for reviewing at the pace agents produce applications?

The parser is a critic in the loop

There is a second property here that matters specifically for AI authorship, and it is easy to miss: a hallucinated backend runs. A hallucinated ADL file doesn’t load.

Generated Python with a subtly wrong permission check starts up, serves requests, and passes the click-around test. It fails silently, in production, months later. ADL’s grammar is closed: the type system is a fixed set, domain names are validated, unknown constructs are rejected, and the parser returns a structured 422 at submission time. When an agent invents a field type, references an enum it never defined, or emits a permission symbol the language doesn’t support, the error is immediate, loud, and machine-readable.

For agent workflows this is more than a safety property. It is a feedback loop. The parser acts as a critic: the agent submits, reads the 422, corrects, resubmits. An entire class of AI error is converted from “latent vulnerability” into “compile-time conversation.” We lean into this deliberately: ADL ships with an LLM Authoring Spec, a generation contract written for machine consumption, with MUST/AVOID tables and a pre-emission checklist targeting the specific mistakes language models actually make. The language was designed knowing who would be writing it.

Seeds: closing the vibe loop

The part of vibe coding worth keeping is the loop: describe, see it working, iterate. A backend with an empty database doesn’t demo, and an agent that can’t see its work running can’t check its work.

This is what the seed manifest is for. Alongside the domain file, the agent emits a seed: roles, then users, then role assignments, then data, in that order, idempotently.

roles:
  - { name: admin,   description: "Full access" }
  - { name: agent,   description: "Handles tickets" }
  - { name: manager, description: "Oversees the queue" }

users:
  - { username: admin, email: admin@demo.test, password: "Admin123!", verified: true }
  - { username: dana,  email: dana@demo.test,  password: "Demo123!",  verified: true }

assignments:
  - { username: admin, roles: [admin] }
  - { username: dana,  roles: [agent] }

data:
  - action: adl.escalations.ticket.create
    as: dana
    data:
      title: "Login fails for enterprise SSO customers"
      customer: "Acme Corp"
      details: "Reported by three accounts since Monday."
      createdById: "dana"

Notice the as: dana. The seeder dispatches that create as the agent-role user, through the same permission pipeline as a real request. Even the demo data respects the security model: the seed cannot fabricate a record that no role could legitimately have created. Anyone who has watched a coding agent seed a database by writing raw INSERTs around the ORM will understand why this detail matters.

Together, domain plus seed plus a generated verification script mean the agent’s deliverable is a running, populated, role-configured application it can immediately test against:

TOKEN=$(curl -s -X POST "$BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"username":"dana","password":"Demo123!"}' | jq -r '.data.access_token')

curl -s "$BASE/api/v1/escalations/tickets?filter[status]=open" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.items'

The agent runs the curls, compares responses to expectations, and iterates. This is the closest thing vibe coding has ever had to test-driven development, and the human never had to read a line of implementation to get it.

What guardrails don’t do

Now the honest part, because the argument dies if it’s oversold.

Declarative authoring narrows the error surface. It does not eliminate error; it moves the residual errors up a level of abstraction. We know this concretely, because we have reviewed a substantial body of LLM-authored ADL and cataloged what survives. The parser catches the loud mistakes: invented routes, undefined enums, fields required by a transition but never declared. What it does not catch is valid YAML expressing the wrong policy: an event conditioned on a deadline that can never fire because time passing is not a field change; a required approval satisfied by a boolean explicitly set to false, because “required” checks presence, not value; a segregation-of-duties claim undone by putting admin in every guard; a regulatory threshold computed against the wrong base.

These files load. They run. They are wrong in exactly the way a policy document can be wrong, and no parser will save you from a correctly-stated bad policy.

Two things follow. First, the honest claim is guardrails, not autopilot. A wrong ADL policy is still a categorically better failure than a wrong generated codebase — it is forty lines of YAML that a compliance officer, a security engineer, or a second AI can actually read, rather than a few thousand lines of Python nobody will — but human judgment has not been declared obsolete. It has been given a surface small enough to exercise judgment on.

Second, the residual error classes are lintable. Every failure pattern we cataloged is a static check over the YAML: flag time-based conditions attached to field-change events; flag booleans in required-field lists; flag guards where the owner symbol can never resolve; flag foreign keys without indexes; flag enums referenced but never defined. A linter that runs between the agent and the parser (a second guardrail layer that critiques policy shape, not the grammar the parser already handles) is the obvious next artifact, and it is on our roadmap for exactly this reason. The defects taught us what to build.

The reframe

Vibe coding is not the problem. The unbounded authoring surface is the problem. When an agent is free to hand-roll authentication, authorization, and lifecycle logic per application, “forget the code exists” is negligence. When the agent’s output is a constrained declaration interpreted by an engine whose semantics were verified once for everyone — when hallucination fails loudly at submission instead of silently in production, when the demo data itself obeys the permission model, when the whole security posture of the app fits on one screen — forgetting the code exists becomes something closer to reasonable, because there was never any code to forget.

The fix for AI-built software is not reading the AI’s code more carefully. It is giving the AI less to be wrong about.


ADL v0.3 runs on Kalos, ISDG’s C++ application-server runtime. The complete language reference and the LLM Authoring Spec are available on request.