Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Local

Explaining your app's security

osy explain [path] [--json] [--with-findings]

Explains your app's declared authorization in plain English — who can read, create, update and delete each entity, which fields are masked, each page's auth posture, and where a page's access does not line up with the data it shows.

stablelocalcliauthoringsecurity

Summary#

Turns your app's declared authorization into a plain-English report — who can do what, per entity and per page — so you can reason about access across the whole app without reading a single security predicate. It needs no platform and no database: it parses and resolves, nothing else.

Signature#

osy explain [path] [--json] [--with-findings]

Description#

Your app states access as declarations — a security { } block on an entity, a policy, an [Authorize] on a page. Each is a precise rule, but reading them one file at a time never adds up to the question you actually have: across this whole app, who can read the customer's card number? who can create an order? which page is public? osy explain answers that. It walks every declaration and writes it out in sentences.

Because access here is declared, not coded, the translation is exact and repeatable — the same rules always produce the same words. It is not a summary that guesses; it is a rendering. When a rule uses a shape the report cannot state plainly, it says so — it prints the shape faithfully and marks it (needs review) rather than paraphrasing a meaning it cannot stand behind. A sentence with no such mark is one you can rely on.

The report has three parts.

The data surface — per entity. For every table-backed entity, who may read, create, update and delete it, in one line each:

  • A role check reads as "callers who hold the Staff role." A grant to signed-in users reads as "any signed-in user." A grant that also admits anonymous visitors reads as "anyone, including anonymous visitors (public)."
  • A row filter reads as "only their own rows (where Owner is the caller)." Membership through a related table reads as "only rows they belong to."
  • An entity that grants nothing reads as "no one (denied by default)" — the honest reading of a security { } block with no matching rule, or of an entity with no block at all under secure by default (deny-all).

It also lists field masking: a field a rule withholds ("the CardNumber field is never returned to any reader"), and a [Classification] mask ("Ssn is classified Secret — masked from readers below Secret"), noting where a field is also redacted from the audit trail.

The UI surface — per page. Access is enforced at the data, but each routed page also declares its own posture, and the two are independent — so the report shows both. Per page: its route; whether it is public ([AllowAnonymous]), gated by a policy ([Authorize(P)], stated as the same English the data surface uses), open to any signed-in user (the secure-by-default rule), or a composable fragment that inherits its host's gate; the entities it reads and writes; and any policy-aware controls it carries (a button or field that reflects a policy). A composable that touches no data is left out — the report is about pages that matter to access, not every presentational primitive.

The coverage cross-check. The reason to put the two surfaces side by side is to catch where they disagree. A page's gate opens the screen, but the row read is gated separately at the entity, and the two do not inherit — so a page that lets in a caller the data will deny renders fine and shows an empty list, with no error to point at. The report flags every such gap: a public page over data that is not anonymously readable, a signed-in page over data only a role may read, or a page that reads an entity nothing grants a read to. It only flags what it can prove — a page that plainly admits more than the data serves. It does not guess a direction between two different policies.

Output. Markdown by default — a document you can read, review, or commit. --json writes the whole report as JSON, the form to hand to a coding agent or to diff between two revisions; the English sentences ride inside the JSON, so the machine-readable surface is also the readable one. --with-findings additionally folds in the maturity linter's security and UI advice inline (see Checking your app) — off by default, because the report's job is to state what your rules are, and the findings are what a reviewer would then suggest. Exit is non-zero when the source did not fully resolve, so the report is partial and says so.

For the machine model of the whole app — types, relations, function effects — see Understanding your app. For where the app falls short of a production bar, see Checking your app.

Examples#

osy explain                    # the plain-English security report, human-readable
osy explain --json             # the same report as JSON (the English rides in it too)
osy explain --with-findings    # also fold in the linter's security / UI advice

The data surface for one entity reads like this:

## `Order` — default-deny

- **Read** — Callers who hold the Staff role; or only their own rows (where Owner is the caller).
- **Create** — Callers who hold the Staff role.
- **Update** — No one (denied by default).
- **Delete** — No one (denied by default).

**Fields**

- The CardNumber field is never returned to any reader.

A page whose access does not line up with the data it shows is flagged in place:

## `OrdersPage` @ `/orders`

- **Access** — Any signed-in user (secure-by-default; not [AllowAnonymous]).
- **Reads** — Order
  - ⚠ 'OrdersPage' admits any signed-in user, but 'Order's read is gated by a policy/role — a signed-in user outside
    that grant sees an empty 'Order'.

See also#

Understanding your app — the machine model of the whole app (types, relations, effects).

Checking your app — where the app falls short of a production bar; --with-findings folds it in.

secure by default (deny-all) — why an entity with no security block grants nothing.

security { } — the security { } block this report reads.

principal predicates (IsAuthenticated / IsAnonymous) and open reads — the policies it translates into English.

Related

Checking your app

Checks your app against production best-practice rules and reports where it falls short — the maturity signal, beside…

Understanding your app

Prints your app's RESOLVED model — field types bound to real types, relations wired to the entity they target, each…

secure by default (deny-all)

Deny-all is the posture, and it is the only one: an entity that declares no `security { }` block is denied to every…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

role grants (and the first admin)

A role is granted by an ordinary entity — any entity that has both a reference to your `[Principal]` and a property…

principal predicates (IsAuthenticated / IsAnonymous) and open reads

Two built-in `when` predicates say who a request is: `IsAuthenticated` is a signed-in user, `IsAnonymous` is an…