Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Local

Checking your app

osy lint [path] [--json] [--strict]

Checks your app against production best-practice rules and reports where it falls short — the maturity signal, beside the correctness ones. Covers security, the tests that prove it, the data model, the cost of your queries, and what happens when a remote call fails.

previewlocalcliauthoring

Summary#

Reports where your app falls short of a production-grade bar. Compiling tells you the app is correct; osy lint tells you whether it is finished — starting with the category that hurts most when it is not: security.

Signature#

osy lint [path] [--json] [--strict]

Description#

Findings come in three tiers:

  • MUST — a production app is broken or exposed without it. Every rule at this tier compiles and type-checks — which is exactly why they need a linter. Most are security; the other is a number that comes back wrong.

    • A total that is quietly short. A function creates rows, does not commit, and then sums, averages or takes the min or max over that entity (Sum / Average / Min / Max / Count). The database computes the aggregate over committed rows only, so the answer silently omits the rows just created — it is short by exactly the work the function just did. Nothing fails: the code compiles, runs, and hands back a wrong number. Call UnitOfWork.Commit(); before the aggregate.

    • A query that filters on an edit you have not committed. Change a property on a row, do not commit, then query that entity with a filter on the property you changed. The filter runs in the database, against the committed value — the one from before your edit. So the query misses the row your edit would now match, and still returns the row it no longer matches; and that row then reads back with your new value, contradicting the very filter that selected it. Commit before the query, or filter the rows you already hold in memory. (Reading the value straight off the row you edited is fine — that always shows your edit. It is only the filter that is computed on the old value, and only for a property you actually changed.)

    • The login nobody tests. The app has an [AuthMethod] and no test proves it both ways — that a right credential is accepted and that a wrong one is refused. Every access rule you wrote sits behind this one function, and it is the one that fails invisibly: a login which handed a ticket to anybody passes a suite that only signs in successfully, and the app behaves exactly as it does now until the wrong person is holding a ticket. Test the refusal too — and include an address with no account, which must fail exactly like a wrong password, or the form tells an attacker which addresses are registered.

    • A credential is handed out. An entity grants reads and a sensitive field (PasswordHash, *Token, *Secret) has no field-level deny read, so it goes to everyone who can read the row.

    • A role grant can be written by its own subject. A role grants (and the first admin) table whose writes are ungated — or guarded by a where row-filter, which here says "you may write your own privileges" — lets a caller hand themselves whatever role the rest of the app trusts. Every other rule you wrote is then decoration. The same hole is reported when someone may update or delete a grant they could not have created: editing a Member grant into an Admin one is the identical escalation through another verb.

    • The auth flow is denied its own credential. An [AuthMethod] runs as the ephemeral auth principal, which bears a role and has no user — working out the user is what it was called to do. An entity it must read, granted only by a where row-filter, therefore denies it: there is no user id to match, so the login cannot read the record it exists to check and fails every time. Your tests still pass, because they run as a real signed-in user for whom that filter works perfectly (auth bootstrap (login, before anyone is signed in)).

    • A secret is compared with ==. storedHash == Security.HashPassword(password) is not merely insecure — it can never be true, because a hash is salted; the login cannot succeed (use Security.VerifyPassword, Security.* — hashing, verifying, tickets, random ids). And == on an HMAC tag leaks, through how long the check takes to fail, how much of a guess was right — enough to forge one (use Crypto.FixedTimeEquals, Crypto.HmacSha256Hex and Crypto.FixedTimeEquals).

    • An integer-only format on a Double. someDouble.ToString("X") or "D" — those specifiers are integer-only, so .NET throws a FormatException at runtime. It compiles (ToString takes any string), so only the crash tells you. Format the Double with N/F/C/P, or convert to an integer type first.

  • SHOULD — expected, and worth flagging.

    • A login that says WHICH half of the credential was wrong. Answering "no account with that address" and "wrong password" differently lets anybody discover which addresses are registered, without ever guessing a password — the input to credential-stuffing and to targeted phishing. It never looks like a security decision while you are writing it; it looks like a helpful error message. Answer every rejection identically, and put the specific message where it is safe to be specific: the sign-up page, or a reset flow that emails the address rather than telling the browser.

    • An entity with no security { } block is safe (deny-all means it grants nothing to anyone) but is usually a grant someone forgot to write — the app cannot read its own data.

    • A credential written to the log. Log.*(… someHash, someToken …) — a log line is not private (it ships to a sink, is retained, often indexed), and nothing redacts it for you. Log an id or an email that identifies the record, never the credential field itself. (Only fields derived as a credential or ending in Hash/Token/Secret/… are flagged — an innocently-named TokenCount is left alone.)

    • An app built to be multi-user, with no way to log in. It declares a [Principal], a [Role] enum, and a gated surface — a secure-by-default page or a role-grant table — so it plainly means to have users; but not one function is an [AuthMethod], so there is no login path at all. Nobody can authenticate, so nobody can pass the deny-all gate those pages and grants sit behind: the app compiles, its data model is complete, and not one real user can get in. Add a login [AuthMethod] and wire it in app.AuthBootstrap (auth bootstrap (login, before anyone is signed in)). A fully public app — no [Principal] — is a valid choice and is never flagged; nor is a [Principal] modelled as plain data with no roles or gated pages yet.

    • A rule nobody ever tried to break. An entity whose rules can deny, and no test acting as a principal proves they do; or an invariant no Assert.Throws proves bites. A rule you have not tested is a rule you only believe you wrote — and an untested invariant does not fail loudly when it rots: the day someone deletes it to make an import work, the suite is still green. A [Test] that asserts nothing at all is reported for the same reason.

      The two refusals are proved differently, and the difference matters. A denied write throws, so Assert.Denied settles it on its own. A denied read does not throw — row security is part of the query, so the row was never in the result set — and you prove it with an empty result. But an empty result is evidence of a denial only if there was something there to deny: on its own it passes just as happily when the rule refuses everyone, when the table is empty, or when the row was never created. So pair it — show that somebody can see a row of that entity, right beside the principal who cannot. Unpaired, the assertion is reported, because it does not yet prove what it appears to.

    • A rule that cannot say why. An invariant or a [Pattern] with no message refuses the user with the rule itself — and a pattern's rule is a regular expression, which explains nothing.

    • An unbounded string. No [MaxLength] means the caller decides how much you store. Fine for prose; wrong for a code, a name or a status.

    • An enum value that reads as a run-together word. Without a [Label] label, an enum member is shown by its own name — perfect for Draft, and wrong the moment there are two words: the grid cell says "InProgress". Only multi-word members are reported; a single-word one needs no label.

    • Work that only shows up on real data. A query materialized with no Take fetches however many rows happen to exist; a Skip with no OrderBy lets page 2 repeat a row from page 1; reading a child collection in a loop over parents is one query per parent; and the same query run twice does the work twice — and may give two different answers. All four are correct, fast on a laptop, and the reason an app that worked in development falls over in production.

    • A remote call that assumes it works. An outbound call (Http.*) has two failure modes and they need two different answers. The network throws — a timeout, a DNS failure, a refused connection — and with no try that kills the function outright, so the caller sees an internal error instead of the failure you meant to handle. The response does not throw: a 404 or a 500 comes back as an ordinary result, so code that never looks at IsSuccess carries on and uses the error page's body as though it were the answer — wrong data, no stack trace, no log line.

    • A function that reaches the network through something it calls. The call may be nowhere in the function's own text — a helper makes it — and the function still has no try. A timeout down there kills this one exactly as dead. Nothing you can read in it warns you, which is the whole reason the linter looks past the source and at what the code actually does.

    • A catch that says nothing. A caught exception with no log is a failure the app decided to survive and then forgot. In production it is invisible: no trace, no count, and no way to answer why the numbers are off.

  • CONSIDER — a candidate for your judgment, reported with its evidence. Never an error.

    • A [Unique] or [Pattern] field with no [Required]: every constraint except [Required] lets null through, so two rows may both have no value and not collide. If the field is genuinely optional that is exactly right — and if you read [Unique] as "every row has one", it does not say that. Only you know which was meant, so the linter asks rather than asserts.
    • A routed page with no title. A page declares its name with [Title("…")] (the chrome/route name a breadcrumb reads) or a meta { title = "…"; } block (the SEO <title>). A routed page with neither is a nameless browser tab and an accessibility gap — a screen reader announces a page by its title on navigation. A title-less route can be deliberate (a redirect-only page), so it is a candidate, not an error.
    • An editable field under a rule the browser can't pre-empt, in a form that catches nothing. Plain field rules ([Required]/[MaxLength]/[Pattern]/…) are surfaced for you — the input carries them as native attributes, so the browser blocks bad input and paints the invalid state without any app code. Two rules can't work that way and still throw a ValidationException on save: a cross-field invariant (Paid <= Total), which the client can't evaluate; and [Unique], enforced by the atomic DB index — a client "is this taken?" check races with the index, so the check is a UX nicety and the catch is the real guard. If a form edits such a field and catches no error, the user sees a raw failure — catch it where the form saves and show the message. If the component handles errors at all, it is not flagged.
    • A class method that quietly hands off to the server. A class method is client-runnable code, so a call it makes to a function or method that runs on the server is a network round trip — and nothing at the call site shows it; whether the callee stays on the client is a fact about its body, not the call. Now that almost everything runs on the client, a server hop is the notable exception. If it is intended, leave it; if the method was meant to stay client-side, keep the server-only work off its path.
    • A number/date format that runs on the server. value.ToString(format[, culture]) formats in the browser only for the specifiers the client reproduces byte-identically; anything else fails closed to the server — a round trip, invisible at the call site. A Double outside N/F/C/P, a custom pattern over a Double (which also rounds differently), a runtime-built format or culture, an unsupported specifier under a culture, or an unsupported date format all round-trip. The finding names which, and the fix (switch the specifier, use a Decimal, make the format or culture literal). If the round trip is fine, leave it.

Each finding names the rule, what it is about, and how to close it.

--strict makes any MUST-tier finding fail the run, so it can gate a ship. --json writes the findings as JSON, for a coding agent or a CI step.

This is a growing rule set, not a finished one — rules are added as patterns emerge. To see what the app is rather than what it is missing, use Understanding your app.

Which rules exist?#

Every rule the linter knows, from the compiler's own catalogue. This list is generated when the page is built, so it cannot name a rule this release does not have; osy lint --rules prints the same list from the binary. A count on its own would be a vanity number — the names are the point: each one says what it catches.

A rule id is a search term. Type the id the linter printed into osy docs and it answers: the page that documents the rule, or — for a rule no page discusses on its own — this section, with the rule's own line above it so you learn what it catches before anything opens.

⚠ The example below uses a placeholder rather than a real id ON PURPOSE — naming a specific undocumented rule here would make THIS page the one page that mentions it, which is enough to make the lookup treat this page as the rule's OWN documentation instead of falling through to this catalogue. That is a real trap (it happened once — a worked example in this exact spot broke its own guard), and a placeholder cannot fall into it.

$ osy docs <a rule id no page discusses on its own>
matched on lint rule <that id> (<TIER>): <what it catches> — no page documents it on its own, so the linter's
catalogue → local-checking-your-app

129 rules — 33 MUST, 75 SHOULD, 21 CONSIDER — grouped by what they judge.

Security · 32 rules

RuleTierCatches
security-auth-role-tests-nothingMUSTan armed auth role that no guard anywhere ever tests
security-authmethod-entity-row-filteredMUSTthe login's own entity granted only by a row filter the auth principal cannot satisfy
security-credential-mask-hides-it-from-the-loginMUSTan unconditional deny on the credential also hides it from the login that must read it
security-grant-edit-without-createMUSTa caller who may update or delete a role grant they could not have created
security-grant-write-unguardedMUSTa role-grant table whose writes are open, so a caller can hand themselves a role
security-hmac-compared-non-constant-timeMUSTan HMAC tag compared with ==, which leaks how much of a guess was right
security-jwt-issued-unverifiedMUSTa JWT issued before the credential was verified
security-login-enumerates-usersMUSTa login whose refusal reveals which addresses have an account
security-oauth-signup-no-existing-checkMUSTan OAuth sign-up that creates a user without checking for an existing one
security-partial-exposes-platform-credentialMUSTa partial security block on a platform entity that opens a credential column
security-password-compared-directlyMUSTa stored hash compared with == to a fresh hash, which can never be true
security-password-echoed-in-clearMUSTa password field rendered back as visible text
security-principal-credential-client-exposedMUSTa principal's credential field that can reach the browser
security-principal-login-field-not-uniqueMUSTthe field a login looks users up by is not unique
security-reset-token-never-deliveredMUSTa reset secret minted but never delivered, so nobody can finish the flow
security-sensitive-field-exposedMUSTa readable row carries a password hash, token or secret with no field-level deny
security-signup-cannot-create-the-principalMUSTa sign-up that runs as a principal with no create grant on the user entity
security-weak-randomMUSTa token or secret drawn from a seedable Random
security-anon-page-calls-gated-functionSHOULDan anonymous page that calls a function anonymous callers cannot reach
security-anon-page-reads-ungranted-entitySHOULDan anonymous page that reads an entity nobody anonymous may read
security-app-creates-what-its-block-deniesSHOULDa function that creates rows its entity's security block denies to every caller
security-authz-without-authnSHOULDa principal, roles and gated pages, but no login function at all
security-callback-url-widens-an-authorizeSHOULDa CallbackUrl minted for an event that declares Authorize, which the link then bypasses
security-classified-field-audited-unredactedSHOULDa classified field written to the audit trail unredacted
security-concurrency-check-without-its-trailSHOULDa concurrency check on an entity whose audit trail is off
security-entity-no-blockSHOULDan entity with no security block, which grants nobody anything — usually a forgotten grant
security-integration-role-granted-by-signup-orderSHOULDa privileged role handed to whoever signs up first, in an app that mints per-user API keys
security-login-reveals-which-credential-failedSHOULDa login that answers a wrong address and a wrong password differently
security-password-typed-in-clearSHOULDa password bound to a plain text field instead of a password field
security-secret-in-logSHOULDa credential field written to the log
security-signup-no-password-policySHOULDa sign-up that accepts any password at all
security-auth-trail-disabledCONSIDERthe authentication audit trail switched off

Authentication · 1 rule

RuleTierCatches
auth-bootstrap-without-app-authCONSIDERan AuthBootstrap with no app.Auth, so only its own methods can sign in

Data model · 14 rules

RuleTierCatches
data-category-derived-from-free-textMUSTa category list built from the rows' own free-text values, which fragments on the first typo
data-root-dialog-cannot-confirmMUSTa root dialog that never calls Dialog.Confirm, so nothing it edits can be saved
data-unique-swapped-within-one-commitMUSTtwo rows that swap a unique value inside one commit, which the index refuses
data-write-never-committedMUSTa write that is never committed
data-caught-write-fault-left-stagedSHOULDa caught write fault whose failed changes are left staged instead of discarded
data-detached-child-querySHOULDchildren fetched by a standalone query instead of the parent's collection
data-enum-member-no-labelSHOULDa multi-word enum member with no Label, shown as one run-together word
data-required-without-a-messageSHOULDa Required field with no message for the refusal
data-rule-without-messageSHOULDan invariant or pattern with no message, so a refusal shows the rule itself
data-unbounded-stringSHOULDa string with no MaxLength, so the caller decides how much you store
data-uniqueness-guarded-only-in-an-actionSHOULDuniqueness checked in an action instead of declared on the field
data-constraint-lets-null-throughCONSIDERa Unique or Pattern field that is not Required, so null satisfies it
data-row-compared-by-idCONSIDERrows compared by Id by hand where == already is row identity
data-unique-editable-uncheckedCONSIDERa Unique field edited in a form that catches no error on save

Correctness · 10 rules

RuleTierCatches
correctness-aggregate-over-pending-writesMUSTan aggregate over rows written but not yet committed, so the total is short
correctness-call-has-no-sql-formMUSTa call inside a query with no SQL form
correctness-nullable-tested-for-zeroMUSTa nullable tested for zero, which null passes
correctness-parse-that-answers-zeroMUSTa parse that answers zero on bad input instead of failing
correctness-egress-that-nothing-callsSHOULDan outbound call declared that nothing in the app ever invokes
correctness-external-value-replaced-by-a-literalSHOULDa missing external value replaced by a literal that looks like real data
correctness-freshness-stamp-with-no-sourceSHOULDa FetchedAt-style field in an app that has no egress to have fetched anything from
correctness-member-read-off-a-nullableSHOULDa member read off a nullable that may be null
correctness-null-substituted-for-a-valueSHOULDa null replaced in arithmetic by a value indistinguishable from a real one
correctness-pool-slot-credited-to-its-assigneeSHOULDa pool slot's own arm reading its Assignee, which is nothing unless somebody claimed — actor is who acted

Cost · 8 rules

RuleTierCatches
cost-child-read-without-includeSHOULDa child collection read with no Include on the parent query
cost-index-of-in-its-own-loopSHOULDan IndexOf inside the loop over the same list
cost-n-plus-oneSHOULDa child read inside a loop over parents, one query per parent
cost-page-reads-the-whole-tableSHOULDa page that reads the whole table
cost-paging-without-an-orderSHOULDa Skip with no OrderBy, so page 2 can repeat page 1
cost-the-same-query-twiceSHOULDthe same query run twice in one function
cost-unbounded-readSHOULDa query materialized with no Take
cost-clause-calls-the-server-per-elementCONSIDERa clause that calls the server once per element

Reliability · 3 rules

RuleTierCatches
reliability-http-result-uncheckedSHOULDan HTTP result used without checking IsSuccess
reliability-outbound-call-unguardedSHOULDan outbound call with no try, so a timeout kills the function or re-runs the workflow body
reliability-reaches-the-network-unguardedSHOULDa function or workflow body that reaches the network through a helper, with no try

Observability · 1 rule

RuleTierCatches
observability-catch-without-logSHOULDa catch that logs nothing

Workflows · 8 rules

RuleTierCatches
workflow-dead-end-stateMUSTa non-terminal state with no way out
workflow-ambient-clock-in-a-durable-bodySHOULDthe ambient clock read inside a durable body, which replays wrong
workflow-clock-without-routeSHOULDa deadline that passes with nothing routed to happen
workflow-initial-state-never-observedSHOULDan Initial state whose Start body always redirects, so no run is ever in it
workflow-no-success-terminalSHOULDa workflow whose every ending is a cancel or an error
workflow-slot-open-to-everyoneSHOULDa slot offered to everyone
workflow-unreachable-stateSHOULDa state nothing can ever enter
workflow-repeated-completion-conditionCONSIDERthe same completion condition repeated at the end of several route arms

UI · 37 rules

RuleTierCatches
ui-app-has-no-home-pageMUSTno page serving the app's front door
ui-toggle-handler-writes-it-againMUSTa toggle whose handler writes the value the toggle already wrote
ui-toggle-written-as-a-buttonMUSTa boolean written as a button instead of a toggle
ui-action-never-invokedSHOULDan action nothing in any render can invoke
ui-atom-where-the-kit-has-a-controlSHOULDa raw atom hand-built where the kit ships the control
ui-button-indistinguishable-from-a-text-fieldSHOULDa button styled so it reads as a text field
ui-component-reimplements-a-kit-controlSHOULDa component that hand-rolls what a bundled kit control already does
ui-control-call-without-an-accessible-nameSHOULDa control call that passes no accessible name
ui-currency-without-a-cultureSHOULDa currency formatted with no culture
ui-date-kept-as-a-stringSHOULDa date kept as a string and bound to a plain text box
ui-draft-field-ghosts-its-own-listSHOULDa draft row created at mount on a component that lists the same entity, so it appears in its own list
ui-enum-rendered-without-its-labelSHOULDan enum rendered by its member name instead of its label
ui-guard-and-action-disagree-about-the-listSHOULDa guard and its action reading two different lists
ui-inert-affordanceSHOULDa control that accepts the click and does nothing
ui-input-without-an-accessible-nameSHOULDan input with no accessible name
ui-key-read-with-no-key-surfaceSHOULDKeyboard.Down asked about a key no element declares, so it is false forever
ui-label-drawn-twiceSHOULDa label drawn twice for one control
ui-nondeterministic-render-slotSHOULDa render-slot value whose behaviour depends on what else its expression reads
ui-page-root-flush-against-the-viewportSHOULDa page root that paints a surface and sits welded to the viewport edge
ui-page-server-read-with-no-skeletonSHOULDa page that reads from the server with nothing shown while it waits
ui-row-guard-reads-the-unfiltered-listSHOULDa per-row guard that reads the unfiltered list
ui-spacing-step-looks-like-pixelsSHOULDa spacing argument that reads as pixels but is steps on the 0.25rem scale
ui-state-nothing-readsSHOULDa state field nothing reads
ui-text-field-bound-to-a-numberSHOULDa text field bound to a number
ui-theme-primary-collides-with-a-toneSHOULDa theme's Primary is too close to a semantic tone the app also paints, so a destructive action looks ordinary
ui-theme-token-shadows-nothingSHOULDa theme token named to shadow a kit token that does not exist
ui-control-state-not-announcedCONSIDERa control whose state a screen reader is never told
ui-control-without-an-accessible-nameCONSIDERa control with no accessible name
ui-data-read-declared-inside-renderCONSIDERa data read declared inside a render instead of as a field
ui-date-rendered-without-a-formatCONSIDERa date rendered with no format
ui-editable-field-no-error-surfaceCONSIDERa field under a rule the browser cannot pre-empt, in a form that catches nothing
ui-editable-field-write-policy-unreflectedCONSIDERan editable field whose write is gated by a declared policy the input does not reflect
ui-mount-hook-is-a-fetchCONSIDERan on-mount hook that only loads data a field could declare
ui-page-no-titleCONSIDERa routed page with neither a Title nor a meta title
ui-rank-rendered-as-a-positionCONSIDERa stored rank drawn as the reader's position in a loop over a filtered list, so it reads 1, 3
ui-raw-style-literal-repeatedCONSIDERthe same raw style literal repeated where a token belongs
ui-rendered-list-query-not-liveCONSIDERa rendered list bound to a query that is not live

Formatting · 3 rules

RuleTierCatches
format-throws-on-doubleMUSTan integer-only format on a Double, which throws at runtime
format-double-custom-rounds-differentlyCONSIDERa custom pattern on a Double that rounds differently on the client
format-runs-on-the-serverCONSIDERa format the browser cannot reproduce, so it round-trips to the server

Client · 1 rule

RuleTierCatches
client-server-hop-in-class-methodCONSIDERa class method that quietly makes a server round trip

Testing · 11 rules

RuleTierCatches
testing-login-untestedMUSTa login no test proves both ways
testing-scope-names-text-not-a-containerMUSTa test scope that names text rather than a container
testing-app-has-no-testsSHOULDan app that ships no tests at all
testing-denial-provable-by-its-setupSHOULDa denial the setup alone would prove, with nothing there to deny
testing-gated-read-outside-runasSHOULDa gated read asserted outside a runas, so a denial passes as empty
testing-invariant-untestedSHOULDan invariant no Assert.Throws proves bites
testing-page-never-drivenSHOULDa routed page no test ever visits
testing-security-rule-untestedSHOULDa rule that can deny, and no test acting as a principal proves it does
testing-test-without-assertionSHOULDa test that asserts nothing
testing-visible-on-a-numberSHOULDan Assert.Visible on a bare number, a contains-check the whole page can satisfy
testing-write-denial-unprovenSHOULDa denied write with nothing anywhere proving the same write can succeed for anybody

What does it find in the sample apps?#

osy lint run over every sample app in the repository when this page was built — the same call a downloader makes, on the same source. A rule set is only as credible as what it says about the apps its authors ship, so the result is published whether or not it is clean.

36 apps, 302 files: 1 MUST, 599 SHOULD and 117 CONSIDER findings.

AppFilesMUSTSHOULDCONSIDERDistinct rules
agent-expenses18046910
arcade280512211
auth-demo60000
chart-demo80545
chat-demo601058
chat-room601629
concurrency901339
dialog-demo601718
docs-site9013210
dropdown-demo509310
ember15038813
entity-inheritance903509
file-manager11015512
generic-grid5011910
gestures20123
gridprobe30413
hello-osy40212
kanban5018413
markdown-demo60000
media-demo40415
memory-lab701316
motion30101
shell-arrangements711114
shell-showcase1001501
shop601215
tabbed_admin_paused180402017
template-stretch50969
todo30000
wf-approvals902419
wf-expense-hitl902007
wf-fanout-quorum1102707
wf-nightly-digest701205
wf-order-saga903107
wf-signup-invite11020112
wf-supplier-dispatch602106
wf-support-sla16035413

Examples#

osy lint                  # what this app is missing
osy lint --strict         # fail the run on any MUST-tier finding
osy lint --json           # findings as JSON
osy lint --rules          # every rule: id, tier, what it catches

See also#

Understanding your app — the resolved model: what the app is.

Explaining your app's security — who can do what, in plain English; --with-findings folds these findings into it.

secure by default (deny-all) — the deny-all default the security rules are written against.

security { } — how to declare an entity's access rules.

Related

Understanding your app

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

Explaining your app's security

Explains your app's declared authorization in plain English — who can read, create, update and delete each entity…

Compiling your app

Compiles your app's source into the app on the local platform — the inner-loop compile-and-apply. It applies additive…

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…

auth bootstrap (login, before anyone is signed in)

Under deny-all, login faces a paradox: it must read a user row *before* anyone is authenticated. `app.AuthBootstrap`…

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…

invariant

A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is…

runas

Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that…

Sum / Average / Min / Max / Count

Fold rows down to a single number — a query or a `List<T>` you already hold. The one thing to know before you use them:…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…

Log.*

Write a line to your app's structured log. Both a plain template (`"Order {OrderCode} shipped", order.Code`) and an…