Written by agents
A language that answers questions, for a reader that cannot ask a colleague.
More of your code is being written by something that has no tribal knowledge, no teammate to interrupt, and no memory of why this field is nullable. What it has is the source, the compiler, and whatever the compiler is willing to say. Osy# was built to be asked — and to make a wrong answer land somewhere it cannot do harm.
osy modelWhat the app IS — resolved, not inferred from the source.
osy explainWho can read what, computed from the declarations.
osy inspectWhat actually ran, end to end, step by step.
osy checkOne verdict, and a refusal that carries its fix.
01
The reader changed. Most languages have not noticed.
Every language quietly assumes its reader can do things a machine cannot.
Ask a colleague which of these forty tables the invoice one really is. Notice that the auth check
three files away is why this query returns nothing. Remember that Amount went nullable
because of a migration in 2023. None of that is written down. All of it is load-bearing.
An agent has none of it — and unlike a new hire, it will not come back and ask. It will produce something plausible instead. So the useful question is not whether the model is good enough. It is whether the language will tell it the truth when it asks, and what happens when it does not.
Everything below is the ordinary toolchain. Not a plugin, not an MCP server, not a prompt.
02osy model
Ask what the app is.
One command, and the whole resolved model — not the files, the model the compiler built out of them. No database, no server.
$ osy model --json { "summary": …, "capabilities": …, "entities": [ … ], "provided": …, "providedEnums": …, "enums": …, "functions": …, "workflows": …, "clients": …, "components": …, "themes": …, "topics": …, "apis": … } // one entity out of that, verbatim { "name": "Invoice", "kind": "entity", "fields": [ { "name": "Amount", "type": "decimal", "required": true }, { "name": "Owner", "type": "User", "relation": { "target": "User", "kind": "reference" } } ], "security": { "posture": "deny", "rules": [ { "effect": "allow", "operations": ["read"], "hasRowFilter": true } ] }, "platformFields": ["Id", "CreatedAt", "ModifiedAt", …] }
Every field with its real type. Every relation wired to its target. Each entity's security posture as the compiler computed it. The alternative is what an agent does everywhere else — grep for a class name, read three files, and infer. Inference is where the wrong answers come from.
03osy explain
Ask who may read what.
Generated from the same declarations the runtime enforces — so it cannot drift from the behaviour, which is the failure mode of every security document ever written.
entity Invoice { [Required, MaxLength(80)] string Reference; [Required] decimal Amount; User Owner; security { allow read where Owner == user; allow create, update where Owner == user; } } [Principal] entity User { [Unique, MaxLength(200)] string Email; [MaxLength(200)] string PasswordHash; security { allow read when IsAuthenticated; allow read, create when IsAuthenticator; deny read PasswordHash when !IsAuthenticator; } } // RoleGrant declares no security block at all. entity RoleGrant { User Grantee; [Required] AppRole Level; }
$ osy explain # Security report — who can do what > Posture: deny-all. An entity with no `security { }` block grants no access to anyone. ## `RoleGrant` — deny-all > No `security { }` block, and deny-all is in force — this entity grants NO access to anyone. - Read — No one (denied by default). ## `Invoice` — default-deny - Read — Only their own rows (where Owner is the caller). - Delete — No one (denied by default). ## `User` — default-deny - Read — Any signed-in user; or the caller holds the Authenticator role — `PasswordHash` carries a rule of its own. Fields - The PasswordHash field is withheld from a reader unless the caller holds the Authenticator role.
04osy validate
A refusal that carries its own fix.
Error messages are the largest surface an agent reads. They were written as such — because a diagnostic that names a line and stops is a search, and a search is a wrong turn waiting to happen.
// Invoice.Amount is a decimal, and Max over NO rows has no value // to answer with — so this returns null through a `decimal`. decimal biggest() { return Invoice.Max(i => i.Amount); }
$ osy validate model/bad.osy:1:39 ERROR RESOLVE_ERROR this function returns 'decimal', which cannot hold null — but this is the result of a `Max(…)` aggregate, which answers null over NO ROWS — `Min`/`Max`/`Average` have no zero to fall back on, where `Sum` and `Count` answer 0, and nothing here says what to return when the value is absent. Answer for absence at the `return` — `?? 0m` if that is what absence means here — or declare the function 'decimal?' and let it be absent. → for more, RUN: osy docs query-aggregates
Three things there that an ordinary error does not have: why the rule
exists, both remedies rather than the first one, and the contrast that makes it stick —
Sum and Count behave differently, and now you know that too.
What a guess cannot turn into
Max(…) returned through a non-nullable decimalnew List<string> { "a", "b" }osy check refuses it before you run.int x; read on a path that never assigned itAnd the whole of it survives into --json,
message and doc topic included — so the loop does not have to scrape a terminal.
$ osy validate --json { "success": false, "files": 3, "errors": [ { "file": "model/bad.osy", "line": 1, "column": 39, "code": "RESOLVE_ERROR", "message": "…", "docTopic": "query-aggregates" } ], "warnings": [], "symbols": { "entities": ["Invoice", …] } }
05osy inspect
Ask what actually ran.
A compiler can only tell you about the program. The other half of debugging is the run — and that is the half an agent normally cannot see at all, so it re-reads the code and guesses again.
// The test, and the function it calls. Three parameters, two locals — // which is what makes the trace below worth reading. [Test] void a_rate_is_applied() { Assert.Equal(12.35m, applyRate(49.4m, 0.25m, "quarterly")); } decimal applyRate(decimal amount, decimal rate, string note) { var scaled = amount * rate; var rounded = Math.Round(scaled, 2); return rounded; }
$ osy trace start --full $ osy test $ osy inspect ╭───────────┬───────────────────┬───────────┬───────┬──────────╮ │ trace │ function │ outcome │ steps │ started │ ├───────────┼───────────────────┼───────────┼───────┼──────────┤ │ e9b6cab1… │ a_rate_is_applied │ Completed │ 7 │ 15:32:00 │ ╰───────────┴───────────────────┴───────────┴───────┴──────────╯ $ osy inspect e9b6cab1 0 server stmt a_rate_is_applied 1 server eval applyRate amount = 49.4 rate = 0.25 note = "quarterly" 3 server stmt applyRate scaled = 12.350 4 server return applyRate scaled = 12.350 rounded = 12.35 5 server stmt a_rate_is_applied $ osy inspect e9b6cab1 --step 3 // the STACK at that moment, innermost first server applyRate scaled = 12.350 server a_rate_is_applied → applyRate amount = 49.4 rate = 0.25 note = "quarterly" server a_rate_is_applied (no locals in scope)
The values, not just the shape. Every step names the function it ran in
and carries the locals in scope; --step unrolls the whole stack at that moment, frame by
frame, with the arguments each call was made with — a_rate_is_applied → applyRate reads
as what it is. That is the difference between “the function was entered” and “the function was
entered with 0.25”, which is usually the entire question.
Server and client under one correlation id. And a fault is captured
whether recording was on or not — the thing you most need to see is the thing you were least likely
to have prepared for. --fault jumps straight to the throw site.
Four verbs, and the app has stopped being a guessing game. Now make being wrong cheap.
06
And the guess that goes wrong reaches nothing.
Everything above makes an agent right more often. This is the part that decides whether letting one near your data was a good idea.
In most stacks, authorization is a thing you remember to write. A missing check is invisible: the code reads fine, the tests pass, the endpoint returns everyone's rows. That is a bad failure mode when a person writes it. It is a much worse one when the author is a machine producing plausible code at speed — because plausible is exactly what a missing check looks like.
Here, access is declared on the entity and the default is deny. An entity with no
security { } block grants nobody anything: the empty state is the safe one, not the
open one. There is no check to forget, because there is no check to write — the filter is applied
to every read the language can express, by the engine, underneath.
The shape of a mistake
security blockSo it is right more often
It can ask what the app is, who may read what, and what actually happened — and get an answer that is true of THIS app, not of the framework in general.
So being wrong is bounded
The blast radius of a bad guess is set by a declaration, not by whether the guesser remembered to check. Nothing it can write reaches data the declaration did not open.
Either one alone gives you a fast way to make a mess, or a safe way to be useless.
07osy check
The loop, in full.
Shorter than this page makes it look.
$ osy init --agent claude // scaffolds CLAUDE.md + settings $ osy model --json // what am I working on $ osy docs <anything> // how is this spelled … write the code … $ osy check // validate + lint + test — ONE verdict $ osy inspect --fault // when it ran, and was wrong
osy check is one command and one answer, which is the shape a loop
wants — not three whose output has to be reconciled. Nearly all of it answers --json.
And osy docs exists so the vocabulary comes from the platform rather than from reading
the compiler's source, which is what an agent does when nothing better is on offer.
Where to go next
Default-deny in full: row filters, field masks, and the two things it deliberately does not do.
The other direction — agents INSIDE your app: declaring one, streaming its answer, parking it on a person.
Where the compiler put each line, and why you did not choose.