Guardrails
The rules Magik refuses to boot without. The spec says this is the product; this page is the list, with what fails, when it fails, and the error the failure raises.
Status: spec only — no guardrail is implemented, no error code is registered, and magik check does not exist. Every code below is a planned name reserved by design. Reviewed 2026-08-26.
Why boot
A guardrail is worth building only if it catches the mistake before the mistake reaches a user. Three enforcement stages, in order of how early they catch:
| Stage | Runs | Catches | Cost |
|---|---|---|---|
| boot | every magik server, magik console, magik worker, magik test |
anything derivable from the frozen declaration registry | the app does not start |
magik check |
on demand, in an editor loop, in CI | the same rules plus static analysis that needs no connection, plus --scale heuristics over query sites |
exit 1 |
| CI | bin/check on every push |
boot + check + tests + lint + docs | the build is red |
The rule: if it can fail at boot, it fails at boot. magik check exists so an agent can run the same rules in a tight loop without starting a server (07-ai-first.md), not so a rule can be downgraded to a warning.
The one exception is --scale, which is a warning by design: an unscoped query is sometimes correct (a platform-wide admin report), so it reports rather than refuses.
The catalogue
Every guardrail named in 00-build-spec.md, plus the ones its architecture decisions imply. Format and field contract: ../architecture/03-error-codes.md.
From the spec's guardrail list
| Guardrail | What fails | When | Code | fix: |
|---|---|---|---|---|
| Ledger entries must balance | an entry block whose debit lines do not sum to its credit lines |
boot | MAGIK_LEDGER_UNBALANCED |
magik errors explain MAGIK_LEDGER_UNBALANCED |
| No raw card numbers | a field :card_number — or any field matching the PAN name set — declared on any model |
boot | MAGIK_MODEL_FORBIDDEN_FIELD |
magik generate model <Name> --tokenized-payment |
| Domain boundaries | a domain referencing another domain's model constant without going through exposes or an event |
boot | MAGIK_DOMAIN_BOUNDARY |
magik check --domains |
| Stateless screens | a screen or action assigning an instance variable that outlives the request, or a mutable constant written after boot |
boot | MAGIK_RENDER_SCREEN_STATEFUL · MAGIK_ACTION_STATEFUL |
magik errors explain MAGIK_ACTION_STATEFUL |
| Timestamps need a zone | a timestamp rendered in a component with no zone: argument |
boot | MAGIK_RENDER_TIMESTAMP_NO_ZONE |
magik check --render |
| Tenant scoping at scale | a query site with no tenant_id in its WHERE clause |
magik check --scale (warning) |
MAGIK_SCALE_UNSCOPED_QUERY |
magik check --scale --json |
Authorization, layout and uploads
The three the spec adds beside the list above. Each is derivable from the frozen registry, consequential, unambiguous and fixable — the four bars at the bottom of this page.
| Guardrail | What fails | When | Code | fix: |
|---|---|---|---|---|
| Every surface reaching a model names a policy verb | a screen, action, api resource, channel, job, flow, webhook :incoming or admin_panel with no policy: and no explicit policy: :public / policy: :system |
boot | MAGIK_POLICY_UNDECLARED |
magik describe policy --json |
| A policy predicate performs no I/O | a can block issuing a query — live re-evaluates one per subscriber per change, so a query here is one round trip per row per open socket |
boot | MAGIK_POLICY_IO |
magik errors explain MAGIK_POLICY_IO |
| A named verb exists | policy: %i[Invoice publish] where policy :Invoice declares no :publish |
boot | MAGIK_POLICY_UNKNOWN_VERB |
magik registry --kind policy --json |
| Denial is the default | a policy block with no default :deny |
boot | MAGIK_POLICY_NO_DEFAULT |
magik errors explain MAGIK_POLICY_NO_DEFAULT |
A rule receiving a nil record denies |
a row-level rule that would pass on an absent record | boot (static) | MAGIK_POLICY_NULL_PASSES |
magik errors explain MAGIK_POLICY_NULL_PASSES |
| Every screen has a layout | a screen with no layout: and no layout: :None |
boot | MAGIK_LAYOUT_MISSING |
magik generate layout App |
| Navigation cannot rot | a nav_item naming a screen that does not exist |
boot | MAGIK_LAYOUT_UNKNOWN_SCREEN |
magik registry --kind screen --json |
| An incoming webhook names the verbs it may exercise | a webhook :incoming with no acts_as :service, can: [...], or one naming policy: :system. A verified signature authenticates an origin, not a person, and :system would grant every verb in the app to the one surface a stranger can call directly |
boot | MAGIK_WEBHOOK_UNSCOPED_ACTOR |
magik describe webhook --json |
| A policy subject is a declared construct | policy :Receivables where nothing declares Receivables. The subject may be any construct that owns data — a model, a ledger, a flow — but it must exist |
boot | MAGIK_POLICY_UNKNOWN_SUBJECT |
magik registry --json |
| Navigation cannot rot, actions included | a layout declaration naming an action that does not exist — search action: :global_search in a topbar is the same rot as a nav_item pointing at a deleted screen, and was missed because only screens were guarded |
boot | MAGIK_LAYOUT_UNKNOWN_ACTION |
magik registry --kind action --json |
| Uploads are bounded | a :file field or an attachment with no max_size and no content_types — an unbounded upload field is an unbounded storage bill and a trivial DoS |
boot | MAGIK_MODEL_UNCONSTRAINED_UPLOAD |
magik describe model.attachment --json |
MAGIK_POLICY_UNDECLARED is the load-bearing one. It makes authorization non-optional the way
tenant_id is non-optional, which is the only mechanism that survives an agent in a hurry. An
admin_panel with no policy: is not a separate rule — it is this one, and the catalogue
deliberately carries one code rather than two, because the admin is a surface like any other and the
guardrail that protects it is the guardrail that protects all of them.
Implied by the non-negotiable architecture decisions
| Guardrail | What fails | When | Code |
|---|---|---|---|
| Money is never a float | a :money field given a float default, or a float assigned at runtime |
boot · runtime | MAGIK_MODEL_FLOAT_MONEY |
| Ledger is append-only | an update/delete reaching a posted ledger entry |
runtime | MAGIK_LEDGER_ENTRY_MUTATED |
| Immutability holds | a write to a field covered by immutable_after: past its point |
runtime | MAGIK_MODEL_IMMUTABLE_VIOLATION |
| Money mutations dedupe | an action that posts a ledger entry and declares no idempotent_by: |
boot | MAGIK_ACTION_IDEMPOTENCY_REQUIRED |
| Every model is tenant-scoped | a model that opts out of tenant_id with no declared reason |
boot | MAGIK_MODEL_NO_TENANT |
| Routes are unique | two actions or screens compiling to the same path | boot | MAGIK_ROUTER_PATH_CONFLICT |
| Inbound webhooks are verified | a webhook :incoming with no verify_signature |
boot | MAGIK_WEBHOOK_UNVERIFIED |
| Migrations are reversible | a migrate with no down block |
boot | MAGIK_SCHEMA_IRREVERSIBLE |
| Schema matches declarations | a model field with no applied migration behind it | boot | MAGIK_SCHEMA_DRIFT |
| Backends are known and proven | use :cache, :something_unshipped |
boot | MAGIK_CONFIG_UNKNOWN_BACKEND |
| No offline | pwa do offline … end, or any request for a client-side data cache |
boot | MAGIK_PWA_OFFLINE_UNSUPPORTED |
| Component contracts hold | a component replacing a kit one (satisfies Magik::Kit::X) missing a required prop, slot, htmx target or event (08-component-overrides.md) |
boot | MAGIK_COMPONENT_CONTRACT_VIOLATION |
| Locale completeness | a t() key missing from a shipped locale |
magik check |
MAGIK_I18N_MISSING_KEY |
| App is defined once | a second App.define in one process |
boot | MAGIK_BOOT_REDEFINED |
What a failure looks like
Boot failure output is the same object rendered three ways — terminal, --json, and the dev error page (../architecture/03-error-codes.md). The intended terminal shape:
MAGIK_LEDGER_UNBALANCED app/ledgers/payments.rb:14
entry :refund debits 1 line (refunds) and credits 2 lines (cash, fees);
debits total :amount, credits total :amount + :fee_amount
fix: magik errors explain MAGIK_LEDGER_UNBALANCED
Boot aborted. 1 guardrail failed, 23 passed.
| Field | Rule |
|---|---|
| code | stable forever once shipped. Agents and log pipelines pattern-match on it. |
| location | the declaration site — file:line of the offending block, never a framework frame. |
| cause | the specific fact, with the real identifiers. Not a restatement of the code. |
fix: |
an executable command wherever one exists. Advice only where no command can help. |
Adding a guardrail
The bar, so the list stays worth reading:
| Test | Question |
|---|---|
| Derivable | can the rule be decided from the frozen registry or from source, with no network and no guesswork? |
| Consequential | does violating it cause a production incident, a security hole, or a migration nobody survives? |
| Unambiguous | is there a legitimate app that breaks the rule on purpose? If yes, it is a magik check warning, not a boot failure. |
| Fixable | is there a command, a generator flag, or a one-line edit that resolves it? |
A rule that fails the last test ships as a warning with a documented rationale. A rule that fails the third one does not ship at all.
Implementation contract — where guardrail code lives, and why the boot runner is in core while the static rule set is in check: ../architecture/02-boundaries.md.