---
title: "Check codes"
description: "Every MAIN- code the specs define so far, grouped by area, with where you meet it, what it means and how to fix it."
section: "reference"
status: "Planned for 0.2"
spec: "errors"
canonical: "https://mainsdk.dev/docs/reference/check-codes"
updated: "2026-10-28"
---

# Check codes

> Every MAIN- code the specs define so far, grouped by area, with where you meet it, what it means and how to fix it.

Status: Planned for 0.2 · Canonical: https://mainsdk.dev/docs/reference/check-codes · Updated: 2026-10-28

Every finding from `mainsdk check` and every error the SDK raises carries a stable code: `MAIN-<AREA>-<NNNN>`. A code is assigned once and never reused or changed in meaning. Each one has a page at `https://mainsdk.dev/errors/<code>`, which is also the `type` of its [problem details](https://mainsdk.dev/docs/reference/errors).

Look any code up offline:

```text
$ npx mainsdk explain MAIN-DATA-0007
MAIN-DATA-0007  Record belongs to another user  (403)
cause  A query read a record owned by a different user.
fix    Scope the query with db.orders.for(user).
```

The **Raised by** column says where you meet the code: `check` (a gate finding before merge), `runtime` (an error or log from the SDK in your running app), or `CLI` (a command that refuses to run). A code not listed here is not defined yet. A family written `MAIN-<AREA>-*` is a group of Main 27 rules whose individual numbers arrive with the baseline pack.

## CHECK, CONFIG and CONTRACT

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-CHECK-0026` | check | An interface resolves to a local driver in production | Set a real driver in `main.env.prod.yaml` |
| `MAIN-CONFIG-0001` | check, runtime | Unsupported `schema` value in `main.yaml` | Use `schema: 1`; the error lists supported versions |
| `MAIN-CONFIG-0002` | check, runtime | `main.yaml` can't be read: bad indent, a tab, an anchor or tag, or a duplicate key; the error names the line | Fix the YAML at that line; main.yaml uses plain maps, lists and scalars |
| `MAIN-CONFIG-0003` | check, runtime | A value has the wrong format: money (`"$0.05"`), a rate (`$5/day`), a size (`25MB`) or a duration (`1h`) | Write the value in the format the error shows |
| `MAIN-CONFIG-0005` | check, runtime | No `main.yaml` in the project folder | Run `npx mainsdk init` |
| `MAIN-CONFIG-0004` | check, runtime | A literal value in a field marked secret | Use a `secret:` reference, for example `secret:RESEND_API_KEY` |
| `MAIN-CONFIG-0010` | check, runtime | A layer weakens its parent, for example adds a host `main.org.yaml` doesn't allow | Remove the change, or add an exception with rule, reason, owner and expiry |
| `MAIN-CONFIG-0020` | runtime | A config value fails its schema; the last good value is kept | Fix the value in your config store |
| `MAIN-CONFIG-0021` | runtime | A pushed `main.` value is looser than `main.yaml` allows, or a limit's period differs; the value is refused and the last good one kept | Push a value equal to or tighter than `main.yaml`, or change `main.yaml` in a reviewed deploy |
| `MAIN-CONFIG-0022` | runtime | A pushed key starts with `main.` but names a path that can't be pushed, such as `main.payments.driver`; the key is refused | Change that setting in `main.yaml` in a reviewed deploy |
| `MAIN-CONFIG-0023` | runtime | The config store can't be read (down, or a load timed out); the app serves its last good values, logged once per outage | Restore the store; the app needs no restart |
| `MAIN-CONTRACT-0003` | CLI | No driver version satisfies every interface range during `sync` | Widen a range or pick a driver that implements it; the error names both ranges |
| `MAIN-CONTRACT-0021` | CLI | A contract has no threat model (`mainsdk contract check`) | Add the interface's STRIDE threat model |
| `MAIN-CONTRACT-0030` | CLI | The vendored getqr.id `openapi.yaml` doesn't match the sha256 in its `vendor.yaml`, or the generated TypeSpec import is stale (ADR-0057) | Restore the vendored file and take a new version only through `contract diff --vendor getqr`; rebuild with `contracts:build` |
| `MAIN-CONTRACT-0031` | CLI | `contract diff --vendor getqr` found an upstream change that breaks an operation or field MainSDK uses; the finding names both | Keep the pin until the call is updated in the same pull request, or ask the GetQR ID team |
| `MAIN-CORE-0001` | runtime | Native core ABI version doesn't match the binding | Reinstall so the core and binding versions match; the error names both |
| `MAIN-CORE-0500` | runtime | Unknown error in a handler; no internal detail in production | Read the trace with `mainsdk explain <trace_id>` |
| `MAIN-PAGE-0001` | runtime | A list cursor is malformed, older than 24 hours, or made by a different query, tenant or user (400) | Start again without a cursor, or pass back the `next` value of the same query |
| `MAIN-PAGE-0002` | runtime | `limit` is not a whole number of at least 1 (400) | Pass a whole number from 1 up to the call's maximum |

## SECRET

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SECRET-0001` | check (build) | `mainsdk/secrets` imported from client code | Read the secret in server code only |
| `MAIN-SECRET-0002` | check | A key pattern in source code | Move the key to `secrets` and use `secret:NAME` |
| `MAIN-SECRET-0003` | check | A committed `.env` file other than `.env.example` | Remove it from git and use the secrets driver |
| `MAIN-SECRET-0004` | check (build output) | A value from a `secret:` reference is in a browser bundle | Keep the call on the server; the finding names the chunk |
| `MAIN-SECRET-0007` | runtime | Secrets provider unreachable; serving the last good value from memory | Restore the provider; nothing is written to disk |
| `MAIN-SECRET-0005` | runtime | A `secret:` reference names a secret that isn't set | Set it in your secrets manager, or in your shell for local development |

## FLAGS

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-FLAGS-0001` | runtime | A flag was evaluated with no definition in config; the fallback was served, logged once per name | Define `flags.<name>` in your config store, or pass the fallback you meant |
| `MAIN-FLAGS-0002` | runtime | A pushed flag definition is invalid; it was refused and the last good definition kept | Fix the definition: `enabled` as a boolean, `rollout` from 0 to 100 with at most two decimals, one of `user`, `in` or `notIn` per rule |
| `MAIN-FLAGS-0003` | runtime | A rule served a value of a different type from the fallback, or `enabled` got a non-boolean; the fallback was served, logged once per name | Serve a value of the same type as the fallback |

## AUTH and SESSION

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-AUTH-0001` | runtime | Not signed in (401 from `auth.require()`) | Sign in; the problem body carries a `fix` |
| `MAIN-AUTH-0006` | runtime | WhatsApp code reused, expired or tried too often; the challenge is closed | Start a new sign-in |
| `MAIN-AUTH-0007` | runtime | OIDC SSO callback with a mismatched `state` or `nonce` | Restart sign-in from your app |
| `MAIN-AUTH-0008` | runtime | SAML assertion unsigned, for the wrong audience, expired or replayed | Check the IdP metadata and clock for the organisation |
| `MAIN-AUTH-0040` | runtime | A sign-in challenge was verified a second time; a challenge works once | Start a new challenge with `auth.challenge.start` for each sign-in |
| `MAIN-AUTH-0041` | runtime | Invite used, expired, revoked, or accepted by a different email (410); a mismatch leaves the invite valid | Ask for a new invite, or sign in with the invited email |
| `MAIN-AUTH-0042` | runtime | Domain not verified, already verified by another tenant, or a public email domain | Publish the TXT record the fix shows, or use a domain you own |
| `MAIN-AUTH-0043` | runtime | The change would leave the tenant with no owner (409) | Make another member an owner first |
| `MAIN-AUTH-0044` | runtime | The tenant requires its SSO; this session came from another method (403) | Sign in through the organisation's SSO |
| `MAIN-AUTH-0045` | runtime | Invites, teams and domains are not available in a personal tenant (409) | Create an organisation with `auth.orgs.create` |
| `MAIN-AUTH-0046` | runtime | Sign-in link used, expired, unknown, or opened in a browser other than the one that asked (401; the challenge is closed) | Ask for a new link with `links.signIn()` from the same browser |
| `MAIN-AUTH-*`, `MAIN-SESSION-*` | check | Main 27 sign-in and session rules: home-made password storage, implicit flow, long-lived tokens, tokens in local storage | Use `auth` and its session cookies |

## TENANT

Planned for 0.3 (spec `tenancy`).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-TENANT-0001` | runtime | A tenant-scoped call ran with no tenant in context (500) | Run jobs and scripts inside `tenant.run(id, fn)`; requests get the tenant from the adapter |
| `MAIN-TENANT-0002` | runtime | The tenant doesn't exist, or the caller isn't a member or key holder (404, same body either way) | Switch to a tenant you belong to |
| `MAIN-TENANT-0003` | runtime | The tenant is suspended; tenant-scoped calls fail closed (403) | Resume the tenant, or list the call under `tenancy.suspended.allow` |
| `MAIN-TENANT-0004` | runtime | `tenant.system()` called from a file outside `tenancy.system.allow` | Move the admin code under a listed path |
| `MAIN-TENANT-0005` | runtime | A record, file, team or reference of another tenant was used; treated as not found (404) | Read it inside its own tenant |
| `MAIN-TENANT-0006` | runtime | The tenant's region store or database is not configured or not reachable; nothing is written elsewhere (503) | Configure the store for the tenant's region, or its entry in `tenancy.databases` |
| `MAIN-TENANT-0007` | runtime | Erasure refused: the tenant is active, under a legal hold, or still in its waiting period (409) | Suspend it first, lift the hold, or wait for `tenancy.erase.wait` |
| `MAIN-TENANT-0010` | check | A raw query on a tenant-scoped table has no `tenant_id` condition | Use `db.<model>` or add the `tenant_id` condition |
| `MAIN-TENANT-0011` | check | `tenant.system()` used in a file outside `tenancy.system.allow` | Move the code or list the path |
| `MAIN-TENANT-0012` | check, runtime | A per-tenant config or limit override loosens the app value | Only tighten per tenant |
| `MAIN-TENANT-0013` | check | Shared isolation on a driver with row-level security, but the migrations don't enable it | Regenerate the migrations with MainSDK |

## APIKEY

Planned for 0.3 (spec `apikeys`).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-APIKEY-0001` | runtime | No key, or a malformed one: wrong prefix, wrong length, bad checksum, or a key sent in the query string (401). Decided offline | Send the key as `Authorization: Bearer <key>` or in the `apikeys.header` header |
| `MAIN-APIKEY-0002` | runtime | Unknown, revoked or expired key (401). The body is the same for all three; the log says which | Create or rotate a key |
| `MAIN-APIKEY-0003` | runtime | The key's scopes don't include the permission this action needs (403) | Create a key with that scope, or call an action the key may use |
| `MAIN-APIKEY-0004` | runtime | Invalid `create` or `rotate` input: a scope not under `apikeys.scopes`, an expiry in the past or past `max-ttl`, an overlap over 30 days, or an empty name (400) | Fix the input the error names |
| `MAIN-APIKEY-0005` | runtime | The caller may not issue this key: no `apikeys.manage` permission, a scope the caller doesn't hold, or a tenant other than the current one (403) | Ask for the permission, or issue only scopes you hold in the current tenant |
| `MAIN-APIKEY-0006` | runtime | The key's rate limit is reached (429, with `Retry-After`) | Wait for `Retry-After`, or raise the rate with a new key |
| `MAIN-APIKEY-0007` | runtime | The key's environment doesn't match: a `test` key in `env: prod`, or a `live` key anywhere else (401). Decided offline | Use a key made for this environment |
| `MAIN-APIKEY-0008` | runtime | The key store can't be reached. Keys verified in the last 15 minutes are served from memory and this is logged once; other keys get 503 (retryable) | Restore the store; retry the call |
| `MAIN-APIKEY-0009` | runtime | `rotate` on a revoked or expired key (409) | Create a new key instead |
| `MAIN-APIKEY-0010` | check | A key in this app's format, with a valid checksum, is committed to source | Remove it, revoke the key and issue a new one |
| `MAIN-APIKEY-0011` | check | A scope under `apikeys.scopes` that no `access` permission defines | Define the permission in `access`, or remove the scope |

## PLAN

Planned for 0.3 (spec `entitlements`).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-PLAN-0001` | runtime | The tenant's plan doesn't include this feature (403, with an `upgrade` hint naming the plans that do) | Upgrade the plan, or grant the feature with `entitlements.grant` |
| `MAIN-PLAN-0002` | runtime | The tenant has reached this limit of its plan (403, with an `upgrade` hint) | Upgrade the plan, or raise the limit with `entitlements.grant` |
| `MAIN-PLAN-0003` | runtime | An override would remove a feature or lower a limit; overrides may only add (400) | Change the plan in `main.yaml` instead |
| `MAIN-PLAN-0004` | runtime | The plan store can't be reached; the last known plan is served, or the default plan for a tenant with none. Logged once per outage | Restore the store; nothing else to do |
| `MAIN-PLAN-0005` | runtime | A subscription event names a price under no plan, or carries no tenant; it is ignored and the plan is unchanged | Add the price to a plan under `plans.<name>.prices`, or tag the subscription with its tenant |
| `MAIN-PLAN-0006` | check, runtime | `plans` or `entitlements` is invalid: the default plan isn't defined, a name is both a feature and a limit, a limit has a bad value, a windowed usage source has a limit with no window, or a price is under two plans. The app doesn't start | Fix the key the error names |
| `MAIN-PLAN-0007` | runtime | A plan change for another tenant outside `tenant.system()`, or by a caller without the `entitlements.manage` permission (403) | Make the change inside `tenant.system()` in admin code, or grant the permission |
| `MAIN-PLAN-0008` | runtime | A feature or limit name that no plan declares. `has` returns false and logs once; `limit` and `within` throw (500) | Declare the name under `plans` |
| `MAIN-PLAN-0009` | runtime | The tenant has reached its plan's request rate (429, with `Retry-After`) | Wait for `Retry-After`, or move to a plan with a higher `requests` limit |
| `MAIN-PLAN-0010` | check | A feature or limit name used in code that no plan declares | Declare the name under `plans`, or fix the typo |

## ACCESS and POLICY

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-ACCESS-0001` | runtime | Access denied (403) | Check the role or relation the action needs |
| `MAIN-ACCESS-0004` | check | Relations need a ReBAC driver (openfga, spicedb, permit) or Cedar entities | Switch `access.driver` or model the relation in Cedar |
| `MAIN-ACCESS-0007` | runtime | Remote engine unreachable and no cached decision; denied (fail closed) | Restore the engine; tune `access.unreachable.cache` |
| `MAIN-ACCESS-0011` | check | OpenFGA 1.8.0 to 1.13.1 pinned (CVE-2026-34972, wrongly permissive BatchCheck) | Upgrade OpenFGA past 1.13.1 |
| `MAIN-ACCESS-0020` | runtime | An attribute source timed out or is down, so `access` denied the request | Wait for the source or fix its driver; never turn the gate off |
| `MAIN-POLICY-0004` | runtime (load) | A repo policy weakens an org policy; loading fails, naming both files | Remove the repo policy or change it at the org layer |
| `MAIN-POLICY-0009` | runtime | A new policy set failed to load; the previous version stays in force | Fix the syntax; run `mainsdk test policy` |
| `MAIN-POLICY-0011` | runtime (load) | A Cedar policy uses a construct the built-in TypeScript engine doesn't support yet; it names the construct and line and never ignores it | Rewrite the policy without the construct, or use the OPA driver |

## DATA, KEYS and PRIV

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-DATA-0001` | check | Raw database client used outside MainSDK | Wrap it: `withPrisma(client)` or `withDrizzle(db)` |
| `MAIN-DATA-0007` | runtime | Record belongs to another user (403) | Scope the query with `db.<model>.for(user)` |
| `MAIN-DATA-0013` | check | A query joins tables across regions | Query each region separately; combine only totals |
| `MAIN-DATA-0020` | check | A field labelled `pan`: card numbers are never stored | Use hosted checkout through `pay`; drop the field |
| `MAIN-DATA-0021` | check | A field that holds personal data has no label | Add a label: `personal`, `sensitive`, `child` or `phi` |
| `MAIN-DATA-0030` | check | SQLite local driver in production | Set `db.driver` to `postgres`, `neon` or `supabase` for prod |
| `MAIN-DATA-0008` | runtime | The record isn't there, with tenancy off (404) | Check the id and the model |
| `MAIN-DATA-0009` | runtime | A create repeats an id or a declared unique value (409) | Leave `id` out, or change the unique value |
| `MAIN-DATA-0031` | runtime | The local SQLite driver can't start: this Node has no `node:sqlite` (500) | Use Node 22.18 or later, or the postgres driver |
| `MAIN-DATA-0032` | runtime | `db.driver` is not built, or a Postgres driver has no client (400) | Use sqlite, postgres, neon or supabase, and pass the `postgres` option |
| `MAIN-DATA-0050` | runtime | A query ran past its statement timeout and was cancelled (503, not retried) | Add an index or narrow the query; raise `db.timeout.request` up to 30s, or use a job |
| `MAIN-DATA-0052` | runtime | No connection was free within `db.pool.acquire` (503 with `Retry-After: 1`) | Retry; raise `db.pool.max` or shorten transactions |
| `MAIN-DATA-0053` | runtime | A query took longer than `db.slow` (warn log, once a minute per query shape) | Add an index or narrow the query |
| `MAIN-DATA-0054` | runtime | One query shape ran more than `db.repeat` times in one request or job (warn log) | Load them with one `where: { id: { in: ids } }` query |
| `MAIN-DATA-0060` | runtime | `ifVersion` doesn't match the record's version (409; 412 from `If-Match`) | Read the record again and retry |
| `MAIN-DATA-0063` | runtime | `restore` of a record not in trash, or past its trash period (404) | Restore within the trash period |
| `MAIN-DATA-0066` | runtime | A write to a versioned record came with no `If-Match` (428) | Send the ETag you read, or `If-Match: *` |
| `MAIN-DATA-*`, `MAIN-API-*` | check | Main 27 database and API rules: SQL built from strings, shared admin credentials, unvalidated input, open CORS | Use typed queries through `db`; follow the fix in each finding |
| `MAIN-KV-0001` | runtime | `incr` was given a step that is not a whole number (400), or the key holds a value that is not a whole number (409) | Pass an integer `by`; use `incr` only on keys it created |
| `MAIN-KV-0002` | runtime | `ttl` is not a positive duration (400) | Pass milliseconds or a duration such as `30s` |
| `MAIN-KV-0003` | runtime | A key is empty or longer than 512 characters (400) | Use a shorter, non-empty key |
| `MAIN-KEYS-0001` | check | Local keys driver in production | Set `keys.driver` to a KMS driver for prod |
| `MAIN-KEYS-0004` | runtime | Data can't be decrypted because its owner's key was destroyed (erasure) | Expected after `privacy.erase` with `keys.per-user: true` |

## NET

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-NET-0001` | check | Raw socket, another HTTP client or direct `fetch` outside the SDK: `new WebSocket`, `EventSource`, `node:http`, `node:https`, `node:dgram`, `ws`, or `git`, `curl` or `wget` run as a child process | Call through `http.*` or a driver; for a program, declare it under `permissions.processes` and run it with `system.run` |
| `MAIN-NET-0002` | check, runtime | Call to a host not in `network.allow` (or not declared by a driver) | Add the host to `network.allow`, or remove the call |
| `MAIN-NET-0003` | runtime | Redirect to an undeclared host refused | Declare the target host or call it directly |
| `MAIN-NET-0004` | check | A third-party API offers less than TLS 1.2 (TLS 1.3 is the minimum for endpoints your app serves, under `MAIN-NET-*`) | Ask the provider to upgrade; meanwhile an exception with an expiry |
| `MAIN-NET-0005` | runtime | An entry in `network.allow` is not an IP, a CIDR range, an exact host or a `*.domain` (for example a prefix length out of range) | Fix the entry; the error names it |
| `MAIN-NET-0006` | runtime | A request was redirected more times than the SDK follows, or `redirect: "error"` met a redirect | Call the final URL directly, or fix the redirect loop |
| `MAIN-NET-0007` | check | The code connects to a host that `network.allow` doesn't list; the finding names the call (`http`, `websocket`, `event-stream`, `database`, `git`) | Add the host to `network.allow`, or remove the call; `npx mainsdk discover` prints the lines to add |
| `MAIN-NET-0008` | runtime | `http.json` or `http.events` got a non-2xx answer (429 when the provider rate-limits, else 502), or JSON that didn't parse; the body is never echoed | Check the URL, the credentials and the service status, or read the response with `http.fetch` |
| `MAIN-NET-0009` | runtime | An outbound call timed out: the connect or total timeout of its last attempt, or its deadline, passed (504; the error names which) | Raise the timeout for that host or call, or check the provider's latency |
| `MAIN-NET-0010` | runtime | The circuit breaker for the host is open, or half-open with its probe in flight; no request was sent (503, `retryable`, `retryAfter` is the rest of the cooldown) | Wait for `retryAfter`, and check the provider's status; breakers are per app instance |
| `MAIN-NET-0011` | runtime | An outbound call failed with a network error (refused, reset, DNS failure) on its last attempt (502; states the number of attempts) | Check the host, DNS and the provider's status |
| `MAIN-NET-0012` | runtime | A value under `network.http` is invalid: a bad duration, `attempts` outside 1 to 10, `failures` outside 1 to 100 %, `min` below 1, or an unknown key (fails at load; names the key) | Fix the value the error names |
| `MAIN-NET-*` | check | Main 27 network rules: plain HTTP, TLS below 1.3 on public endpoints | Serve over TLS 1.3 |

## PERM

Permission codes cover the server's own disk, operating system facts and programs (spec `app-permissions`). They appear only for what `permissions` in main.yaml declares; see [Declare what your app uses](https://mainsdk.dev/docs/guides/declare-what-your-app-uses).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-PERM-0001` | check | Raw `node:fs`, `node:os` or `node:child_process` used outside a driver | Use `disk` for files and `system` for facts and programs |
| `MAIN-PERM-0002` | runtime | In `enforce` mode, a `disk` path, a `system` fact or a program is used but not declared under `permissions` (403) | Declare it under `permissions` in main.yaml, or remove the code that asks |
| `MAIN-PERM-0003` | check | A `disk` path, a `system` fact or a program found in the code isn't declared under `permissions` | Declare it; `npx mainsdk discover` prints the lines to add |
| `MAIN-PERM-0004` | runtime | An unknown name under `permissions.system` | Use one of `user`, `hostname`, `platform` or `network-interfaces` |
| `MAIN-PERM-0005` | runtime | A declared program could not start, for example because it is not installed on the server | Install it on the server, or check its name |

## AI, MCP and actions

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-AI-0001` | check | The driver's training setting is weaker than `ai.data.training` | Pick a driver or plan that meets it |
| `MAIN-AI-0005` | runtime | Prompt injection suspected in a tool result; wrapped as untrusted data | Review the source of the tool result |
| `MAIN-AI-0007` | runtime | Call refused: it would pass a spend limit | Raise `ai.limits` or wait for the period to reset |
| `MAIN-AI-0010` | runtime (start) | `ai.tool()` for a function not declared with `action()` | Declare it with `action()` |
| `MAIN-AI-0011` | runtime | Typed output does not match `schema` after its repairs (one by default, up to two with `repairs`); no value is returned | Fix the schema or the prompt; never use the raw reply as if it were valid |
| `MAIN-AI-0002` | runtime | The model isn't in the `ai.models` allowlist (403) | Add it to `ai.models`, or call an allowed model |
| `MAIN-AI-0003` | runtime | The model provider answered with an error (502); the message has the status, never the body | Check the key, the model name and the provider's status, then retry |
| `MAIN-AI-0004` | runtime | No price is set for the model, so its spend can't be limited | Add the model to `ai.prices` |
| `MAIN-AI-0006` | runtime | The driver has no embeddings, for example `anthropic` | Use a driver with embeddings, such as `openai` |
| `MAIN-AI-0008` | runtime | An agent's model call waits for a person's approval; nothing reached the provider (202) | A person approves it, then the call runs |
| `MAIN-AI-0012` | runtime | A call passed neither or both of `prompt` and `messages` (400) | Pass `prompt` for one instruction, or `messages` for a conversation |
| `MAIN-AI-0013` | runtime | `ai.driver` names a driver that isn't built yet | Use `fake`, `anthropic` or `openai` until it ships |
| `MAIN-AI-0014` | runtime | A model verb option is invalid: `repairs` is not 0, 1 or 2, a label list is empty or repeats a label, or a summary `length` is unknown (400); no request is sent | Pass a valid option, as the verb's page lists them |
| `MAIN-AI-0015` | runtime | A speech or transcription option is invalid: both or neither of `text` and `input`, an unknown format or sample rate, or empty text (400); nothing is sent | Pass one of `text` or `input` and a listed format |
| `MAIN-AI-0016` | runtime | Voice clone refused: no granted `voice-clone` consent recorded by the owner in their own session (403) | The voice's owner records the consent themselves, then clones |
| `MAIN-AI-0017` | runtime | The consent statement's transcript doesn't match `ai.speech.clone.script`, or the statement and samples aren't one speaker (422) | The owner reads the script aloud again |
| `MAIN-AI-0018` | runtime | The voice can't be used: blocked or deleted after consent was revoked or the owner erased, or the caller isn't its owner or in its shared tenant (403) | Use another voice; only the owner can clone it again |
| `MAIN-AI-0019` | check, runtime | A voice that isn't an alias under `ai.speech.voices` or a cloned voice id | Add an alias under `ai.speech.voices`, or use a cloned voice id |
| `MAIN-AI-0020` | runtime | Speech text holds labelled data and the speech driver's data use doesn't meet `ai.data` (403); nothing is sent | Use a driver declared with zero retention, or leave the data out |
| `MAIN-AI-0021` | runtime | The driver doesn't offer this: speech, transcription, streaming, diarisation or cloning (501) | Pick a driver that offers it, as its page lists |
| `MAIN-AI-0022` | runtime | Deleting a cloned voice at the provider failed; the voice stays blocked and the delete is retried (logged) | Nothing to do; check the provider's status if it keeps failing |
| `MAIN-AI-0023` | runtime | Audio is larger than `ai.speech.max-audio`, or a live transcription passed `max-duration` (413) | Send shorter audio, or raise the limit on purpose |
| `MAIN-MCP-0001` | runtime | The server is not in `mcp.servers`; no request leaves the process | Add the server to `main.yaml` on purpose, and allow its host under `network` |
| `MAIN-MCP-0002` | runtime | A pinned server's tool description changed | Read the diff in `mainsdk check`; a person accepts the new description |
| `MAIN-MCP-0003` | runtime | A tool description changed after signing; the tool is left out | Re-sign by releasing again; investigate the change |
| `MAIN-MCP-0004` | runtime | A call limit was reached, for example `per-user: 100/day`; the call is refused before any request | Wait for the period to reset, or raise `limits` on purpose |
| `MAIN-MCP-0005` | runtime | A write or destructive tool call wasn't confirmed by the person, so nothing ran (returned in the tool result) | Wire `elicit` so the person can accept, or have them accept |
| `MAIN-MCP-0006` | runtime | The MCP server answered with an error or not at all (502) | Check the server and the tool arguments |
| `MAIN-MCP-0007` | runtime | A remote server's protected resource metadata is missing or invalid, or its `resource` isn't the server's URL (502) | Fix the server's protected resource metadata, or ask its owner to |
| `MAIN-MCP-0008` | runtime | The remote server's authorization server offers no dynamic registration and no `client-id` is set for it (500) | Set `client-id` for the server under `mcp.servers` |
| `MAIN-MCP-0009` | runtime | A token sent to the app's MCP server was issued for another resource (401); it is never passed on | Send a token issued for this app's MCP server |
| `MAIN-MCP-0010` | runtime | A token sent to the app's MCP server lacks the scope the tool needs (403, `insufficient_scope`) | Ask the person to connect again with the scope the tool needs |
| `MAIN-ACTION-0001` | runtime | An action's input failed its schema (400) | Send input that matches the action's schema |

## SCHEMA and parse

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SCHEMA-0001` | runtime | A value does not match its schema (422); the problem's `errors` lists each issue's path, code and message, never the value | Send a value that matches the schema; read each `errors` entry |
| `MAIN-SCHEMA-0002` | runtime | The input is longer than the parser's size limit (413); nothing was matched | Send a shorter input, or raise `maxLength` on purpose |
| `MAIN-SCHEMA-0003` | runtime (definition) | A pattern has a nested quantifier, such as `(a+)+`, and is refused when the schema or parser is defined (500) | Rewrite the pattern so no repeated group sits inside another repeat |
| `MAIN-SCHEMA-0004` | runtime | A parser found no match, or no parser in `parse.first` matched (422); the input is not echoed | Check the input's format, or add a parser or a model fallback to `parse.first` |

## AGENT

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-AGENT-0001` | runtime | A call names an agent that is not under `agents` | Fix the name, or add the agent under `agents` |
| `MAIN-AGENT-0002` | runtime | The agent is over its budget; the call is refused and nothing reaches the provider | Wait for the budget period to reset, or raise the budget on purpose |
| `MAIN-AGENT-0003` | runtime | An agent tried to approve its own step | A person approves; an agent never can |
| `MAIN-AGENT-0004` | runtime | The agent is switched off (`enabled: false`) | Set `enabled: true` when a person decides to |
| `MAIN-AGENT-0005` | runtime | An approval id that isn't pending: unknown, already approved or expired | List pending approvals and use one of their ids |
| `MAIN-AGENT-0006` | runtime | The store that counts budgets did not answer, and the agent has a budget, so the call is refused (503, retryable) | Check `runtime.store`; calls with no budget keep running |
| `MAIN-AGENT-0007` | runtime | A sync budget call (`admit`, `charge`, `request`) on a store other instances share | Use `reserve`, `settle`, `release` or `ask` |

## Messaging, payments, files, webhooks and events

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-EMAIL-0001` | check | Marketing email without an unsubscribe link (the rule behind the `email.unsubscribe` guarantee) | Send marketing with `email.marketing({ …, unsubscribe })` |
| `MAIN-NOTIFY-0003` | runtime | SMS skipped: no recorded opt-in | Record SMS consent first |
| `MAIN-NOTIFY-0004` | runtime | WhatsApp skipped: no consent | Record WhatsApp consent first |
| `MAIN-PAY-0001` | check | A form posts card data to an app route | Use hosted checkout: `pay.checkout()` |
| `MAIN-PAY-0030` | runtime | `pay.agent` named a payee not in `pay.payees`, or an amount above the payee's `max` | Declare the payee with its `host` and `max`, and allow the host under `network` |
| `MAIN-PAY-0002` | runtime | An amount isn't positive money, or doesn't fit the currency's minor units (400) | Pass money such as `"$20.00"`, or a positive integer in minor units |
| `MAIN-PAY-0010` | runtime | A payment webhook failed verification: bad or missing signature, or outside the 5-minute window (400); no handler ran | Check the webhook secret, and pass the raw, unparsed body |
| `MAIN-PAY-0020` | runtime | The payment provider answered with an error | Look up the request in the provider's dashboard logs |
| `MAIN-PAY-0031` | runtime | `pay.agent` was called with no agent payment rail set | Pass `agentRail` when you create the app; MainSDK holds no keys or funds |
| `MAIN-PAY-0032` | runtime | A held agent payment was sent before a person approved it | A person approves it first |
| `MAIN-FILES-0002` | runtime | `access` denied this user a file URL (403), or someone other than the uploader called `files.complete()` | Read or complete as the owner, or add an access rule for the reader |
| `MAIN-FILES-0005` | runtime | `files.complete()` ran the scan and found an infected file; the upload fails and the file is quarantined, with no readable URL | Nothing to fix in code; ask the user for a clean file |
| `MAIN-FILES-0010` | check | A storage SDK used for user uploads outside a `files` driver, for example `@vercel/blob` in route code | Upload through `files`, and pick the store with `files.driver` |
| `MAIN-FILES-0003` | runtime | A per-call `maxSize` is above `files.maxSize` | Lower it in the call, or raise `files.maxSize` on purpose |
| `MAIN-FILES-0004` | runtime | An upload target is invalid, already used or older than 15 minutes (403) | Ask the server for a new one with `files.uploadUrl()` |
| `MAIN-FILES-0006` | runtime | The file is larger than the upload allows (413); it is removed and never readable | Send a smaller file |
| `MAIN-FILES-0007` | runtime | The file's type isn't one the upload allows (415); it is removed and never readable | Send one of the allowed types |
| `MAIN-FILES-0008` | runtime | `files.complete()` ran before any bytes reached the store (409) | Send the file to `upload.url` first |
| `MAIN-FILES-0011` | runtime | The s3 driver found no credentials: no keys in `main.yaml` and no runtime role | Set `files.accessKeyId` and `files.key: secret:NAME`, or run with an IAM role |
| `MAIN-FILES-0012` | runtime | `files.upload()` from the server isn't supported by the s3 driver yet | Use `files.uploadUrl()` and let the browser send the file |
| `MAIN-FILES-0013` | runtime | The store answered with an error (502) | Check the bucket, region and credentials; the message has the store's status |
| `MAIN-FILES-0014` | runtime | `files.driver` names a driver that isn't built yet | Use `local`, `memory` or `s3` until it ships |
| `MAIN-FILES-0015` | runtime | The getqr.id API has no endpoint for this step, such as downloading or deleting stored bytes (501) | Send files with `files.uploadUrl()` and read them with `files.url()`; getqr.id scans on its side |
| `MAIN-SHARE-0001` | check | A link shortener, deep-link or QR code library imported in app code, such as `bitly`, `tinyurl`, `qrcode`, `qrcode.react`, `react-qr-code`, `qr-code-styling`, `branch-sdk` or `@react-native-firebase/dynamic-links` | Share through `share()` or `links` and getqr.id (ADR-0009, ADR-0056) |
| `MAIN-SHARE-0002` | runtime | `expiresIn` is longer than `share.max-expiry` (400) | Use a shorter `expiresIn`, or raise `max-expiry` on purpose |
| `MAIN-SHARE-0003` | runtime | A share asked for access other than `read` (400); share links never grant writes | Use `access: "read"` |
| `MAIN-SHARE-0004` | runtime | `share.stats()` was called without the person's consent (403) | Ask for analytics consent, then pass `consent: true` |
| `MAIN-SHARE-0005` | runtime | getqr.id answered a share or stats call with an error | Check the key, its scopes and the plan; quote the request id to support |
| `MAIN-SHARE-0006` | runtime | getqr.id returned a share that breaks the rules (kept forever, no expiry, or off getqr.id); it is revoked and the call fails (502) | Check the plan and the key |
| `MAIN-SHARE-0007` | check | Code calls, or `network.allow` lists, a link shortener, deep-link or QR rendering service such as `api-ssl.bitly.com`, `tinyurl.com`, `is.gd`, `api.short.io`, `firebasedynamiclinks.googleapis.com`, `api2.branch.io` or `api.qrserver.com` | Remove the call and the `network.allow` entry; share through `share()` (ADR-0056) |
| `MAIN-SHARE-0008` | runtime | `share.driver: local` in production; the app refuses to start (500) | Set `share.driver: getqr` and `share.key` in `main.env.prod.yaml` |
| `MAIN-SHARE-0009` | runtime | A local share link was opened after it expired or was revoked (410) | Make a new share |
| `MAIN-SHARE-0010` | runtime | `share.update` was asked to keep a share forever (400); share links always expire, and placements are the only kept links | Use `extend` within `max-expiry`, or a placement for a printed QR code |
| `MAIN-SHARE-0011` | runtime | `share.placements.create` failed its row checks and created nothing (422; the problem lists each row, field and reason) | Fix the listed rows and send all of them again with the same idempotency key |
| `MAIN-SHARE-0012` | runtime | A getqr.id webhook event type that isn't in the pinned contract; acknowledged, logged once per type, not passed on | Take the new getqr.id contract version with `contract diff --vendor getqr` |
| `MAIN-SHARE-0013` | runtime | A share's `title` or `metadata` holds personal data, `metadata` has more than 16 keys or a reserved `mainsdk_` key, or a `contact` share isn't the signed-in person's own (400) | Use record ids your app resolves after sign-in; share only the person's own contact card |
| `MAIN-LINK-0001` | runtime | Invite link used up, expired, revoked, or accepted by a session whose verified email isn't the bound one (410; a mismatch leaves the invite valid) | Ask the inviter for a new invite, or sign in with the invited email |
| `MAIN-LINK-0002` | runtime | `links.create` or `links.invite` named a screen not in `links.routes`, or params missing or not declared (400) | Declare the screen and its params under `links.routes` |
| `MAIN-LINK-0003` | check | A route in `links.routes` declares a param that identifies a person: `email`, `phone`, `name`, `ip`, `token` or a labelled field | Pass a record id and resolve it after sign-in |
| `MAIN-LINK-0004` | check | `links.apps` is missing what association files need (iOS `team-id` and `bundle-id`, Android `package` and a `sha256`), or `links.base` isn't an HTTPS origin | Add the missing identifiers; the finding names them |
| `MAIN-LINK-0005` | check, runtime | `links.signIn` is called but `link` isn't in `auth.methods` (runtime: 400) | Add `link` to `auth.methods`, or remove the call |
| `MAIN-HOOK-0002` | runtime | Incoming webhook with a wrong signature or a timestamp too old (400) | Check the signing secret and the sender's clock |
| `MAIN-HOOK-0003` | check, runtime | `webhooks.receive` names neither a key under `webhooks.inbound` nor a preset, or an entry names an unknown preset | Declare the endpoint under `webhooks.inbound` with a listed preset |
| `MAIN-HOOK-0004` | runtime | A customer endpoint URL is refused: not `https`, carries credentials, or is or resolves to a loopback, private, link-local, shared, multicast or metadata address (400; a delivery fails without connecting) | Use a public `https` URL with no credentials |
| `MAIN-HOOK-0005` | runtime | The tenant already has `webhooks.endpoints.max-per-tenant` endpoints (409) | Delete an endpoint, or raise the limit on purpose |
| `MAIN-HOOK-0006` | runtime | An event type that isn't under `webhooks.endpoints.events` (400) | Subscribe to a listed type, or add it to `main.yaml` |
| `MAIN-HOOK-0007` | runtime | An endpoint was disabled after `disable-after` failures; an audit event and `webhook.endpoint.disabled` are written | Fix the receiver, then enable the endpoint again |
| `MAIN-HOOK-0008` | runtime | A test or redeliver to a disabled endpoint (409) | Enable the endpoint first |
| `MAIN-HOOK-0009` | runtime | No endpoint or delivery with that id in the current tenant (404) | Use an id from `webhooks.endpoints.list` |
| `MAIN-HOOK-0010` | runtime | An endpoints call without the `webhooks.manage` permission (403) | Grant `webhooks.manage` to the person who manages endpoints |
| `MAIN-HOOK-0011` | check | A `twilio` inbound entry has no `public-url` | Set `public-url` to the URL Twilio calls |
| `MAIN-HOOK-0012` | CLI | `mainsdk webhooks replay` refused: not a dev environment, no `mainsdk dev` running, or an unknown id (exit 2) | Run `mainsdk dev`, then pick an id from `mainsdk webhooks list` |
| `MAIN-EVENT-0004` | check | A breaking event schema change under the same version | Publish the change as a new event version |
| `MAIN-EVENT-0010` | check, runtime | A broadcast payload is larger than `max-payload` (runtime), or `max-payload` is over 1MB (check) | Send less data, or lower `max-payload` to 1MB or less; use `events.publish` or `jobs` for large messages |
| `MAIN-EVENT-0011` | check, runtime | `events.broadcast` uses the `local` driver in production, so other instances never receive it | Set `events.broadcast.driver: redis` in `main.env.prod.yaml` |
| `MAIN-EVENT-0012` | runtime | A broadcast topic doesn't match `^[a-z0-9][a-z0-9._-]{0,127}$`, or the payload isn't JSON | Use a lowercase topic such as `cache.invalidate`, and send JSON |
| `MAIN-EVENT-0013` | runtime | A broadcast was dropped because the driver is unreachable; the call resolves with `dropped: true` and logs once per outage | Restore the driver. Broadcast is best effort, so anything that must happen goes through `events.publish` or `jobs` |

## LOG, TUF and the Main mark

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-LOG-0003` | runtime | `MAINSDK_DEBUG=driver` ignored in production | Use a debug session (phase 2) |
| `MAIN-LOG-0005` | runtime | An attribute not in `telemetry@1` was dropped before export | Remove the attribute or propose it for the contract |
| `MAIN-LOG-0010` | runtime | Audit sink unavailable; buffering to local disk | Restore the sink |
| `MAIN-OBS-0001` | runtime | An observer threw or rejected; caught, the request is untouched, and the observer keeps receiving | Fix the observer; it is logged at most once a minute |
| `MAIN-OBS-0002` | runtime | An observer's queue was full and its oldest events were dropped | Make the observer faster, or raise its `max` |
| `MAIN-OBS-0003` | runtime | An observer's synchronous work took over 10 ms and blocked the event loop | Move the work into an async step or a job |
| `MAIN-OBS-0004` | runtime (testing kit) | A published payload breaks its channel schema | Match the payload to the channel's schema, or add a new channel version |
| `MAIN-OBS-0005` | runtime | `observe()` named a channel that isn't in the registry, or a pattern that matches none | Use the channel the message suggests; see `observe.channels()` |
| `MAIN-TUF-0001` | runtime | Rollback: the mirror served older metadata; the newer version is kept | None in your app; report the mirror |
| `MAIN-TUF-0004` | runtime | Delivered packs are stale; the last good pack stays in force | None needed; it refreshes when the mirror is back |
| `MAIN-INFO-0002` | CLI | The signed `/.well-known/mainsdk.json` doesn't verify (exit 1) | Treat the record as untrusted |
| `MAIN-CRYPTO-0002` | check | A Main 27 crypto rule, in the family that refuses MD5, SHA-1 and DES | Use `crypto` or `keys` |
| `MAIN-LOG-*`, `MAIN-CRYPTO-*` | check | Main 27 logging and crypto rules: secrets or personal data in logs, MD5, SHA-1, DES | Follow the fix in each finding |
| `MAIN-SUPPLY-*`, `MAIN-RELEASE-*` | check | Main 27 supply-chain rules: unpinned or unverified packages, unsigned builds | Follow the fix in each finding |

## ANALYTICS

Planned for 0.3 (spec `analytics`).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-ANALYTICS-0001` | runtime | A product event that `analytics.events` doesn't declare (throws in dev and test; dropped and logged once per name in production) | Declare the event under `analytics.events` |
| `MAIN-ANALYTICS-0002` | runtime | An event's props fail its schema (throws 422 with issue paths in dev and test; dropped and logged once per name in production) | Send props that match the declared types |
| `MAIN-ANALYTICS-0003` | runtime | An event was dropped: no consent for its purpose. Logged as a count, never with the person | Collect consent for the event's purpose |
| `MAIN-ANALYTICS-0004` | runtime | A query names an undeclared event, prop or metric, or a `by` the query kind doesn't take (400) | Declare the name, or use a `by` the query kind takes |
| `MAIN-ANALYTICS-0005` | runtime | A metric attribute that isn't declared was dropped, or a metric passed 1,000 attribute combinations and the rest were folded into `_other` | Declare the attribute, or use fewer distinct values |
| `MAIN-ANALYTICS-0006` | runtime | A driver in `analytics.to` is unreachable; events were buffered, then dropped with a counter | Restore the driver; nothing else to do |
| `MAIN-ANALYTICS-0007` | runtime | `analytics.query` called without `builtin` in `analytics.to` (501) | Add `builtin` to `analytics.to` |
| `MAIN-ANALYTICS-0008` | runtime | The event's region store isn't configured or reachable; the event was dropped and written nowhere else | Configure the store for the tenant's region |
| `MAIN-ANALYTICS-0010` | check | `analytics.track` or `page` with an event name that isn't declared, or isn't a string literal | Declare the event and pass its name as a string literal |
| `MAIN-ANALYTICS-0011` | check | A prop or metric attribute that would identify a person: a name such as `email`, `phone`, `name`, `ip`, `userId`, `deviceId`, `idfa`, `gaid` or `fingerprint`, or a field with a data label | Remove the identifying prop; use a declared, non-identifying one |
| `MAIN-ANALYTICS-0012` | check | A driver in `analytics.to` doesn't serve a region the app stores data in, or declares cross-app identifiers or third-party cookies | Pick a driver that serves your regions without cross-app identifiers |
| `MAIN-ANALYTICS-0013` | check | A children's audience (`users.audience: children`) with a per-person driver (`posthog`) in `analytics.to` | Use `builtin` or `otel` for a children's audience |

## HEALTH

Spec `health`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-HEALTH-0001` | runtime | This instance is not ready: starting, draining, or a critical dependency is down (503 on `/readyz`) | Wait for start-up, or restore the dependency |
| `MAIN-HEALTH-0002` | runtime | A request arrived after the app stopped accepting work (503 with `Retry-After` and `Connection: close`) | Retry; the load balancer should stop sending traffic once `/readyz` fails |
| `MAIN-HEALTH-0003` | runtime | The shutdown deadline cut work that was still running; the record lists what was released | Raise `health.deadline`, or shorten the work |
| `MAIN-HEALTH-0004` | runtime | A probe threw or timed out; its dependency is marked down (logged once per change) | Fix the dependency or the probe |
| `MAIN-HEALTH-0005` | check | An `slo` value is invalid: availability outside 90% to 99.999%, or a latency that isn't a duration | Use a percentage in range and a duration such as `300ms` |
| `MAIN-HEALTH-0006` | check | `health.critical` names a dependency the app doesn't configure, or names `mainsdk-cloud` or `tuf`; or `drain-delay` plus 2s is not less than `deadline` | List only dependencies the app configures; raise `deadline` or lower `drain-delay` |
| `MAIN-HEALTH-0010` | runtime | The availability error budget is burning faster than a burn rule allows | Find the failing dependency or release |
| `MAIN-HEALTH-0011` | runtime | The latency budget is burning faster than a burn rule allows | Find the slow call or release |

## ALERT

Spec `alerts`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-ALERT-0001` | check | An `alerts` value is invalid: an unknown driver, source or severity, a route to an undeclared recipient, a quiet-hours time or time zone that doesn't parse, or an escalation `after` shorter than `group.wait` | Declare every recipient a route names, use a known driver and an IANA time zone, and escalate after the group wait |
| `MAIN-ALERT-0002` | check | Production has an `alerts` section, but no route sends `critical` alerts to a recipient | Add a route for `severity: critical`, or a default route with no `match` |
| `MAIN-ALERT-0003` | check | A `webhook` recipient's host isn't in `network.allow`, or a recipient key or webhook URL is written as a literal | Add the host to `network.allow` and reference keys and URLs as `secret:NAME` |
| `MAIN-ALERT-0010` | runtime | An alert was raised or resolved; the log record carries the alert | Nothing to fix; follow the alert's runbook |
| `MAIN-ALERT-0011` | runtime | A recipient failed after its last retry; the others were still sent to | Check the recipient's key or URL with `mainsdk alerts test <recipient>` |
| `MAIN-ALERT-0012` | runtime | The runtime store didn't answer; dedupe and escalation fell back to this instance | Restore the store; until then each instance may send once |
| `MAIN-ALERT-0013` | runtime | `alerts.raise` refused or trimmed an input: a key over 200 characters, more than 20 attrs, or an attr outside `telemetry@1` | Shorten the key and send only `telemetry@1` attributes |
| `MAIN-ALERT-0014` | runtime | `alerts.ack` refused: the caller lacks `alerts.manage` (403) | Grant `alerts.manage` to the people who acknowledge alerts |
| `MAIN-ALERT-0015` | runtime | A recipient passed `max-per-hour`; the rest of the hour arrives as one summary | Group more alerts, fix the noisy source, or raise `max-per-hour` |

## RESOURCE

Spec `resources`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-RESOURCE-0001` | runtime | A new non-critical request or realtime connection was shed (503 with `Retry-After`); logged at most once a minute with a count | Retry after the delay; add capacity, or list the route under `resources.critical` if it must never be shed |
| `MAIN-RESOURCE-0002` | runtime | The instance entered or left `pressure` or `shed`, with the reasons and the values | Find what uses the CPU, event loop or memory; `mainsdk status` shows the snapshot |
| `MAIN-RESOURCE-0003` | check | A `resources` value is invalid: a percentage outside 10% to 100%, a `loop` under 20ms, a `shed` threshold not above its `pressure` threshold, a `critical` pattern that doesn't parse, or `nice` outside 0 to 19 | Use values in range and a `shed` threshold above `pressure` |
| `MAIN-RESOURCE-0004` | runtime | `nice` couldn't be applied; the app starts anyway (logged once) | Remove `nice`, or run where the process may lower its own priority |
| `MAIN-RESOURCE-0005` | runtime | Background work waited longer than `max-pause`; it now runs one at a time until pressure ends | Add capacity, or move the background work to a worker process |
| `MAIN-RESOURCE-0006` | runtime | The V8 heap limit is above the container's memory limit, so the process can be killed before the heap fills | Set `--max-old-space-size` to the value the record names |

## SEAL

Spec `seal`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SEAL-0001` | runtime | The files on disk don't match the sealed build hash (logged in warn mode; the app refuses to start in refuse mode) | Redeploy the sealed build |
| `MAIN-SEAL-0002` | runtime | No seal, or an unreadable one, in an environment other than dev while `seal.mode` isn't `off` | Run the seal step in the build |
| `MAIN-SEAL-0003` | seal | The record would carry a secret value, an env value or personal data; nothing is written | Remove the value from what is sealed |
| `MAIN-SEAL-0004` | verify | The signature is valid but the signer isn't one of `seal.signers` or the given `--key` or `--identity` | Verify with the expected signer, or add it to `seal.signers` |
| `MAIN-SEAL-0005` | seal | A file can't go into the build hash: a symlink leaving the roots, an unreadable file, a path with CR or LF, or a missing root | Fix or exclude the file the error names |
| `MAIN-SEAL-0006` | check | `check --env prod` with `seal.mode: off` | Turn sealing on for production |
| `MAIN-SEAL-0007` | verify | The record's build hash doesn't match the files of the folder given to `seal verify` | Verify the folder that was sealed |
| `MAIN-SEAL-0008` | seal diff | A sensitive capability is new since the old seal: a call of a new sensitive kind, a new sensitive call, a widened grant, or a new destructive action | Review the change; a person accepts it |

## REALTIME

Spec `realtime`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-REALTIME-0001` | runtime | Subscribe with no session (401) | Sign the person in first |
| `MAIN-REALTIME-0002` | runtime | Subscribe refused: access denied, or a `user:` channel that isn't the caller's (403) | Subscribe to a channel the person may read |
| `MAIN-REALTIME-0003` | runtime, check | A channel prefix not declared under `realtime.channels` (404 on subscribe, a throw on publish; a check on a literal in code) | Declare the prefix under `realtime.channels` |
| `MAIN-REALTIME-0004` | runtime | Event data is not JSON, or is larger than `max-event` | Send JSON under the size limit |
| `MAIN-REALTIME-0005` | runtime | A channel or event name doesn't match its pattern | Use the pattern the error shows |
| `MAIN-REALTIME-0006` | check, runtime | The `local` driver in production without `single-instance: true` | Use a shared driver, or set `single-instance: true` for one instance |
| `MAIN-REALTIME-0007` | check | `max-event` plus the 4KB envelope is larger than `events.broadcast.max-payload` | Lower `max-event`, or raise `events.broadcast.max-payload` |
| `MAIN-REALTIME-0008` | runtime | A stream was closed because its reader fell more than `buffer` behind (logged) | Reconnect; read faster or raise `buffer` |
| `MAIN-REALTIME-0009` | runtime | The person already has `per-user` open streams (429) | Close another stream, or raise `per-user` |
| `MAIN-REALTIME-0010` | runtime | WebSocket upgrade refused: the `Origin` is neither the app's own nor in `inbound.cors.origins` (403) | Add the origin to `inbound.cors.origins` |
| `MAIN-REALTIME-0011` | runtime | WebSocket upgrade refused: the ticket is unknown, expired, used or from another tenant (401) | Get a fresh ticket with `realtime.ticket()` right before connecting |
| `MAIN-REALTIME-0012` | runtime, check | A client message or stream refused: no `send` action on the prefix, access denied, or no handler | Declare `send` under `realtime.channels.<prefix>` and register `realtime.on` |
| `MAIN-REALTIME-0013` | runtime | A client message isn't JSON, has an unknown type, is over `max-message` or fails the handler's schema | Send JSON that matches the handler's schema |
| `MAIN-REALTIME-0014` | runtime | Client messages over `realtime.socket.rate` | Send less often, or raise the rate on purpose |
| `MAIN-REALTIME-0015` | check | `realtime.transports` lists `websocket` on an adapter or platform without HTTP upgrade | Use `sse`, or a server adapter with upgrade support |
| `MAIN-REALTIME-0016` | runtime, check | A binary stream refused: prefix not under `realtime.streams`, type not listed, or `max-per-socket` reached | Declare the prefix and type under `realtime.streams` |
| `MAIN-REALTIME-0017` | runtime | A stream passed its byte `budget`, or a frame passed `max-frame`; the stream or socket is closed | Send smaller frames, or raise the budget on purpose |

## WORKFLOW

Spec `workflows`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-WORKFLOW-0001` | runtime | `step`, `sleep`, `waitFor` or `approve` called outside a workflow run (500) | Call it inside a workflow function |
| `MAIN-WORKFLOW-0002` | runtime | On resume, the code reached a step the journal doesn't have in that place (code changed under a live version); the run stops as `stuck` | Ship the change as a new workflow version |
| `MAIN-WORKFLOW-0003` | runtime | No registered code for the run's workflow version; the run stops as `stuck` | Register the version the run started on |
| `MAIN-WORKFLOW-0004` | runtime | A step result or workflow input is not JSON, or is larger than `max-step-result` | Return JSON under the size limit |
| `MAIN-WORKFLOW-0005` | runtime | A step failed after its last retry; the run is `failed` | Fix the cause and start a new run |
| `MAIN-WORKFLOW-0006` | runtime | No run with that id in the current tenant (404) | Use an id from this tenant |
| `MAIN-WORKFLOW-0007` | runtime | The journal store is unreachable, so the run was not started (503, retryable) | Retry once the store is back |
| `MAIN-WORKFLOW-0008` | runtime | Cancel of a run that has already finished (409) | None; the run is done |
| `MAIN-WORKFLOW-0009` | runtime, check | A duration is not a valid `<n><s\|m\|h\|d>` value, or is over 365d | Write a duration such as `30m` |
| `MAIN-WORKFLOW-0010` | check | A clock, random or network call (`Date.now`, `new Date()`, `Math.random`, `crypto.randomUUID`, `http.*`, `fetch`) in a workflow function outside a step | Move the call into a `step` |
| `MAIN-WORKFLOW-0011` | check, runtime | The `local` driver in production | Use a database driver in `main.env.prod.yaml` |
| `MAIN-WORKFLOW-0012` | check | Two workflow definitions with the same name and version | Give each definition its own name and version |

## APPS

Spec `app-registry`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-APPS-0001` | runtime | The registry folder is unsafe (world-writable, or owned by another user and not root) or not writable; nothing is written and the app keeps serving | Fix the folder's owner and mode |
| `MAIN-APPS-0002` | CLI | Two records claim one project id from different repositories | Give the copied app a new id with `mainsdk init --new-id` |
| `MAIN-APPS-0003` | CLI | A record says running but was last seen more than 5 minutes ago | Restart the app, or remove the stale record |
| `MAIN-APPS-0004` | check | Code reads the registry folder directly, or `permissions.disk` grants it, instead of `apps.peer()` | Use `apps.peer()` |
| `MAIN-APPS-0005` | check | A file that reads `apps.peer()` or `apps.call()` results also makes an outbound call; registry data may leave the machine | Split the code so peer data stays local |
| `MAIN-APPS-0006` | runtime | A peer call between apps that don't both list each other under `apps.peers` | List each app under the other's `apps.peers` |
| `MAIN-APPS-0007` | runtime | A peer call to an action the callee doesn't list under `apps.expose` | List the action under the callee's `apps.expose` |
| `MAIN-APPS-0008` | runtime | The link handshake failed: the peer's proof doesn't verify with the identity key in its seal, or its seal is invalid | Re-seal the peer and check its identity key |
| `MAIN-APPS-0009` | runtime | `apps.link: on` without `apps.identity` and `MAINSDK_APP_KEY`; the link stays off | Set `apps.identity` and `MAINSDK_APP_KEY` |

## ADOPT, ASSURE and CLOUD

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-ADOPT-0001` | CLI | `adopt next` before key flows are locked | Run `mainsdk adopt lock` first |
| `MAIN-ASSURE-0003` | CLI | The record `assure` read from a URL has an invalid signature (exit 2) | Treat the app's record as untrusted |
| `MAIN-CLOUD-0402` | Cloud (phase 2) | Free allowance used, after the warning and grace period. Only the Cloud answers it; your app keeps running | Add a card with a cap, or move to Teams |

## MEM

Agent memory (spec `memory`, planned for 0.4).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-MEM-0001` | runtime | No subject: `about` is missing and the call has no `on_behalf_of` or session user (400) | Pass `about`, or call inside a session or an agent call made for a person |
| `MAIN-MEM-0002` | runtime | No memory with that id in the current tenant, app and scope (404, same body either way) | Use an id returned in this tenant and app |
| `MAIN-MEM-0003` | runtime | The injection check flagged the text of a write; nothing was stored (422) | Store what the person said, not instructions from a tool or page |
| `MAIN-MEM-0004` | runtime | A recalled memory was flagged by the injection check and left out of the context (logged) | Review the memory and `forget` it if it is an injection |
| `MAIN-MEM-0005` | runtime | The agent has no memory grant for this verb, or `forget: deny` (403) | Add `read` or `write` under `agents.<name>.memory` on purpose |
| `MAIN-MEM-0006` | runtime | The agent is over its memory `recalls` or `writes` per day (429) | Wait for the day to reset, or raise the limit on purpose |
| `MAIN-MEM-0007` | runtime | Forget refused: a legal hold applies to the person (409) | Lift the hold with the person who set it, then forget |
| `MAIN-MEM-0008` | runtime | A children's rule refused it: children are off, no guardian consent, or a provider model would extract (403) | Set `children: guardian` and record the guardian's consent; use a local extract model |
| `MAIN-MEM-0009` | runtime | Invalid input: unknown kind, bad duration, `pan` label, `k` over 100, `hops` over 2 or a bad predicate (400) | Fix the argument the problem's `errors` member names |
| `MAIN-MEM-0010` | runtime | Revise of a version that is no longer current (409) | Read the current version with `memory.history` and revise that |
| `MAIN-MEM-0011` | runtime | Import refused: a line is malformed, or personal or non-procedure memory would cross a tenant or app (422) | Import into the same tenant and app, or export only `procedure` memories labelled `none` |
| `MAIN-MEM-0012` | runtime | The driver doesn't declare the capability the call needs, such as `asOf`, `hops` or `relate` (501) | Drop the option, or use a driver that declares the capability |
| `MAIN-MEM-0013` | runtime | The memory store is unreachable (503, retryable); `context` degrades to an empty block | Retry; check the store and its region endpoint |
| `MAIN-MEM-0014` | runtime | An Anthropic memory tool path is outside `/memories/` or tries to traverse (400) | Keep paths under `/memories/` with no `..` or encoded separators |
| `MAIN-MEM-0015` | runtime | A driver hasn't confirmed the erase yet; no receipt is issued and the erase keeps retrying (503) | Wait for the retry job; the receipt follows the driver's confirmation |
| `MAIN-MEM-0016` | runtime | A write from a tool, MCP, the web or an import was refused by `from-tools: refuse` (403) | Ask the person to confirm the fact, or set `from-tools: quarantine` |
| `MAIN-MEM-0020` | check | A driver that only soft-deletes holds memories that may be personal | Use a driver with `erase: hard`, such as `db` |
| `MAIN-MEM-0021` | check | A region the app serves has no endpoint in the memory driver | Add the region's endpoint, or use the `db` driver in that region |
| `MAIN-MEM-0022` | check | Recalled memory is passed as the `system` text of an `ai` call | Pass `memory.context()` text in a user message as data |
| `MAIN-MEM-0023` | check | A driver is named `memory`; the in-process driver is `local` (ADR-0039) | Rename the driver to `local` |
| `MAIN-MEM-0024` | check | `children: guardian` with an extract model on a provider | Use a local extract model, or turn children off |
| `MAIN-MEM-0025` | check | The `local` memory driver in production | Use the `db` driver or a provider driver in production |

## PORT and SDK

Portable core (spec `portable-core`, planned for 0.4) and SDK conventions (spec `sdk-conventions`, planned for 0.3).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-PORT-0001` | check | `main.yaml` uses an interface or driver that the declared `targets.<name>.runtime` can't run, such as `disk` on `workerd` | Move that work to a target on Node, Bun or Deno, or drop the interface from this target |
| `MAIN-PORT-0002` | runtime | A call reached the portable fallback of an adapter-only interface on a runtime without it | Call it from a server target on Node, Bun or Deno |
| `MAIN-PORT-0003` | runtime | A portable global the call needs is missing, such as `crypto.subtle` on an old runtime | Upgrade the runtime to one that implements the WinterTC Minimum Common API |
| `MAIN-SDK-0001` | runtime | The call was aborted through its `signal`; `cause` is the signal's reason. Never retried | Nothing to fix if the abort was meant; otherwise don't abort the signal |
| `MAIN-SDK-0002` | check | Code for a production target imports `mainsdk/experimental/*` (a warning) | Use the stable subpath once the interface ships, or accept the risk on purpose |

## API

Spec `app-api`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-API-0001` | runtime (start) | An action's HTTP mapping is invalid: a `GET` on a write or destructive action, a path parameter that isn't a top-level string or integer input key, a nested field on a `GET` or `DELETE`, two actions on one method and path, or a name segment that isn't an identifier (500) | Fix the mapping the error names: reads use `GET`, path parameters are top-level inputs |
| `MAIN-API-0002` | runtime | An `Idempotency-Key` was reused with a different input (422) | Send a new `Idempotency-Key` for a new request |
| `MAIN-API-0003` | runtime | The first request with this `Idempotency-Key` is still running (409, retryable) | Wait for the first request to finish, then retry |
| `MAIN-API-0004` | runtime | An action returned a value that fails its `output` schema; nothing of it is sent (500) | Return a value that matches the `output` schema |
| `MAIN-API-0005` | runtime | A risk listed under `api.idempotency.require` was called without `Idempotency-Key` (400) | Send an `Idempotency-Key` header with the call |
| `MAIN-API-0006` | runtime | The major version was called after its `sunset` date (410) | Move the client to the current major version |
| `MAIN-API-0007` | client | The generated client got no answer, or an answer that isn't problem details or JSON (retryable for network failures) | Check the URL and the network; retry network failures |
| `MAIN-API-0010` | check | An action's input or output has no JSON Schema (a foreign schema without `toJSONSchema()`); the document and client type it as unknown | Use a schema with `toJSONSchema()`, or a native one from `mainsdk/schema` |
| `MAIN-API-0011` | check | The committed snapshot at `api.snapshot` differs from what the code generates | Run `mainsdk api openapi` and commit the new snapshot |
| `MAIN-API-0012` | check, CLI | `mainsdk api diff` found a breaking change in a major version that already shipped | Ship the change in a new major version, and keep the shipped one working |

## IMPORT

Spec `imports`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-IMPORT-0001` | runtime | The format or encoding isn't supported: not CSV, XLSX, JSON or NDJSON, not UTF-8, a JSON value that isn't an array, a legacy `.xls` or an encrypted workbook (415) | Convert the file to CSV, XLSX, JSON or NDJSON in UTF-8 |
| `MAIN-IMPORT-0002` | runtime | The file is over a limit: `max-rows`, `max-columns`, `json-max` or the `files` size (413); rows up to the limit are not written | Split the file, or raise the limit under `imports` |
| `MAIN-IMPORT-0003` | runtime | The mapping is incomplete or unconfirmed: a required field has no column, a mapped column doesn't exist, or a field would come from a model suggestion not passed back in `mapping` (422) | Map every required field to a column, and confirm the suggested mapping |
| `MAIN-IMPORT-0004` | runtime | The file isn't `ready` yet (still scanning) (409, retryable) | Wait for the scan to finish, then retry |
| `MAIN-IMPORT-0005` | runtime | The run stopped because failed rows passed `maxErrors`; the report lists them (422) | Fix the rows the report lists, then run again |
| `MAIN-IMPORT-0006` | runtime | An unsafe workbook: a zip entry over the size or ratio limit, a path outside the archive, or XML with a `DOCTYPE` or entity (422) | Save the workbook again without the flagged parts |
| `MAIN-IMPORT-0010` | runtime (logged) | A pull failed: the source was unreachable or refused; the cursor is unchanged and the next run retries | Check the source's address and credentials; the next run retries |
| `MAIN-IMPORT-0011` | runtime (logged) | The deletion guard tripped: a full pass returned fewer keys than `deletes-guard`, so nothing was deleted | Check the source; raise `deletes-guard` only on purpose |
| `MAIN-IMPORT-0012` | runtime (logged) | The source rejected the stored cursor; a full pass is scheduled | Nothing to fix; the full pass runs |
| `MAIN-IMPORT-0020` | check | A source under `imports.sources` has no `imports.source()` in the code, or the code defines a source `main.yaml` doesn't declare | Declare each source in both `main.yaml` and the code |

## CONN

Spec `connections`. The MCP codes that the connections spec adds are under [AI, MCP and actions](#ai-mcp-and-actions).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-CONN-0001` | runtime | The provider isn't under `connections.providers` (500 at start for config, 400 for a call) | Declare the provider under `connections.providers` |
| `MAIN-CONN-0002` | runtime | No active connection to this provider for this person or tenant (403); the fix names `connections.connect` | Ask the person to connect with `connections.connect` |
| `MAIN-CONN-0003` | runtime | The connection wasn't granted the scope this call needs (403); the fix asks to connect again with it | Ask the person to connect again with that scope |
| `MAIN-CONN-0004` | runtime | The agent may not use this scope: it isn't under `agents.<name>.connections.<provider>` (403); nothing is sent | Add the scope under `agents.<name>.connections.<provider>` |
| `MAIN-CONN-0005` | runtime | The URL's host isn't one of the provider's hosts, or isn't `https`; the token is not sent (403) | Call an `https` host the provider lists under `hosts` |
| `MAIN-CONN-0006` | runtime | The callback was refused: state missing, unknown, expired, used or from another session, code verifier rejected, or issuer mismatch (400) | Start the connect flow again |
| `MAIN-CONN-0007` | runtime | The connection expired or was revoked at the provider (`invalid_grant`), or its key was destroyed; connect again (401) | Ask the person to connect again |
| `MAIN-CONN-0008` | runtime | The provider couldn't be reached to refresh an expired token (503, retryable) | Retry when the provider is reachable |
| `MAIN-CONN-0009` | runtime | `connect` asked for a scope beyond `providers.<name>.scopes` (400); nothing is sent | Request only scopes listed under `providers.<name>.scopes` |
| `MAIN-CONN-0020` | check | An agent or `step-up` names a scope not under its provider's `scopes`, or a provider not under `connections.providers` | Name only scopes and providers declared under `connections` |
| `MAIN-CONN-0021` | check | A provider has no preset and no `hosts`, so no call could be sent | Use a `preset`, or list the provider's `hosts` |

## SEED and PREVIEW

Spec `seed-preview`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SEED-0001` | runtime | Seed or fixtures refused: the resolved environment is production (nothing is opened or written) | Run seed and fixtures outside production |
| `MAIN-SEED-0002` | runtime | Seed refused: the target database is not seedable (not local, not a preview, no seedable marker) | Seed a local or preview database that carries the seedable marker |
| `MAIN-SEED-0003` | runtime | A field can't be generated from its schema (an unsupported pattern or a `refine`); set a generator under `seed.models` | Set a generator under `seed.models` for that field |
| `MAIN-SEED-0004` | runtime | AI text unavailable or over `seed.ai.limit`; built-in text was used and the run went on | Nothing to fix; raise `seed.ai.limit` or check the AI driver if you want AI text |
| `MAIN-PREVIEW-0001` | runtime | The db driver has no branching capability; use `preview.driver: local` or a driver that has it | Use `preview.driver: local`, or a db driver with branching |
| `MAIN-PREVIEW-0002` | runtime | The provider refused or failed while making the preview; what was made was deleted | Check the provider's status and credentials, then retry |
| `MAIN-PREVIEW-0003` | runtime | Refused to delete or seed the parent, or a database the preview driver did not make | Point the preview at a database it made |
| `MAIN-SEED-0010` | check | `seed` or `preview` is active in production, or app code outside tests and scripts imports the seed engine | Keep seed and preview out of production; import the seed engine only in tests and scripts |
| `MAIN-SEED-0011` | check | A literal under `seed.models` looks like real personal data (an email outside the reserved domains, a phone number outside the reserved ranges) | Use reserved example values, such as `example.com` emails and reserved phone ranges |

## ENGAGE

Spec `engagement`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-ENGAGE-0001` | runtime | Engagement message skipped: the person reached the weekly limit | Nothing to fix; the message waits for next week |
| `MAIN-ENGAGE-0002` | runtime | Engagement messages stopped for this person: `stop-after-ignored` messages in a row were ignored | Nothing to fix; the person can opt back in |
| `MAIN-ENGAGE-0003` | runtime | AI copy was refused by the copy rules, was over the tenant's `copy.limit`, or failed; the template text was used | Nothing to fix; raise `copy.limit` or fix the copy rules if you want AI copy |
| `MAIN-ENGAGE-0004` | runtime | Guardian control refused: the caller isn't the child's guardian, or the change loosens a limit (403) | Ask the child's guardian to make the change |
| `MAIN-ENGAGE-0005` | runtime | Not sent to a children's audience: the copy has streak-loss or guilt pressure, or comparative social proof | Remove streak-loss, guilt pressure and comparative social proof from the copy |
| `MAIN-ENGAGE-0006` | runtime | `urgent: true` on an engagement message refused; engagement never skips quiet hours (400) | Remove `urgent: true` |
| `MAIN-ENGAGE-0007` | runtime | A social proof placeholder had no declared query, or its result was hidden or empty; the text without proof was used | Declare a query for the placeholder, or accept the text without proof |
| `MAIN-ENGAGE-0008` | runtime | A guardrail value in `main.yaml` is invalid (start fails), or a pushed value loosens one (refused, last good kept) | Fix the guardrail value in `main.yaml` |
| `MAIN-ENGAGE-0010` | check | Confirmshaming in unsubscribe or opt-out copy | Write neutral unsubscribe copy |
| `MAIN-ENGAGE-0011` | check | Social proof not backed by data: a literal count or name ("1,240 people finished today") instead of a `{{proof.*}}` placeholder | Use a `{{proof.*}}` placeholder backed by a declared query |
| `MAIN-ENGAGE-0012` | check | Engagement messages are sent (`journey`, `ladder`, `engagement.send` or an engagement category) with no `notify.engagement.max-per-week` | Set `notify.engagement.max-per-week` |
| `MAIN-ENGAGE-0013` | check | A children's audience is possible with no `notify.engagement.guardian` settings | Set `notify.engagement.guardian` |
| `MAIN-ENGAGE-0014` | check | Copy that can reach a children's audience has streak-loss or guilt pressure, or comparative social proof | Remove streak-loss, guilt pressure and comparative social proof from that copy |

## SEARCH

Spec `search`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SEARCH-0001` | runtime | The index isn't declared under `search.indexes` (400) | Declare the index and its fields under `search.indexes` |
| `MAIN-SEARCH-0002` | runtime | The access filter for this searcher can't be expressed in the engine's query; refused, never post-filtered (403) | Use an access driver that lists for this resource type, or store the fields the filter needs in the index |
| `MAIN-SEARCH-0003` | runtime | `search.query` without `for`, outside `tenant.system` (400) | Pass the searcher as `for` |
| `MAIN-SEARCH-0004` | runtime | The engine is unreachable or the query passed `timeout` (503, retryable); index changes stay queued | Retry; restore the engine; changes apply when it is back |
| `MAIN-SEARCH-0005` | runtime | A filter, sort or facet names a field not declared for it, or a reserved field (`_tenant`, `_region`, `_v`) (400) | Declare the field as `filter`, `sort` or `facet`; never filter on reserved fields |
| `MAIN-SEARCH-0006` | runtime | No index endpoint for the tenant's region; the change stays queued and is sent nowhere else (logged) | Add an endpoint for that region under the driver's `regions` |
| `MAIN-SEARCH-0007` | runtime | A cursor would go past `max-depth` (400) | Refine the query or filters |
| `MAIN-SEARCH-0008` | runtime | `q` is longer than 512 characters, or a filter value has the wrong type for its field (400) | Shorten the query or send a value of the field's type |
| `MAIN-SEARCH-0009` | runtime | An index change failed after its last retry and is in the dead-letter queue; counted in `search.status` | Read the dead-letter entry, fix the cause and run `mainsdk search reindex` |
| `MAIN-SEARCH-0010` | check | An index's access filter needs a field the index doesn't store, or the access driver's profile can't list for that resource type | Add the field to the index, or use an access driver that lists |
| `MAIN-SEARCH-0011` | check | A field labelled `sensitive`, `phi`, `child` or `secret` is indexed without `index: allow` on the field | Remove the field from the index, or add `index: allow` with a reason |
| `MAIN-SEARCH-0012` | check | An engine driver has no endpoint for a region the app stores data in | Add an endpoint per region under the driver's `regions` |
| `MAIN-SEARCH-0013` | check | An engine SDK (`typesense`, `meilisearch`, `algoliasearch`) imported outside a driver | Call `search` instead |
| `MAIN-SEARCH-0014` | check | `search.query` without `for` in a file outside `tenancy.system.allow` | Pass the searcher as `for` |

## FEED

Spec `feeds`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-FEED-0001` | runtime | Follow, request or profile read refused: one side blocks the other (403) | None; the block stands until it is removed |
| `MAIN-FEED-0002` | runtime | A follow or item across tenants (403) | Keep follows and items inside one tenant |
| `MAIN-FEED-0003` | runtime | The item doesn't exist, is removed, or isn't visible to the viewer (404) | Use an id the viewer may see |
| `MAIN-FEED-0004` | runtime | Edit or remove by someone who isn't the author and whom `access` doesn't allow `feeds.remove` (403) | Let the author act, or grant `feeds.remove` to moderators |
| `MAIN-FEED-0005` | runtime | Following more than `max-following` (422) | Unfollow someone first |
| `MAIN-FEED-0006` | runtime | A fan-out batch failed after its last retry and is in the dead-letter queue; affected timelines are served by merging at read time (logged) | Fix the cause and retry the dead-letter entry |
| `MAIN-FEED-0007` | runtime | The ranker threw, returned an entry without a reason, or passed `ranked.budget`; the chronological timeline was served (logged) | Fix the ranker so it returns a reason for every entry within the budget |
| `MAIN-FEED-0008` | runtime | A pushed value loosens `page.end`, `page.pause_after`, `trending.min-count` or `max-following`; refused, last good kept | Push only tighter values, or change `main.yaml` in a reviewed deploy |
| `MAIN-FEED-0010` | check | A feed with no end: `page.end: false`, `page.pause_after` off or above 500, or a client that requests the next page with no user action | Keep `page.end: true`, set `pause_after` to 500 or less, and load more only on a person's action |
| `MAIN-FEED-0011` | check | `timeline.order: ranked`, or a `feeds.ranker`, in an app with a children's audience and no chronological-only rule for it | Serve children the chronological timeline |
| `MAIN-FEED-0012` | check | `trending.min-count` below 10, or a trending key built from a labelled field | Raise `min-count` to 10 or more; build keys from tags or item ids |
| `MAIN-FEED-0013` | check | `fanout.threshold` above 100,000, `timeline.max` above 5,000 or `max-following` above 50,000 | Lower the value to its ceiling |

## MOD

Spec `moderate`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-MOD-0001` | runtime | Report refused: the person passed `reports.per-user`, or already reported this target (429) | None; the person can report again later |
| `MAIN-MOD-0002` | runtime | Unknown reason, or the target doesn't exist in the current tenant (400) | Use a reason from `moderate.reasons` and a target in this tenant |
| `MAIN-MOD-0003` | runtime | The caller lacks `moderate.review`, or the case is assigned to someone else (403) | Grant `moderate.review`, or assign the case to the caller |
| `MAIN-MOD-0004` | runtime | Appeal refused: outside `appeals.window`, already appealed, or the decision isn't the caller's to appeal (409) | None; one appeal per decision, by its author, within the window |
| `MAIN-MOD-0005` | runtime | An appeal resolved by the reviewer who made the decision (403) | Assign the appeal to another reviewer |
| `MAIN-MOD-0006` | runtime | A pre-screen driver is unreachable or failed; the content stays held and is retried, then queued for people after `hold.max` (logged) | Restore the driver; held content clears when it is back |
| `MAIN-MOD-0007` | runtime | Known-abuse hash match: the content is blocked and never published; logged with the list name, never the content | Follow your legal duty through the `onMatch` handler |
| `MAIN-MOD-0008` | runtime | A decision other than `none` without a ground or facts (400) | Name the terms clause or law and the facts |
| `MAIN-MOD-0009` | runtime | Held, blocked or removed content read through `files.url`, `media.url` or `feeds` (403) | Wait for the pre-screen to clear it |
| `MAIN-MOD-0010` | check | A hash list or hash database in the repository or the build output | Delete it; fetch lists from your provider at run time under `prescreen.hash.lists` |
| `MAIN-MOD-0011` | check | User content is published (`feeds.publish`, `share` of an upload, a public file URL) with no `moderate` section | Add a `moderate` section with `hold` and `reports` |
| `MAIN-MOD-0012` | check | No `moderate.reports.per-user`, or one above 100 an hour | Set `reports.per-user` to 100 an hour or less |
| `MAIN-MOD-0013` | check | `appeals` off or a window under 6 months, or `remove-on` lists a label other than `spam` | Keep appeals on for at least 6 months; send other labels to `hold-on` |

## MEDIA

Spec `media`.

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-MEDIA-0001` | runtime | The source file isn't `ready`, doesn't exist in the current tenant, or the caller may not read it (404) | Complete the upload first and pass a file the caller may read |
| `MAIN-MEDIA-0002` | runtime | The source is over a limit: duration, size or pixels; the job failed in the probe step, nothing was transformed (422) | Ask for a smaller file, or raise the limit up to its ceiling |
| `MAIN-MEDIA-0003` | runtime | The source's magic bytes don't match a supported type, or don't match its declared type (415) | Upload a supported image, video or audio file |
| `MAIN-MEDIA-0004` | runtime | The driver doesn't support this kind or format, such as an image on `mux` (501) | Use a driver whose profile lists the kind |
| `MAIN-MEDIA-0005` | runtime | A transform step failed after its last retry: the provider failed, or `ffmpeg` exited non-zero; the job is `failed` | Read the step's error and start a new job |
| `MAIN-MEDIA-0006` | runtime | No job with that id in the current tenant (404) | Use a job id from this tenant |
| `MAIN-MEDIA-0007` | runtime, check | `media.keep` lists `gps`, `location` or an unknown metadata name (start fails; check finding) | Keep only `icc` and `orientation-tag` |
| `MAIN-MEDIA-0010` | check, runtime | The `ffmpeg` driver in production | Use `getqr`, `mux` or `cloudinary` in `main.env.prod.yaml` |
| `MAIN-MEDIA-0011` | check | An image or video library (`sharp`, `jimp`, `fluent-ffmpeg`, `@ffmpeg/ffmpeg`, `ffmpeg-static`) or an `ffmpeg` program run outside a driver | Call `media` instead |
| `MAIN-MEDIA-0012` | check | `delivery: provider` on `mux` or `cloudinary`: outputs served by the provider, outside `files` (named gap) | Use `delivery: files`, or accept the gap |
| `MAIN-MEDIA-0013` | check | A media kind used with no limits, or a limit above its ceiling: `max-duration` over 4h, `max-size` over 10GB, `max-pixels` over 100MP | Declare limits for each kind within the ceilings |


## METER and LIMIT

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-METER-0001` | runtime, check | A metric not declared under `meter.metrics` | Declare the metric with its unit and aggregate |
| `MAIN-METER-0002` | runtime | A value that isn't finite, is negative for `sum` or `count`, or an `at` outside its window (400) | Record a valid value at a time within 35 days back and 1 hour ahead |
| `MAIN-METER-0003` | runtime | The buffer was full while the store was down, so the oldest records were dropped (logged with the count) | Restore the store; raise `meter.buffer` if outages are long |
| `MAIN-METER-0004` | runtime | Usage was served from the last known value because the store can't be reached (`stale: true`) | Restore the store |
| `MAIN-METER-0005` | runtime | An export to `pay` failed for a tenant and hour; retried with the same identifier | Check the pay driver and the provider's meter name |
| `MAIN-METER-0006` | check, runtime | `meter` is invalid: unknown aggregate, `unique` without `of`, an exported metric with no provider meter, `keep` under 35 days, or `local` in production | Fix the key the finding names |
| `MAIN-METER-0007` | runtime | No meter store for the tenant's region; the record is refused, never written elsewhere | Add a store for that region under `meter` |
| `MAIN-METER-0008` | runtime | A usage window is invalid: `from` after `to`, not on whole hours, or over 13 months (400) | Pass a calendar window or a whole-hour range |
| `MAIN-LIMIT-0001` | runtime | The rate limit for this policy and key is reached (429, with `Retry-After`) | Wait `Retry-After` seconds |
| `MAIN-LIMIT-0002` | runtime | The quota for this window is used up (429, with `Retry-After`) | Wait for the window to reset |
| `MAIN-LIMIT-0003` | runtime | No concurrency slot came free within `wait` (429, with `Retry-After`) | Retry later, or raise `max` on purpose |
| `MAIN-LIMIT-0004` | runtime | The lock wasn't acquired within `wait` (409, retryable) | Retry, or pass a longer `wait` |
| `MAIN-LIMIT-0005` | runtime | Refused because the store is down and the policy's `on-store-down` is `closed` (503, with `Retry-After`) | Restore the limits store |
| `MAIN-LIMIT-0006` | runtime, check | A policy, quota or concurrency name not declared under `limits` | Declare it under `limits` |
| `MAIN-LIMIT-0007` | check, runtime | A policy is invalid: `max` under 1, a bad `per`, an unknown algorithm or key, `burst` under `max`, or a `ttl` over 1 hour | Fix the key the finding names |
| `MAIN-LIMIT-0008` | check, runtime | The `local` limits driver in production without `single-instance: true` | Use `redis`, or set `single-instance: true` for one instance |
| `MAIN-LIMIT-0009` | runtime | A lock or concurrency lease expired before the work finished; its signal was aborted (logged) | Raise `ttl`, or check the store's health |

## COMPUTE

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-COMPUTE-0001` | runtime, check | A target not declared under `compute.targets` | Declare the target |
| `MAIN-COMPUTE-0002` | runtime | Refused before submit: the worst case (price times timeout) passes `maxCost`, a `compute.limits` ceiling or the agent's budget (402) | Lower the timeout, raise `maxCost` on purpose, or wait for the period |
| `MAIN-COMPUTE-0003` | runtime | The running cost reached `maxCost`; the job was cancelled at the provider | Raise `maxCost` on purpose, or make the job cheaper |
| `MAIN-COMPUTE-0004` | runtime | No heartbeat for three periods; the job is `lost` and was cancelled at the provider | Check the provider; start the job again |
| `MAIN-COMPUTE-0005` | runtime | The provider failed submit, status or cancel after retries (502) | Check the provider's status and your account |
| `MAIN-COMPUTE-0006` | runtime | The job passed its timeout and was cancelled | Raise the target's timeout on purpose |
| `MAIN-COMPUTE-0007` | runtime | Labelled input to a target outside the tenant's region (403) | Use a target in the tenant's region |
| `MAIN-COMPUTE-0008` | check | A target isn't pinned: no model version, app or endpoint id | Pin the version or id |
| `MAIN-COMPUTE-0009` | runtime | An output is too large or of a type not in `compute.results.types` (413 or 415) | Raise the limit or allow the type on purpose |
| `MAIN-COMPUTE-0010` | runtime | No job with that id in the current tenant (404) | Use an id from `compute.list` |
| `MAIN-COMPUTE-0011` | runtime | Cancel of a finished job (409) | Nothing to cancel |
| `MAIN-COMPUTE-0012` | runtime | A provider callback failed verification (400) | Check the provider's webhook secret |
| `MAIN-COMPUTE-0013` | check, runtime | A target has no `price` or `timeout`, or a timeout over 24h | Set both, with a timeout of 24h or less |

## MAIL and CAL

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-MAIL-0001` | runtime | No mailbox or route matches the recipient; dropped and logged | Add a mailbox or a `mail.on` route for the address |
| `MAIN-MAIL-0002` | runtime | A raw message can't be parsed, or passes the size, part or nesting limit | Nothing to fix in code; the record keeps the headers |
| `MAIN-MAIL-0003` | runtime | Rejected by `mail.reject`: DMARC failed or a virus was found | Nothing to fix; the sender's domain or file is the problem |
| `MAIN-MAIL-0004` | runtime | An attachment was refused by size, type or scan; a placeholder is kept | Raise `mail.attachments.max-size` on purpose if needed |
| `MAIN-MAIL-0005` | runtime | A reply address token is invalid, expired or for another tenant; threaded by headers instead | Nothing to fix; raise `mail.reply.expires` if replies come late |
| `MAIN-MAIL-0006` | runtime | An IMAP or JMAP server failed or timed out during a pull; retried | Check the provider's status |
| `MAIN-MAIL-0007` | runtime | A mailbox's cursor can't be continued; it is pulled again from the start | Nothing to do |
| `MAIN-MAIL-0008` | check, runtime | `mail` is invalid: a mailbox outside `domains`, an inbound provider with no secret, or a pull provider not under `connections.providers` | Fix the key the finding names |
| `MAIN-MAIL-0009` | check | Code listens for SMTP: a server on port 25, 465 or 587, or an SMTP server package | Receive mail through `mail` and a provider webhook (ADR-0052) |
| `MAIN-MAIL-0010` | runtime | An `Authentication-Results` header from a server other than the provider's was ignored | Nothing to fix; it may be a spoofing attempt |
| `MAIN-CAL-0001` | runtime, check | An event is invalid: end before start, an unknown IANA zone, a date-time mixed with a date, or no title (400) | Fix the field the error names |
| `MAIN-CAL-0002` | runtime, check | A recurrence rule is invalid or uses an unsupported part (400) | Use the parts the calendar page lists |
| `MAIN-CAL-0003` | runtime | A window passes `max-window`, or the expansion passes `max-occurrences` (400) | Ask for a shorter window |
| `MAIN-CAL-0004` | runtime | iCalendar text can't be parsed at a line; that event is skipped | Fix the line, or accept the skip |
| `MAIN-CAL-0005` | runtime | An import is larger than `calendar.max-import` (413) | Split the file, or raise the limit on purpose |
| `MAIN-CAL-0006` | runtime | No calendar with that id in the tenant, or access denies it (404 or 403) | Use a calendar the person may read |
| `MAIN-CAL-0007` | runtime | An edit scope that doesn't fit the event or occurrence (400) | Use `all` for a single event, and an occurrence from the series |
| `MAIN-CAL-0008` | runtime | The event changed since `version` was read (409) | Read it again and reapply the change |

## INCIDENT

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-INCIDENT-0001` | runtime | Switched off by an incident control (503, retryable) | Wait for the control to clear, or clear it with `mainsdk incident off` |
| `MAIN-INCIDENT-0002` | runtime | The app is read-only during an incident (503 for requests; thrown by writes) | Wait for `mainsdk incident off read-only`, or use a path in `read-only-allow` |
| `MAIN-INCIDENT-0003` | runtime | The session was issued before the incident sign-out cut-off (401) | Sign in again |
| `MAIN-INCIDENT-0004` | check, runtime | A kill target is unknown or malformed, or names a protected interface | Name an existing action, route, interface, driver or agent |
| `MAIN-INCIDENT-0005` | runtime | `mainsdk incident` could not write: no `--reason`, or the config driver has no `write` | Pass `--reason`, or use a config driver with `write` and set the key by hand |
| `MAIN-INCIDENT-0006` | runtime | `incident.set` refused: no `incident.manage`, or no fresh phishing-resistant session (403) | Grant `incident.manage`, and sign in again with a passkey |
| `MAIN-INCIDENT-0007` | check, runtime | A control with no `expires` has been in force for 24 hours (warn) | Add `--expires` to the control, or clear it when it is no longer needed |

## PROTECT

Codes from protected files (spec `protect-taps`, planned for 0.4). Runtime codes are also listed in [errors](https://mainsdk.dev/docs/reference/errors).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-PROTECT-0001` | runtime | Licence denied by the policy (403); the TAPS error type is in `taps`, the failed condition only in the decision log | Check the asset's policy with `protect.policy.check()`; read the decision log record |
| `MAIN-PROTECT-0002` | runtime | Time-locked: before `releaseAt`, or the release round isn't available yet (403, `retryAfter`) | Wait for the release time |
| `MAIN-PROTECT-0003` | runtime | The asset, the person's access, the link or the licence was revoked, or the asset was erased (403; 410 when erased) | Protect and share a new copy if access should continue |
| `MAIN-PROTECT-0004` | runtime | No such asset, version, link, delegation or approval in this tenant (404, same body either way) | Check the id and the current tenant |
| `MAIN-PROTECT-0005` | runtime | A person must approve first; the problem carries `approvalId` and `retryAfter` (403) | Approve with `protect.approvals.update()`, or pass `approvals` |
| `MAIN-PROTECT-0006` | runtime | The approver declined, or the approval timed out (403) | Ask again, or `extend` the approval before it times out |
| `MAIN-PROTECT-0007` | runtime | Delegation invalid: wider than the person, expired, revoked, used up or deeper than 3 (403) | Create a delegation within what the person may do |
| `MAIN-PROTECT-0008` | runtime | A query or play budget, or a delegation's `maxUses`, is used up (429) | Wait for the budget to reset, or raise it in the policy |
| `MAIN-PROTECT-0009` | runtime | A session, print, device or recipient-verification rate limit was passed (429, `retryAfter`) | Retry after `retryAfter` |
| `MAIN-PROTECT-0010` | runtime | Suite not supported: `TAPS-CS3` is refused until it is registered; no post-quantum protection is claimed (400) | Use `TAPS-CS1` or `TAPS-CS2` |
| `MAIN-PROTECT-0011` | runtime | Threshold release not reached: fewer than `t` parties have released (403, retryable) | Wait for more parties; see `protect.threshold.get()` |
| `MAIN-PROTECT-0012` | runtime | The asset or its label is locked down, suspended or quarantined (423) | Lift the lockdown with approvals, or replace a quarantined file |
| `MAIN-PROTECT-0013` | runtime | Step-up required: the policy needs a fresher or stronger sign-in, listed in `requirements` (401) | Sign in again with the listed method |
| `MAIN-PROTECT-0014` | runtime | The TAPS Node, a threshold party or the sealing service is unreachable (503, retryable); nothing opens without a licence | Check `protect.url` and the network; retry |
| `MAIN-PROTECT-0015` | runtime | Invalid input: unknown policy key, label `pan`, a zone id that isn't opaque, `t` outside 2 to n, `releaseAt` in the past, an unknown party or an undeclared agent (400) | Fix the call; `protect.policy.lint()` names policy issues |
| `MAIN-PROTECT-0016` | runtime | The file's version is superseded; `location` names the current version (409) | Open the current version |
| `MAIN-PROTECT-0017` | runtime | The ledger is stale or the registration is pending (503, `retryAfter`) | Retry after `retryAfter` |
| `MAIN-PROTECT-0018` | runtime | A query answer was refused by the output controls: the whole document, a verbatim span over the limit, or a denied field (422) | Ask a narrower question |
| `MAIN-PROTECT-0019` | runtime | The driver's profile lacks the capability the call needs, such as `query` or `web3` (501) | Use a driver whose profile has it |
| `MAIN-PROTECT-0020` | check | A `@taps/*` package is imported outside `@mainsdk/protect-taps` | Call `protect` instead |
| `MAIN-PROTECT-0021` | check | The `protect` section is invalid: unknown key or driver, no `url` or `share-domain` for a `taps-*` driver, no `key`, `suite: TAPS-CS3`, `renew` outside 5s to 5m, or a threshold party without a URL | Fix the section the finding names |
| `MAIN-PROTECT-0022` | check | A production build resolves `@taps/*` from a folder (`link:` or `file:`) instead of the registry | Install `@taps/*` from npm once published; until then don't ship `@mainsdk/protect-taps` to prod |

## LICENCE

Codes from app licensing (spec `licence`, built, and spec `licence-cloud`, planned for 0.6). The check reports 0011, 0012 and 0014; the rest are raised at runtime. Not TAPS licences: those are `MAIN-PROTECT`. Runtime codes are also listed in [errors](https://mainsdk.dev/docs/reference/errors).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-LICENCE-0001` | runtime | The feature needs a licence and the licence in scope doesn't include it, or isn't valid or in grace (403, with `licence: { feature, status, reason }`) | Run `mainsdk licence show`; install a licence that includes the feature with `licence.update()` |
| `MAIN-LICENCE-0002` | runtime | A licensed limit is reached: the licence's ceiling is the one that binds (403, with `licence: { name, limit }`) | Ask the vendor for a licence with a higher limit, or set `licence.over: report` |
| `MAIN-LICENCE-0003` | runtime | A licensed limit was passed with `over: report`; the call was allowed. Logged once per limit and scope per UTC day | Renew the licence with a higher limit before the true-up |
| `MAIN-LICENCE-0004` | runtime | A licence, CA certificate or revocation list doesn't verify; `reason` is `signature`, `chain`, `format`, `subject`, `binding`, `rollback` or `test-root` (400 from `licence.update()`) | Ask the issuer for a new file; check `licence.roots`, `licence.product`, `project` and the environment |
| `MAIN-LICENCE-0005` | runtime | The licence is in grace: expiring, revoked or suspended. Warned once a day with `graceEndsAt` | Renew the licence, or let refresh fetch the renewed one |
| `MAIN-LICENCE-0006` | runtime | The licence is expired, revoked or suspended past its grace; licensed features are off | Install a renewed licence; the rest of the app keeps serving |
| `MAIN-LICENCE-0007` | runtime | No licence, or not yet valid; licensed features are off | Set `licence.file` or `licence.token`, or install one with `licence.update()` |
| `MAIN-LICENCE-0008` | runtime | Refresh failed, or the newest revocation list is past its `nextUpdate`; the current state is kept. Logged once per outage | Check `licence.url` and the network; nothing is blocked meanwhile |
| `MAIN-LICENCE-0009` | runtime | An older licence (`seq`) or revocation list (`number`), or a different list with the same number, was refused; the newer one stays | Install the newest licence; report repeated attempts as a security event |
| `MAIN-LICENCE-0010` | runtime | The clock is behind the high-water time by more than `licence.skew`; the high-water time is used for licence decisions | Fix the machine's clock (NTP) |
| `MAIN-LICENCE-0011` | check, runtime | The `licence` section is invalid: unknown driver or key, no `product`, no `roots` or more than 4, a root that isn't `ed25519:` and 32 bytes, both `file` and `token`, `http` without `url`, `refresh` outside 5m to 7d, `skew` outside 0 to 1h. The app doesn't start | Fix the key the finding names |
| `MAIN-LICENCE-0012` | check | A feature name passed to `licence.has` or `licence.require` isn't under `licence.features` | Add the name to `licence.features`, or fix the typo |
| `MAIN-LICENCE-0013` | runtime | A feature name that isn't under `licence.features`: `has` returns false and logs once; `require` throws (500) | Add the name to `licence.features`, or fix the typo |
| `MAIN-LICENCE-0014` | check | The test root is pinned under `licence.roots` in production | Pin your own root from MainSDK Cloud or your issuer |
| `MAIN-LICENCE-0015` | runtime | The machine id can't be read: no `/etc/machine-id` for `machine: os`, or `MAINSDK_MACHINE_ID` unset for `machine: env` (500) | Set `licence.machine: env` and `MAINSDK_MACHINE_ID`, or use a licence with no machine binding |
| `MAIN-LICENCE-0016` | runtime | `licence.update` refused: no `licence.manage` permission, an app licence outside `tenant.system()` in a tenanted app, or a tenant licence for another tenant (403) | Grant `licence.manage`, or install the licence in its own tenant |
| `MAIN-LICENCE-0020` | runtime | Cloud: the caller lacks the role, or a write has no passkey session from the last 5 minutes (403) | Sign in again with a passkey; ask an owner for `licences:admin` |
| `MAIN-LICENCE-0021` | runtime | Cloud: the licence claims are invalid; `errors` names each field (422) | Fix the fields; see spec `licence-cloud` for the issuing rules |
| `MAIN-LICENCE-0022` | runtime | Cloud: no such root, CA or licence in this account (404) | Check the id and the account |
| `MAIN-LICENCE-0023` | runtime | Cloud: the change conflicts with the state, such as renewing a revoked licence or issuing from a retired CA (409) | Read the resource's status; issue a new licence or use the active CA |
| `MAIN-LICENCE-0024` | runtime | Cloud: a second approver is needed; the problem carries `approvalId` | Ask another admin to approve |
| `MAIN-LICENCE-0025` | runtime | Cloud: more than 60 refresh calls for one licence in an hour (429, `Retry-After`) | Wait for `Retry-After`; keep `licence.refresh` at 5m or more |
| `MAIN-LICENCE-0026` | runtime | Cloud: the key service can't sign; nothing was issued or changed (503, retryable) | Retry; apps keep verifying offline meanwhile |

## Modern AI: multimodal, images, video, agent loop, routing and local models

Specs `ai-multimodal`, `ai-image`, `ai-run` and `ai-routing` (planned for 0.4), `ai-local` (planned for 0.5) and `ai-video` (planned for 0.6).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-AI-0040` | runtime | A prompt part is invalid: unknown type, no file, a `mime` that doesn't match the bytes, `pages` out of order, or `video` given as bytes (400); nothing is sent | Pass a `FileRef` of a supported type, and let MainSDK read the type |
| `MAIN-AI-0041` | runtime | A prompt part is over a limit: size, pixels, pages, duration, frames or `max-parts` (413); nothing is sent | Send a smaller file or fewer parts, or raise `ai.input` on purpose |
| `MAIN-AI-0042` | runtime | The model or driver doesn't accept this kind of part (501) | Use a model whose driver declares the modality |
| `MAIN-AI-0043` | runtime | A labelled image, audio or scan would go to a driver whose data use or region doesn't meet the rules, and media can't be redacted (403) | Use a driver that meets `ai.data` with an endpoint in the tenant's region |
| `MAIN-AI-0044` | check | A prompt part is built from request body bytes or a `data:` URL instead of a `FileRef` | Upload through `files` and pass the `FileRef` |
| `MAIN-AI-0045` | runtime | An image option is invalid: size, count, mask size or an empty prompt (400); nothing is sent | Pass a size from `ai.image.sizes`, a count within `max-count` and a mask the image's size |
| `MAIN-AI-0046` | runtime | The image driver doesn't offer this operation (501) | Use a driver that declares edit, inpaint or variations |
| `MAIN-AI-0047` | runtime | The provider refused the image prompt under its content policy (422) | Change the prompt; the reason code is in the audit event |
| `MAIN-AI-0048` | runtime | A content credential couldn't be signed and `ai.credentials.required` is true (500) | Restore the signer key in `keys`, or check its certificate |
| `MAIN-AI-0049` | check | AI images are generated with no `ai.credentials.signer` (warning) | Set a signer so images carry C2PA content credentials |
| `MAIN-AI-0050` | runtime | A video option is invalid: seconds, size, prompt or references (400); nothing is sent | Pass values within `ai.video` and the driver's limits |
| `MAIN-AI-0051` | runtime | No video job with that id in the current tenant (404) | Use a job id returned in this tenant |
| `MAIN-AI-0052` | runtime | The provider failed or refused the video job; the job is `failed` | Read the reason code; change the prompt or retry later |
| `MAIN-AI-0053` | runtime | A video job lost its provider heartbeat and was cancelled at the provider | Retry the job; check the provider's status |
| `MAIN-AI-0060` | runtime | An `ai.run` option is invalid: goal and messages, `maxSteps`, budget, timeout or agent (400); nothing is sent | Pass one of `goal` or `messages`, a declared agent and limits within range |
| `MAIN-AI-0061` | runtime | The run stopped at `maxSteps` (`stop: "max-steps"`) | Raise `maxSteps` on purpose, or narrow the goal or tools |
| `MAIN-AI-0062` | runtime | The run stopped at its budget (`stop: "budget"`) | Raise the run's budget within the agent's, or use a cheaper model |
| `MAIN-AI-0063` | runtime | The run stopped at its timeout (`stop: "timeout"`) | Raise `timeout`, or use `durable: true` for long runs |
| `MAIN-AI-0064` | runtime | The model asked for a tool that isn't in the run's `tools`; the step was refused | Nothing, if the tool shouldn't be reachable; otherwise add its action to `tools` |
| `MAIN-AI-0065` | runtime | A handoff or delegation names an agent not declared for it (400) | Add the agent under `agents.<name>.run.handoff` or `delegate` |
| `MAIN-AI-0066` | runtime | A delegation asked for more than the parent's remaining budget or the declared share (402) | Delegate less, or raise the declared share on purpose |
| `MAIN-AI-0067` | runtime | No run with that id in the current tenant, or it isn't waiting (404 or 409) | Use a run id from this tenant that is waiting |
| `MAIN-AI-0068` | check | A tool passed to `ai.run` isn't `ai.tool()` of a declared action | Declare the tool with `action()` and pass `ai.tool(action)` |
| `MAIN-AI-0070` | check | A route lists a model not in `ai.models`, unpriced, or whose driver doesn't meet `ai.data` | Remove the model from the route, or fix its allowlist, price or driver |
| `MAIN-AI-0071` | runtime | Every model in the route failed with a retryable error (502) | Check the providers' status; add another model that meets the rules |
| `MAIN-AI-0072` | runtime | A fallback model was skipped because its endpoint is outside the tenant's region for labelled data (logged) | Add a model with an endpoint in that region to the route |
| `MAIN-AI-0073` | runtime, check | A response cache entry is invalid: tenant scope with labelled data, a TTL over 7 days, or `local` kv in production | Use `scope: user`, a shorter TTL, or a production kv driver |
| `MAIN-AI-0074` | runtime | A batch option is invalid: no items, too many, duplicate ids, or no batch API (400) | Fix the items, or use a driver with a batch API |
| `MAIN-AI-0075` | runtime | No batch with that id in the current tenant (404) | Use a batch id returned in this tenant |
| `MAIN-AI-0076` | runtime | The store that counts `ai.limits` did not answer and the call is under a spend limit, so it is refused (503, retryable) | Check `runtime.store`; calls under no limit keep running |
| `MAIN-AI-0078` | runtime | The local model server is unreachable (503, retryable) | Start it, for example `ollama serve` |
| `MAIN-AI-0079` | check | A local endpoint uses plain HTTP off the machine, isn't in `network.allow`, or a remote one lacks `data` or `region` | Use HTTPS, allow the host, and declare its data use and region |
| `MAIN-AI-0080` | runtime | The model isn't on the local server (404) | Fetch it, for example `ollama pull llama3.2:3b` |

## DOC

Documents (spec `documents`, planned for 0.4).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-DOC-0001` | runtime | The file isn't a supported document type, or its bytes don't match its declared type (415) | Send a PDF, DOCX, XLSX, PPTX, PNG, JPEG or TIFF |
| `MAIN-DOC-0002` | runtime | Over a limit: size, pages, ZIP expansion, or more pages than `inline-pages` for `parse` (413) | Use `documents.start` for large files, or raise `documents.limits` on purpose |
| `MAIN-DOC-0003` | runtime | Pages have no text layer and OCR is off or no OCR driver is set (422) | Set `documents.ocr.driver`, or pass `ocr: "auto"` |
| `MAIN-DOC-0004` | runtime | The documents driver doesn't offer this (501) | Use a driver that declares the capability |
| `MAIN-DOC-0005` | runtime | The PDF is encrypted or password protected (422) | Ask for an unprotected copy |
| `MAIN-DOC-0006` | runtime | Active content was found and not run (logged) | Nothing to do; review `findings` if the file is suspicious |
| `MAIN-DOC-0007` | runtime | A render layout or template is invalid (400) | Fix the element, template name, font or value the error names |
| `MAIN-DOC-0008` | runtime | A labelled document would go to a driver outside the tenant's region or not meeting the data rules (403) | Add an endpoint for the region, or use a driver that meets `ai.data` |
| `MAIN-DOC-0009` | runtime | No document job with that id in the current tenant (404) | Use a job id returned in this tenant |
| `MAIN-DOC-0010` | check | A PDF or Office library used outside a driver | Call `documents` instead |
| `MAIN-DOC-0011` | check | A locale with a complex script renders with the `builtin` renderer, which doesn't shape it | Use the `gotenberg` or `docraptor` render driver |

## SANDBOX

Running untrusted code (spec `sandbox`, planned for 0.4).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-SANDBOX-0001` | runtime | A sandbox option is invalid (400); nothing ran | Fix the language, code, limit or path the error names |
| `MAIN-SANDBOX-0002` | runtime | The code reached its time limit and was killed | Raise `timeout` within `sandbox.limits`, or make the code faster |
| `MAIN-SANDBOX-0003` | runtime | The code passed its memory limit and was killed | Raise `memory` within `sandbox.limits` |
| `MAIN-SANDBOX-0004` | runtime | Output or files passed their limit; output is cut | Write less, or raise `limits.output` on purpose |
| `MAIN-SANDBOX-0005` | runtime | A host isn't in `sandbox.network` and `network.allow`, or the code tried a host it wasn't given (403) | Add the host to both lists on purpose |
| `MAIN-SANDBOX-0006` | runtime | The sandbox driver is unavailable (503, retryable) | Retry; check the provider's status |
| `MAIN-SANDBOX-0007` | runtime | The run would pass, or reached, its cost ceiling (402) | Lower `timeout`, or raise `maxCost` on purpose |
| `MAIN-SANDBOX-0008` | runtime | No sandbox session with that id in the current tenant, or it was closed (404) | Create a new session |
| `MAIN-SANDBOX-0009` | runtime | A secret reference isn't under `sandbox.secrets` (403) | List the secret under `sandbox.secrets` on purpose |
| `MAIN-SANDBOX-0010` | check | `eval`, `new Function` or `node:vm` runs a value that isn't a string literal | Run the code with `sandbox.run` |
| `MAIN-SANDBOX-0011` | check, runtime | The `local` sandbox driver in production | Use `vercel-sandbox` or `e2b` in production |
| `MAIN-SANDBOX-0012` | check | `sandbox.network` lists a host that `network.allow` doesn't | Add the host to `network.allow`, or remove it from `sandbox.network` |

## I18N

Languages and formats (spec `i18n`, planned for 0.4).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-I18N-0001` | check | A key is missing from a required locale | Add the message, or run `mainsdk i18n draft` and approve it |
| `MAIN-I18N-0002` | check | A locale isn't supported by the target runtime's `Intl` | Use a runtime with full ICU, or drop the locale |
| `MAIN-I18N-0003` | check | A message has a syntax error, or its variables differ from the default locale's | Fix the message so it parses and uses the same variables |
| `MAIN-I18N-0004` | runtime | A message was missing and a fallback was used (logged once per key and locale) | Add the message in that locale |
| `MAIN-I18N-0005` | runtime | `t()` was called without a value its message needs (logged) | Pass every variable the message uses |
| `MAIN-I18N-0006` | check | `review: none`: AI drafts reach users without review (warning) | Set `review: person` unless you accept the risk on purpose |
| `MAIN-I18N-0007` | runtime | An agent or service tried to approve a translation draft (403) | A person approves drafts |
| `MAIN-I18N-0008` | check | `i18n.negotiate` lists `ip` or an unknown source | Use `choice`, `account` and `header` |
| `MAIN-I18N-0009` | runtime | No message for the key in any locale; production never shows the key (logged) | Add the message to the default locale |

## VOICE

Realtime voice agents (spec `ai-voice`, planned for 0.6).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-VOICE-0001` | runtime | A voice session option is invalid (400) | Pass one of `model` or `pipeline`, a known voice and a declared agent |
| `MAIN-VOICE-0002` | runtime | The voice driver doesn't offer this (501) | Use a driver that declares the capability |
| `MAIN-VOICE-0003` | runtime | Recording refused: recording off, or no consent from the caller (403) | Set `record: consent` and record the caller's consent first |
| `MAIN-VOICE-0004` | runtime | The session reached its duration or cost limit and ended after a closing line | Raise `maxDuration` or the limit on purpose |
| `MAIN-VOICE-0005` | runtime | A telephony or room webhook failed signature verification (401) | Check the webhook secret; investigate the sender |
| `MAIN-VOICE-0010` | check | A phone number routes to a voice session with no disclosure | Set `ai.voice.disclosure` or the session's `disclosure` |
| `MAIN-VOICE-0011` | check | A voice, telephony or room driver's hosts aren't in `network.allow` | Allow the hosts on purpose |

## EVAL

Evals for AI features (spec `ai-evals`, planned for 0.5).

| Code | Raised by | Meaning | Fix |
| --- | --- | --- | --- |
| `MAIN-EVAL-0001` | CLI | An eval set is invalid, or its judge model is the feature's own model (exit 2) | Fix the set as the error names; pick a different judge model |
| `MAIN-EVAL-0002` | CLI, check | A feature's score is below its threshold or regressed from the baseline (exit 1) | Fix the prompt or model, or update the baseline on purpose |
| `MAIN-EVAL-0003` | check | A prompt or model changed since the last eval result, or a feature with an eval set has none | Run `mainsdk eval` |
| `MAIN-EVAL-0004` | check | An eval set or exported sample holds personal data | Replace the value with a synthetic one |
| `MAIN-EVAL-0005` | CLI | The eval run would pass `--max-cost` (exit 2) | Raise `--max-cost` on purpose, or run one feature |

## Codes from other specs

These check, CLI or mixed codes are defined in a spec's Codes table and listed here automatically. The spec holds the full cause, and `npx mainsdk explain <code>` gives the fix.

<!-- codes:begin (generated by tools/build_codes.py; edit the spec's Codes table, never these rows) -->
| Code | Raised by | Meaning | Spec |
| --- | --- | --- | --- |
| `MAIN-ADOPT-0002` | CLI | The stack is not Next.js or Node; `adopt` stops and changes nothing | `migration-skill` |
| `MAIN-ADOPT-0003` | CLI | A route changes or reads data and never checks who is calling (access gap) | `migration-skill` |
| `MAIN-ADOPT-0004` | CLI | The browser queries the database directly, so row rules are the only protection (data gap) | `migration-skill` |
| `MAIN-ADOPT-0006` | CLI | There is nothing to migrate in the module, or it is not a module (`secrets`, `auth`, `data`, `other`) | `migration-skill` |
| `MAIN-ADOPT-0007` | CLI | A call still sits behind an interface's `raw()` escape hatch; listed by `finish` with file and line | `migration-skill` |
| `MAIN-ADOPT-0008` | CLI | The working tree has uncommitted changes; a module step starts from a clean tree | `migration-skill` |
| `MAIN-ADOPT-0009` | CLI | No sign-in or checkout flow was found to lock, or a named flow points at text that is not there | `migration-skill` |
| `MAIN-ADOPT-0010` | CLI | A module step is still open, or `verify` ran with none open | `migration-skill` |
| `MAIN-ADOPT-0012` | CLI | The folder is not a git repository, or the module branch could not be created | `migration-skill` |
| `MAIN-ADOPT-0013` | CLI, MCP | The name is not an adopt tool, or not an agent adopt writes a skill for | `migration-skill` |
| `MAIN-ADVISOR-0001` | check | `advisor` is invalid: `may` empty or naming an undeclared rule, `run: auto` with a rule in `may` that has an `app` or `people` step, an unknown wake signal, or no `agents.advisor` | `advisor` |
| `MAIN-API-0020` | check | CORS lets any origin call the API (`*`, `origin: true` or a bare `cors()`) | `main27-baseline` |
| `MAIN-ASSURE-0001` | check, CLI | An exception in `main.yaml` for a `<standard>:<control>` rule has no reason, approver or expiry (400). Fix: give every exception a reason, an approver and an expiry date | `assure` |
| `MAIN-ASSURE-0002` | CLI | No standard, pack, format or run file by that name, or no standard was chosen (400). Fix: use one of the standards the message lists | `assure` |
| `MAIN-ASSURE-0004` | CLI | An evidence bundle does not verify: the signature fails, a section is missing or no public key was given (exit 1). Fix: do not rely on the bundle; run assure again on the repo | `assure` |
| `MAIN-ASSURE-0005` | CLI | No mainsdk record could be read from the URL (exit 2). Fix: check the address serves `/.well-known/mainsdk.json` over HTTPS | `assure` |
| `MAIN-ASSURE-0006` | cloud | Assigning a failure to a teammate from the console needs Teams (402, with an `upgrade` link). Fix: upgrade to Teams, or assign the failure in your own tracker | `assure` |
| `MAIN-ASSURE-0007` | cloud | The failure to assign is not in the account's latest run (404). Fix: assign a ref from the latest run | `assure` |
| `MAIN-ASSURE-0008` | CLI | `--fix` could not open its pull request: git has no origin, `gh` is not signed in, or the tree is not clean (502). Fix: fix the git setup, or apply the listed changes by hand | `assure` |
| `MAIN-AUTH-0012` | check, runtime | Check (high): an admin role in `auth.admin.roles` can sign in with a method phishing can defeat while `auth.admin.require` isn't `phishing-resistant`. Runtime: `auth.raw()` was called (logged). Fix: set `auth.admin.require: phishing-resistant`, or remove the weak methods from `auth.methods` | `auth` |
| `MAIN-AUTH-0053` | check | `auth.risk.step-up` names a method that isn't on, or only methods weaker than the ones it steps up from | `auth` |
| `MAIN-AUTH-0055` | check | The identity-provider driver named by `auth.driver` does not offer a method (its capability profile says `false`). High, and it fails the check, when the app uses the method; a warning otherwise | `auth` |
| `MAIN-AUTH-0081` | client | The person cancelled the passkey prompt, or the browser refused it (`NotAllowedError` or `AbortError`); not retryable without a new gesture | `auth-client` |
| `MAIN-AUTH-0082` | client | The auth routes answered 404 or HTML: the adapter isn't mounted or `base` is wrong | `auth-client` |
| `MAIN-AUTH-0083` | client | This browser has no WebAuthn, or no conditional mediation when `mediation: "conditional"` was asked for | `auth-client` |
| `MAIN-AUTH-0090` | check | A password is stored with a bare hash instead of passkeys or the auth interface | `main27-baseline` |
| `MAIN-AUTH-0091` | check | The sign-in uses the OAuth implicit flow (`response_type=token`) | `main27-baseline` |
| `MAIN-AUTH-0101` | check | `assetlinks.json` or `apple-app-site-association` does not match the signing identity declared in `targets.mobile.signing`, so app links and passkeys would not bind to the app | `mobile-auth` |
| `MAIN-BACKUP-0001` | check | The provider's best RPO (PITR step, else snapshot gap) is above `backup.rpo`. Fix: turn on PITR or raise `backup.rpo` | `backup-restore-evidence` |
| `MAIN-BACKUP-0002` | check | The provider keeps restore points for less than `backup.keep`. Fix: raise the provider retention or lower `backup.keep` | `backup-restore-evidence` |
| `MAIN-BACKUP-0003` | check | The last drill took longer than `backup.rto`. Fix: shrink the restore path or raise `backup.rto` to what a drill measured | `backup-restore-evidence` |
| `MAIN-BACKUP-0006` | check | `backup.rpo`, `backup.rto`, `backup.keep` or `backup.drill` is missing or not a duration. Fix: set durations such as `15m`, `1h`, `35d` | `backup-restore-evidence` |
| `MAIN-BASE-0001` | pack build | A point update removes a rule an earlier point had; point updates only add checks | `main27-baseline` |
| `MAIN-BASE-0002` | contract check | An interface outside the phase 1 stable core is marked `stable` without two drivers, a passing conformance suite and three production users | `main27-baseline` |
| `MAIN-BOT-0005` | check | A `bots` value is invalid: a door path that matches no route, `challenge` not below `deny`, an unknown country code or CIDR, or an unknown key | `bot-defence` |
| `MAIN-CACHE-0011` | check | `cache.scope: public` on an action whose `risk` isn't `read`, whose method isn't `GET`, or that declares a `permission` | `http-cache` |
| `MAIN-CACHE-0012` | check | An app-scoped `cache.wrap` or `cache.set` holds the result of a `db` read, which is owner or tenant data | `http-cache` |
| `MAIN-CANARY-0003` | check | `canaries` is set but nothing is planted in that env, or a model named under `records` doesn't exist | `exfiltration-guard` |
| `MAIN-CONFIG-0030` | check | A `main.yaml` key isn't in the schema; `fix` names the nearest key, so `maxSize` suggests `max-size` and `payments` suggests `pay` | `cohesion` |
| `MAIN-CONFIG-0031` | check | A store-backed feature that must survive a restart (idempotency, meter, workflow leases) runs on a store whose profile says `durable: false` | `cohesion` |
| `MAIN-CONFIG-0032` | check | A `main.yaml` value fails the schema: wrong type, not the one allowed constant, or not matching the key's pattern; the finding names the path. Fix: write the value in the form the finding shows | `cohesion` |
| `MAIN-CONFIG-0033` | check | The `main.yaml` schema isn't built, so keys and values weren't validated (low, a warning). Fix: run `pnpm build` in `packages/sdk`, which runs `tools/build_main_schema.py` | `cohesion` |
| `MAIN-CONNECTOR-0001` | check, runtime | A connector's settings are not usable: an unknown driver, a missing https `instance` or `url`, or `auth` that is not a `secret:NAME` reference | `connectors` |
| `MAIN-CONTEXT-0001` | check | `context` is invalid: no `group`, a purpose without a description, a field outside the built-in schema or the app's declared extras, `retention` over 90d, a `share-with` app that isn't a mutual peer, or a sink other than `db` or `events` | `context` |
| `MAIN-CONTEXT-0006` | check | Shared context in an app with `users.audience: children`, or a purpose touching a children's audience, without the purpose marked `children: guardian-consent` and spec privacy-lifecycle's age gate in place | `context` |
| `MAIN-CONTRACT-0001` | build | Two error registry entries share a code, or a code does not match `MAIN-<AREA>-<NNNN>`; the message names both entries | `errors` |
| `MAIN-CONTRACT-0004` | CLI | `contract diff --base` found no contract snapshot at the given ref or file | `versioned-contracts` |
| `MAIN-CRYPTO-0013` | check | `crypto.fips` is on but `crypto.module` does not name the validated module, its version and certificate number | `post-quantum` |
| `MAIN-DATA-0040` | check | A statement takes a lock that blocks reads or writes on an existing table while it runs | `db-migrations` |
| `MAIN-DATA-0041` | check | A rename or drop of a column or table that the base ref's code still reads, so the running release breaks during the deploy | `db-migrations` |
| `MAIN-DATA-0042` | check | A new migration for a tool that can't take a lock timeout from the runner doesn't set `lock_timeout` itself | `db-migrations` |
| `MAIN-DATA-0043` | check | An `UPDATE` or `DELETE` with no `WHERE`, or a backfill of a whole table, in a migration; run it as a batched job | `db-migrations` |
| `MAIN-DATA-0044` | CLI | A migration statement timed out waiting for a lock after every retry; nothing after it was applied | `db-migrations` |
| `MAIN-DATA-0045` | CLI | Another instance holds the migration lock; this run waited and found every migration already applied | `db-migrations` |
| `MAIN-DATA-0051` | check | A serverless target connects to Postgres without a pooled endpoint, so each instance opens its own connections | `db-queries` |
| `MAIN-DATA-0055` | check | A `db` call is awaited inside a loop over the results of another query | `db-queries` |
| `MAIN-DATA-0056` | check | A `db.timeout`, `db.pool` or `db.slow` value is invalid: a request timeout over 30s, a pool `max` over 100, or `slow` not under the request timeout | `db-queries` |
| `MAIN-DATA-0061` | check | `jobs.run`, `events.publish`, `webhooks.emit`, `email.send` or `notify` called inside a `db.transaction` callback on the module instead of on `tx` | `db-writes` |
| `MAIN-DATA-0062` | check | A unique index on a model with trash doesn't exclude trashed rows, so a deleted record blocks a new one | `db-writes` |
| `MAIN-DATA-0065` | check | `trash` on a model with a labelled field is longer than that label's retention, or `trash` isn't a valid duration | `db-writes` |
| `MAIN-DATA-0070` | check | SQL is built from strings (a template literal or concatenation) | `main27-baseline` |
| `MAIN-DATA-0071` | check | A connection string uses a shared admin account such as `root`, `admin` or `postgres` | `main27-baseline` |
| `MAIN-DATA-0180` | check | A query filters, sorts or joins on the raw value of a vaulted field, which the database holds only as a token. Fix: use the driver's blind index, or look the person up by an id | `residency-vaults` |
| `MAIN-DATA-0181` | check | The vault driver's declared countries (`vault.countries`) miss a country in `markets`. Fix: pick a driver that supports it or drop the market | `residency-vaults` |
| `MAIN-DATA-0182` | check | A vaulted field has a label a vault cannot hold, such as `pan`. Fix: use a canonical label; never store card numbers | `residency-vaults` |
| `MAIN-DEBUG-0001` | cloud | A debug session, replay, hosted triage or inbox forwarding needs Teams (402, with an `upgrade` link). The app is not changed. Fix: move the project to Teams | `debug-loop` |
| `MAIN-DEBUG-0002` | cloud | A debug session must name exactly one of user, request or release, and a duration such as `30m` or `1h` (422). Fix: give one target and a valid `--for` | `debug-loop` |
| `MAIN-DEBUG-0003` | cloud | No debug session has that id in this project (404). Fix: list the sessions of the project and pass one of their ids | `debug-loop` |
| `MAIN-DEBUG-0004` | cloud, CLI | The trace has no recorded request, or the project has no replay handler for its action (404). Fix: check the trace id, or export the action from `mainsdk.replay.ts` | `debug-loop` |
| `MAIN-DEBUG-0005` | cloud | Triage needs the GitHub app connected for this project (409). Fix: run `mainsdk triage connect github` | `debug-loop` |
| `MAIN-DEBUG-0006` | cloud | Triage has not opened that issue or pull request (404). Fix: run `mainsdk triage run` first, then pass the number it printed | `debug-loop` |
| `MAIN-DEBUG-0007` | cloud | A report, repository or screenshot address is not valid; screenshots must be getqr.id uploads (422). Fix: upload the screenshot through getqr.id and send its link | `debug-loop` |
| `MAIN-DEPLOY-0001` | check, CLI | A schedule can't be deployed: its expression is computed at runtime and the target has no long-running process, or `jobs.schedules` and a `jobs.schedule` call give one name two expressions | `deploy` |
| `MAIN-DEPLOY-0002` | check, CLI | `deploy.regions` names a region outside the app's data regions, or the target can't run in a data region | `deploy` |
| `MAIN-DEPLOY-0003` | CLI | `deploy preflight` found required env var names missing or empty in the build; the names are listed, never a value | `deploy` |
| `MAIN-DEPLOY-0004` | check, CLI | A schedule or region breaks the declared platform plan's limit (for example a cron more often than daily on Vercel Hobby, or two regions on Hobby) | `deploy` |
| `MAIN-DEPLOY-0005` | check, CLI | A Next.js app targets Docker or Fly without `output: "standalone"` in its Next.js config | `deploy` |
| `MAIN-DEPLOY-0010` | check, CLI | A generated block is stale against `main.yaml`, or was edited by hand; the fix is the `deploy config` command that rewrites it | `deploy` |
| `MAIN-DEPLOY-0011` | check, CLI | A secret would reach the image or the build log: a Dockerfile `ARG` or `ENV` named after a `secret:` reference, or `.dockerignore` not excluding `.env*` | `deploy` |
| `MAIN-DEPLOY-0012` | CLI | `deploy diff --fail-on widen` found a change that widens what the target environment may do: a new allowed host, a new secret, a new region or a looser grant | `deploy` |
| `MAIN-DEPS-0001` | check | A direct dependency isn't on its registry: possibly a name a model made up, and anyone may register it (high) | `dependency-check` |
| `MAIN-DEPS-0002` | check | A direct dependency's first version is younger than `deps.min-age` (high) | `dependency-check` |
| `MAIN-DEPS-0003` | check | A direct dependency's name is one edit away from a popular package it isn't (medium) | `dependency-check` |
| `MAIN-DEPS-0004` | check | A resolved package has a known advisory; at or above `advisories.fail-on` it fails, below it warns | `dependency-check` |
| `MAIN-DEPS-0005` | check | A package's licence is denied, outside a non-empty allow list, or unknown with `unknown: fail` | `dependency-check` |
| `MAIN-DEPS-0006` | check | A package runs an install script (`preinstall`, `install`, `postinstall`) and isn't in `deps.scripts.allow` (warning) | `dependency-check` |
| `MAIN-DEPS-0007` | check | Advisories and package ages weren't checked because the network was unavailable (info, or a failure with `offline: fail`) | `dependency-check` |
| `MAIN-DEPS-0008` | check | An advisory ignore entry has expired, or has no `until` or `reason` | `dependency-check` |
| `MAIN-DEV-0001` | cli | A fix, recipe or MCP write would widen a grant (`network.allow`, `permissions`, `access` roles, `agents` budgets, an exception); it is planned but not applied without `--allow-widen` from a person, and `next` returns `ask-person` | `agent-experience` |
| `MAIN-DEV-0002` | cli | A recipe or fix doesn't apply cleanly to the current code; `fix` names the file and the first conflicting hunk | `agent-experience` |
| `MAIN-DEV-0003` | cli | An MCP write tool was called on a server started without `--allow-write` | `agent-experience` |
| `MAIN-DEV-0004` | check | The generated types are older than `main.yaml`; run `mainsdk dev`, `sync` or `next` | `agent-experience` |
| `MAIN-DEV-0005` | cli | An unknown command, subcommand, flag or preset; `fix` names the nearest match and `mainsdk help` (exit 2) | `design-language` |
| `MAIN-DEV-0006` | cli | `init` found an existing `main.yaml`; `fix` names `--new-id` and `--force` (exit 2) | `design-language` |
| `MAIN-DEV-0007` | cli | A command that reads `main.yaml` ran in a folder with none; `fix` names `mainsdk init` (exit 2) | `design-language` |
| `MAIN-DEV-0008` | cli | `mainsdk dev` refused to start under `env: prod`; `fix` names the dev environment (exit 2) | `design-language` |
| `MAIN-DEV-0009` | cli | `mainsdk dev <entry>` found no file at the entry, or no `fetch` function as its default export; `fix` names what to export (exit 2) | `design-language` |
| `MAIN-DEVICE-0001` | check | `mainsdk/device` is imported but `main.yaml` has no `device` section; device calls throw it at runtime (500) | `device-identity` |
| `MAIN-DEVICE-0002` | check | `main.yaml` declares `device` but no code imports `mainsdk/device`; nothing is served | `device-identity` |
| `MAIN-DEVICE-0003` | check, runtime | The `device` section is invalid: unknown driver or key, a value out of range, `authority` written as a literal instead of `secret:NAME`, more than 2 authority keys, an `oidc` entry without `issuer`, `audience` or `tenant`, or a proof not in the list. The app doesn't start | `device-identity` |
| `MAIN-DEVICE-0004` | check, runtime | `driver: taps` without `@mainsdk/device-taps` installed, or an `@taps/*` import outside that package | `device-identity` |
| `MAIN-DEVICE-0016` | check | Production runs with no `device.authority`, or with the development authority key that `mainsdk dev` generated | `device-identity` |
| `MAIN-DEVICE-0022` | check, runtime | The `device.attest` section is invalid: `require: hardware` or `environment`, `formats`, `roots`, `builds` or `min-security-level` with `driver: local`; an unknown format; a root or builds file that doesn't exist; `every` or `grace` out of range | `device-attest` |
| `MAIN-DEVICE-0038` | check, runtime | The `device.telemetry` section is invalid: a stream name that doesn't match the pattern, a size or rate out of range, `redact` that isn't a list of paths | `device-telemetry` |
| `MAIN-DEVICE-0039` | check | `device.telemetry.on` names a stream that isn't declared, or a declared stream has no handler | `device-telemetry` |
| `MAIN-DEVICE-0050` | check, runtime | The `device.policy` section is invalid: stages that don't increase or don't end at 100%, `hold` outside 5m to 7d, `rollback-if` outside 1% to 50%, `approve` above 2, or an unknown `tighten` direction | `device-policy` |
| `MAIN-DEVICE-0066` | check | A `client.commands.handle` handler passes its input to a process or an evaluator (`system.run`, `child_process`, `eval`, `new Function`, `vm`, dynamic `import()`) | `device-commands` |
| `MAIN-DEVICE-0067` | check | A command type is unsafe to declare: its input schema has an unbounded string field named `command`, `cmd`, `script`, `code`, `shell`, `exec` or `args`; it uses the `mainsdk.` prefix; it is `destructive` with `approve: 0`; or it is `reaction: true` while `destructive`, or `write` without `undo` | `device-commands` |
| `MAIN-DEVICE-0071` | check, runtime | The `device.commands` section is invalid: a value out of range, or `ttl` over `max-ttl` | `device-commands` |
| `MAIN-DEVICE-0082` | check, runtime | The `device.heartbeat` section is invalid: `every` outside 15s to 1h, `late-after` outside 2 to 10, `silent-after` outside 3 to 100 or not above `late-after` | `device-health` |
| `MAIN-DEVICE-0092` | check, runtime | The `device.taps` section is invalid: a `did:web` without a key-history log, `TAPS-CS3`, an unknown delegation target, or a status `refresh` outside 5m to 24h | `device-taps` |
| `MAIN-DEVID-0001` | cloud, sdk | The account in a developer record is not a GitHub login (400). Fix: run `mainsdk login` again to sign in with GitHub | `verified-developers` |
| `MAIN-DEVID-0002` | cloud | The identity provider answered something this version does not understand (502). Fix: try the verification again later | `verified-developers` |
| `MAIN-DEVID-0003` | cloud | The organisation could not be verified because a check could not run (502). Fix: try the verification again later | `verified-developers` |
| `MAIN-DEVID-0004` | cloud | Certified partner is not yet available to buy (409). Fix: choose Trusted publisher, which is earned, or ask to be told when partners open | `verified-developers` |
| `MAIN-DEVID-0005` | cloud | The appeal has nothing to appeal or does not say what was fixed (409). Fix: file an appeal after a drop and say what you fixed | `verified-developers` |
| `MAIN-DOCS-0001` | CLI | `mainsdk docs` ran in an install with no bundled docs; a source checkout builds them with `build_docs.py --bundle` | `docs-versions` |
| `MAIN-DOCS-0002` | CLI | The installed version has no docs page with that name; the fix lists the closest pages | `docs-versions` |
| `MAIN-DRIVER-0001` | usage | A listing, claim, revocation feed or driver path is not valid (400). Fix: run `mainsdk driver publish` to build the listing from a conformance run | `driver-marketplace` |
| `MAIN-DRIVER-0002` | usage | The driver fails every contract version it claims, so there is nothing to list (422). Fix: fix the failing conformance tests with `mainsdk driver test`, then publish again | `driver-marketplace` |
| `MAIN-DRIVER-0006` | check | The app uses a driver the marketplace revoked; the warning names the driver and the advisory. The app keeps serving (fail static). Fix: move to another driver or a fixed release | `driver-marketplace` |
| `MAIN-DRIVER-0007` | usage | The driver is not listed (404). Fix: search with `mainsdk driver search <interface>` | `driver-marketplace` |
| `MAIN-E2E-0007` | check, runtime | The post-quantum hybrid suite was asked for before spec post-quantum is in place (501) | `e2e` |
| `MAIN-E2E-0008` | check | A field in `e2e.fields` is read by server-side search, moderation or analytics, which skip it; `e2e.accept` silences it | `e2e` |
| `MAIN-E2E-0009` | check, runtime | The driver is not for production: `local` in production, `mls` without `@mainsdk/e2e-mls`, or a driver whose `production` is false in production (500) | `e2e` |
| `MAIN-EJECT-0001` | eject | No recipe exists for the interface, or for the driver `main.yaml` names; the fix lists the drivers that have one | `eject` |
| `MAIN-EJECT-0002` | eject | No file imports the interface, so there is nothing to eject | `eject` |
| `MAIN-EMAIL-0006` | check, runtime | The `email` section of `main.yaml` is invalid, names no driver, or lacks a value a call needs, such as `email.unsubscribe-url` (400) | `email-notify` |
| `MAIN-EMAIL-0009` | check | Email tracks opens or clicks; reported as a warning | `email-notify` |
| `MAIN-EMAIL-0010` | check | A real email driver in production with no `email.webhook`, so bounces and complaints never reach the suppression list; reported as a warning | `email-notify` |
| `MAIN-EMAIL-0011` | check | The source calls `email.marketing` with no `unsubscribe` | `email-notify` |
| `MAIN-EXFIL-0006` | check | An `exfiltration` value is invalid: a receiver not in `network.allow` or any driver's hosts, an unknown label, a bulk limit below 1, or an unknown key | `exfiltration-guard` |
| `MAIN-FILES-0016` | check | The configured `files` driver cannot meet a guarantee, named by its id in the message (a warning, so the run still passes): `uploadthing` declares `upload-single-use` and `data-region` | `files-share` |
| `MAIN-FLEET-0006` | check, runtime | A build includes a package the fleet policy blocks (403). Fix: update or remove the package | `fleet-control` |
| `MAIN-GEO-0002` | check | `geo.of` is used in access code: in a file under a folder named `access`, `policy`, `policies` or `auth`, or its call, or a variable it was put in, sits in the arguments of `auth.require`, `access.check`, `access.decide` or `policy.decide` | `geo` |
| `MAIN-GEO-0005` | check, runtime | A `geo` key is wrong (runtime: 400): an unknown `driver` or `edge`, a provider driver with no `key: secret:NAME`, or `geo.of` used with no declared edge | `geo` |
| `MAIN-GO-0001` | check | golangci-lint reported an issue in a Go project (the finding names the linter, the file and the line). Fix: Fix the code the linter points at, as its message says; never add a `nolint` | `go-sdk` |
| `MAIN-GO-0002` | check | gosec reported a weakness in Go code. Fix: Fix the weakness gosec names; never add a `#nosec` | `go-sdk` |
| `MAIN-GO-0003` | check | govulncheck found a known vulnerability the program can reach through a function it calls (the finding names the OSV id, the module and the fixed version). Fix: Upgrade the module to the fixed version the finding names | `go-sdk` |
| `MAIN-GO-0004` | check | A Go check tool isn't installed, so its gate did not run (a warning, never a pass). Fix: Install it: `go install golang.org/x/vuln/cmd/govulncheck@latest`, and gosec and golangci-lint the same way | `go-sdk` |
| `MAIN-GUARD-0001` | check, runtime | A rule with a `people` step is `run: auto`; or a source asked for an auto run of one | `reaction-safety` |
| `MAIN-GUARD-0002` | check, runtime | A step is on the never-a-reaction list; refused from every source | `reaction-safety` |
| `MAIN-GUARD-0005` | check, runtime | An app-class step was triggered by one tenant's signals without `tenant-quorum` tenants and without approval; it was turned into an approval | `reaction-safety` |
| `MAIN-GUARD-0008` | check | A `guard` value is outside its range, or `min-prefix` is set wider than /16 or /48 | `reaction-safety` |
| `MAIN-IDENT-0001` | check, runtime | An identity request asks for a claim beyond what it needs, gives no purpose, or an age gate is not a whole age from 1 to 99 | `digital-ids` |
| `MAIN-IDENT-0005` | check, runtime | `digital-ids` in `main.yaml` is not valid: no wallet listed, an unknown wallet, or a request with no claims or no need | `digital-ids` |
| `MAIN-KEYS-0021` | check | `keys.pkcs11` is incomplete or its PIN is a literal (400). Fix: set `keys.pkcs11.pin` to `secret:HSM_PIN` and name the module and `keys.key` | `hsm-keys` |
| `MAIN-KEYS-0023` | check | A FIPS claim has no level or CMVP certificate, or is made on a driver that is not an HSM (400). Fix: state `keys.fips` from the vendor's certificate, or remove it | `hsm-keys` |
| `MAIN-LOG-0020` | check | A password, key, token or authorization value is passed to a log call | `main27-baseline` |
| `MAIN-MIGRATE-0001` | CLI | `mainsdk migrate <from>` applied the codemods but `mainsdk check` or a locked flow failed, so the branch is kept and no pull request opens (400). Fix: Fix the finding on the `migrate/<from>` branch, then open the pull request by hand | `migration-guides` |
| `MAIN-MIGRATE-0002` | CLI | `mainsdk migrate <from>` could not start or finish: not a git repository, a working tree with changes, or a `git` or `gh` step failed (400 or 502). Fix: Commit or move your changes and run it in the app repository, or use `--report-only` to read the report without a branch | `migration-guides` |
| `MAIN-MOBILE-0001` | check | `targets.mobile` in `main.yaml` is not valid (missing or malformed bundle ID, permission, signing identity or name); the finding names the key | `expo-adapter` |
| `MAIN-MOBILE-0003` | check | Source writes a token, secret, password or refresh value to AsyncStorage or MMKV, which are not encrypted | `expo-adapter` |
| `MAIN-MOBILE-0004` | check | App source calls `fetch`, `XMLHttpRequest` or `axios` directly instead of the SDK client, so the network allowlist, audit and attestation do not apply | `expo-adapter` |
| `MAIN-NEIGHBOUR-0001` | check | `neighbours` is invalid: `host: on` with `apps.registry: off`, a `share` or `listen` name that isn't a signal name or glob, `priority` outside 1 to 9, `max-posture: lockdown`, `max-for` over 1h, or `asks.accept` with a verb outside the three | `neighbours` |
| `MAIN-NEIGHBOUR-0005` | check | `asks.accept` lists `posture.raise` or `load.shed` but `apps.link` is off or `apps.peers` is empty | `neighbours` |
| `MAIN-NET-0020` | check, runtime | A `tcp://`, `udp://` or `unix://` entry, a `network.listen` entry or a `network.udp` entry doesn't parse: no port, a port outside 1 to 65535, a range written high to low, an unknown protocol, IPv6 without brackets, a bind address that isn't an IP, or a unix path that is relative, not normalized, holds `..` or `*` (other than a final `/*` in `allow`) or is over 104 bytes (fails at load; names the entry) | `net-sockets` |
| `MAIN-NET-0025` | check, runtime | TLS verification turned off outside `dev` and `test`, or a minimum version below TLS 1.2 anywhere | `net-sockets` |
| `MAIN-NET-0028` | check, runtime | A raw socket on a target that can't run it: `net.listen`, `net.listenUnix` or `net.udp` on a serverless or edge target, `net.connect` or `net.connectUnix` on an edge target, or a unix socket call on Windows; names the target, the runtime and the fallback | `net-sockets` |
| `MAIN-NET-0029` | check | A `net.listen` port or UDP bind found in the code that `network.listen` doesn't declare | `net-sockets` |
| `MAIN-NET-0030` | check | A `network.listen` entry binds beyond loopback with no `from` (a warning, not a failure) | `net-sockets` |
| `MAIN-NET-0040` | check | The code calls a non-local host over plain `http://` | `main27-baseline` |
| `MAIN-NET-0041` | check | TLS below 1.2 is forced, or certificate checks are turned off | `main27-baseline` |
| `MAIN-NET-0050` | check | `Info.plist` or `app.json` turns off App Transport Security with `NSAllowsArbitraryLoads`, so the app can load plain http | `expo-adapter` |
| `MAIN-NET-0051` | check | `AndroidManifest.xml` or `app.json` allows cleartext traffic (`usesCleartextTraffic`), so the app can send data over plain http | `expo-adapter` |
| `MAIN-NORMS-0001` | check | A `norms` value is invalid: `every` outside 1m to 15m, an unknown `season`, metric or time zone, `learn` over 28d, `min-samples` under 10, `z` under 2, or `margin` outside 0% to 200% | `norms` |
| `MAIN-NOTIFY-0001` | check, runtime | The `notify` section of `main.yaml` is invalid or a provider block is missing (400) | `email-notify` |
| `MAIN-NOTIFY-0006` | check, runtime | A quiet-hours time is not `HH:MM` or a time zone is not a known zone (400) | `email-notify` |
| `MAIN-OPS-0001` | check | An `ops` value is invalid: `write: auto`, a port outside 1024 to 65535, or `http: on` in production without `ops.agent` declared under `agents` | `ops-mcp` |
| `MAIN-OPS-0005` | cli | `mainsdk ops mcp` found no running app with an ops socket for the given folder or project | `ops-mcp` |
| `MAIN-PACK-0001` | config | A pack is not a `pack@1` bundle: missing id or version, or not valid JSON (400). Fix: rebuild it with `mainsdk pack build` | `industry-packs` |
| `MAIN-PACK-0002` | config | A pack would weaken the baseline: a permit policy, or a label rule that is not a refusal, a requirement or a restriction (400). Fix: packs add forbid policies and tightening rules only | `industry-packs` |
| `MAIN-PACK-0003` | config | A pack defines a label that is not canonical (400). Fix: use personal, sensitive, child, phi or pan | `industry-packs` |
| `MAIN-PACK-0004` | config | A standard has no pinned version, or a control has no SCF mapping (400). Fix: write `standard: pci-dss-4.0.1` and `scf: <id>` | `industry-packs` |
| `MAIN-PACK-0005` | config | A pack policy is not valid Cedar (400). Fix: fix the syntax and rebuild the pack | `industry-packs` |
| `MAIN-PACK-0006` | config | A control names a check that does not exist (400). Fix: use one of the check kinds the pack reference lists | `industry-packs` |
| `MAIN-PACK-0008` | check | `industry` names a pack that is not in the verified cache or in `packs/`, or whose bundle does not verify; the last good version stays in force (400). Fix: run `mainsdk sync` to fetch the signed pack | `industry-packs` |
| `MAIN-PACK-0009` | usage | `assure --standard` or `pack build` was run without its value (400). Fix: pass the standard or the folder | `industry-packs` |
| `MAIN-PACK-0010` | check | A field has a label the pack refuses in any store, such as `pan` in the payments pack (400). Fix: the mapped fix, for payments "use hosted fields" | `industry-packs` |
| `MAIN-PACK-0011` | check | A field the pack needs encrypted is not, so reads cannot be audited (400). Fix: set `db.fields.<field>.encrypt` to true | `industry-packs` |
| `MAIN-PACK-0012` | check | A driver that can hold regulated data declares no signed business associate agreement (400). Fix: use a driver whose manifest declares `baa: true` | `industry-packs` |
| `MAIN-PACK-0013` | check | The pack does not allow the driver in use for an interface (400). Fix: use one of the drivers the pack lists | `industry-packs` |
| `MAIN-PACK-0014` | check | Audit logs are kept for less time than the pack needs (400). Fix: set `logging.retention` | `industry-packs` |
| `MAIN-PACK-0015` | check | Admin access does not require multi-factor authentication (400). Fix: set `auth.admin.require` to phishing-resistant | `industry-packs` |
| `MAIN-PACK-0020` | check | A third-party provider is used that the pack's provider register does not list (400). Fix: add it to `compliance.providers` with its reason and owner | `industry-packs` |
| `MAIN-PACK-0022` | check | Change control is not switched on, so releases are not reviewed (400). Fix: set `compliance.change-control` and require a reviewed pull request | `industry-packs` |
| `MAIN-PACK-0023` | check | No passing restore drill inside the pack's window (400). Fix: run `mainsdk drill restore --to scratch` and keep the signed evidence | `industry-packs` |
| `MAIN-PERM-0008` | check | A disk value is invalid: a `max` or `reserve` that isn't a size or a percentage, a `reserve` over 50%, or `temp` set to a folder | `app-permissions` |
| `MAIN-PLAT-0001` | check | A Tauri or Electron release build is unsigned, or its updater has no public key or uses plain http | `platforms` |
| `MAIN-PLAT-0002` | check | A browser extension is not Manifest V3, asks for a broad host permission with no declared need under `platforms.extension.needs`, or its CSP allows eval or remote scripts | `platforms` |
| `MAIN-PLAY-0001` | playground, cli | The tests file is not a list of tests with a name, an input and `allow` or `deny` as the expectation (400). Fix: write each test as `{ "name": "…", "input": { … }, "expect": "allow" }` | `policy-playground` |
| `MAIN-PLAY-0002` | playground, cli | The replay holds more recorded decisions than the playground replays at once (413). Fix: replay a smaller sample of at most 100000 decisions | `policy-playground` |
| `MAIN-PLAY-0003` | playground | The link does not hold a playground state this version can read (400). Fix: ask for the link again, or paste the policy and request in by hand | `policy-playground` |
| `MAIN-PLAY-0004` | playground | The state is too large to share, or getqr.id did not make the link (502 or 413). Fix: share without the replay, or share again when the connection is back | `policy-playground` |
| `MAIN-PLAY-0005` | cli | `mainsdk policy play` could not read a policy, request, entities, tests or replay file (400). Fix: pass a folder with policies and a request file that holds JSON | `policy-playground` |
| `MAIN-PLAYBOOK-0001` | check | A `playbooks` or `posture` value is invalid: an unknown step, preset, trust level or severity; a `for` over 24h; a `count` window under 1m; a `cooldown` under 1m; or a parameter that doesn't fit the step | `playbooks` |
| `MAIN-PLAYBOOK-0002` | check | A step's interface isn't configured or built (for example `bots.block` with no `bots` section, `workers.scale` before parallelism); `fix` names the spec | `playbooks` |
| `MAIN-PLAYBOOK-0003` | check, runtime | A step would not tighten: `limits.tighten` to a larger amount or another period, a flag that isn't defined, an agent that isn't declared; refused before anything runs | `playbooks` |
| `MAIN-PLAYBOOK-0010` | check | An action used as a step isn't marked `reaction: true`, or its `undo` names an action that doesn't exist | `playbooks` |
| `MAIN-POLICY-0007` | test | `mainsdk test policy --recorded` was given a folder that does not exist | `policy` |
| `MAIN-PRIV-0001` | check | A data label in use has no retention rule (`privacy.retention.<label>`) | `privacy-lifecycle` |
| `MAIN-PRIV-0004` | check | The `privacy` section of main.yaml does not read: an unknown key, a retention that is not a duration, or retries out of range | `privacy-lifecycle` |
| `MAIN-PYTHON-0001` | check | Ruff reported a rule in a Python project (the finding names the rule, the file and the line). Fix: Fix the code the rule points at, as its message says; never ignore the rule | `python-sdk` |
| `MAIN-PYTHON-0002` | check | Pyright reported a type error in strict mode. Fix: Fix the type error where it is reported; never add a `type: ignore` | `python-sdk` |
| `MAIN-PYTHON-0003` | check | Bandit reported a weakness in Python code. Fix: Fix the weakness Bandit names, using its more_info page | `python-sdk` |
| `MAIN-PYTHON-0004` | check | A Python check tool isn't installed, so its gate did not run (a warning, never a pass). Fix: Install it: `pip install ruff pyright bandit` | `python-sdk` |
| `MAIN-REPORT-0005` | check | `logging.errors` is invalid: `sentry` with no `dsn`, a `dsn` host not in `network.allow`, a `sample` outside 0 to 1, or `alert.spike.over` under 1 | `error-reporting` |
| `MAIN-RUST-0001` | check | Clippy reported a lint in a Rust project (the finding names the lint, the file and the line). Fix: Fix the code the lint points at, as its message says; never allow the lint away | `rust-sdk` |
| `MAIN-RUST-0002` | check | cargo-audit found a dependency with a known advisory (the finding names the advisory and the crate). Fix: Upgrade the crate to the patched version the advisory names | `rust-sdk` |
| `MAIN-RUST-0003` | check | cargo-deny refused a dependency for its licence, a ban, its source or an advisory. Fix: Replace the dependency, or change `deny.toml` with the owner's approval | `rust-sdk` |
| `MAIN-RUST-0004` | check | A Rust check tool isn't installed, so its gate did not run (a warning, never a pass). Fix: Install it: `rustup component add clippy`, `cargo install cargo-audit cargo-deny` | `rust-sdk` |
| `MAIN-SANITIZE-0003` | check | A raw HTML sink (`dangerouslySetInnerHTML`, `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`, `v-html`, `{@html}`) is fed by a value that doesn't come from `sanitize.html` or `sanitize.markdown` | `sanitize` |
| `MAIN-SECRET-0011` | check, runtime | A secret is past its `max-age` (fails `mainsdk check --env <env>` when the driver can report ages; a Main 27 exception covers it) | `secret-rotation` |
| `MAIN-SECRET-0014` | check | A `secrets.rotation` value is invalid: a bad duration, `overlap` longer than `max-age`, `generate` under 16 bytes, or a name the app doesn't reference | `secret-rotation` |
| `MAIN-SECURITY-0001` | check | A `security` value is invalid: an unknown severity, sink or detector, or a detector threshold below 2 | `security-events` |
| `MAIN-SECURITY-0002` | check | `to` includes `otel` and a region the app stores data in has no endpoint (no `regions.<r>` and no `telemetry.otel`) | `security-events` |
| `MAIN-SESSION-0001` | check | A token is written to `localStorage` or `sessionStorage` | `main27-baseline` |
| `MAIN-SESSION-0002` | check | A token is issued for longer than seven days | `main27-baseline` |
| `MAIN-SIGNAL-0006` | check | A `signals` value is invalid: `keep` outside 100 to 10,000, a TTL over `ttl.max` or over 24h, or `key` written as a literal instead of `secret:NAME` | `signals` |
| `MAIN-SIGNAL-0007` | check | Production uses a shared broadcast driver (`redis`) and app-scope signals, with no `signals.key`; another app on the same store could forge them | `signals` |
| `MAIN-SIM-0001` | test | An injection or option needs a spec that has no code yet (`attack`, `key`, `approve` without playbooks); the message names the spec | `intelligence-sim` |
| `MAIN-SIM-0002` | test | A sim input is invalid: an unknown app name, a start that is not a time, or a step that is not positive | `intelligence-sim` |
| `MAIN-SUPPLY-0001` | check | A dependency is `*`, `latest`, empty or fetched from a URL or git | `main27-baseline` |
| `MAIN-SYNC-0007` | check | A table under `sync.tables` has no whole-number `version`, or a `conflicts` value that is not `last-writer-wins`, `server-wins` or `action:<name>` | `sync` |
| `MAIN-TEAMS-0001` | cloud | The member's role does not allow this in the organisation (403). Fix: ask an admin of the organisation to change your role or to do it for you | `teams-plan` |
| `MAIN-TEAMS-0002` | cloud | The invite is unknown, used, past its seven days or sent to another email (410). Fix: ask an admin to invite you again | `teams-plan` |
| `MAIN-TEAMS-0003` | cloud | An add-on applies only to the projects of the buyer's own organisation, and none may offer what Fleet is for (403). Fix: buy it for a project your organisation owns | `teams-plan` |
| `MAIN-TEAMS-0004` | cloud | There is no such add-on, or it has no price yet (404 or 409). Fix: choose an add-on that is on sale | `teams-plan` |
| `MAIN-TEAMS-0005` | cloud | An annual invoice needs a purchase order number (400). Fix: pass the purchase order number of your finance team | `teams-plan` |
| `MAIN-TEAMS-0006` | cloud | The sign-in or SCIM request is not allowed: no code, or the person is not provisioned (400 or 403). Fix: assign the person to the MainSDK application in the identity provider | `teams-plan` |
| `MAIN-TEAMS-0007` | cloud | The organisation is not an approved startup (403). Fix: apply for the startup programme and wait for the approval | `teams-plan` |
| `MAIN-TEAMS-0008` | cloud | The evidence built for a project names another project (500). Fix: build the evidence again for the project it is pushed for | `teams-plan` |
| `MAIN-TEAMS-0009` | cloud | The project to transfer is not owned by the organisation giving it away (404). Fix: transfer a project your organisation owns | `teams-plan` |
| `MAIN-UPDATE-0003` | check | The `updates` section is invalid: a version that isn't dotted numbers, a rollout outside 0 to 100, a channel other than `stable` or `beta`, or a deprecation without a `path` and a `sunset` date | `updates` |
| `MAIN-UPDATE-0004` | check | `updates.min-version` is above the `stable` channel's version, so every current client would be blocked | `updates` |
| `MAIN-UPGRADE-0001` | CLI | The git working tree has uncommitted changes, or the folder isn't a git repository; nothing was changed | `upgrade` |
| `MAIN-UPGRADE-0002` | CLI, check | MainSDK packages are at different versions in the workspace; they move only together | `upgrade` |
| `MAIN-UPGRADE-0003` | CLI | A codemod couldn't run (no `typescript` in the app, or a file didn't parse); it is listed as a manual step with file and line | `upgrade` |
| `MAIN-UPGRADE-0004` | CLI | The registry couldn't be reached, so the target version is unknown; nothing was changed | `upgrade` |
| `MAIN-UPGRADE-0005` | CLI, check | A release marked `security` is newer than the installed version | `upgrade` |
| `MAIN-UPGRADE-0006` | CLI | After the upgrade, the install, `sync`, `check` or the app's tests failed; the branch is kept and the pull request is a draft listing the failures | `upgrade` |
| `MAIN-UPGRADE-0010` | tools | A release's API report has a removed or changed export with no upgrade note entry, or an entry's codemod has no fixture | `upgrade` |
| `MAIN-VAULT-0006` | config | `ai.redact.driver` is unknown, or `agentcloak` has no client (500). Fix: set `built-in` or `agentcloak` and pass the client | `residency-vaults` |
| `MAIN-VERIFY-0004` | check, runtime | A market requires an identity level its driver cannot reach, or no driver serves it (check); an identity call asked for a level above the driver's reach (runtime, 422) | `verify` |
| `MAIN-VERIFY-0006` | check, runtime | The `verify` section is not valid, a driver is not built, or an email check has no app origin, share or email (500) | `verify` |
| `MAIN-WEB-0001` | check | Open CORS: `origins` contains `*` with `credentials: true` (any env), or `*` at all with `--env prod` | `inbound-protection` |
| `MAIN-WEB-0003` | check | An `inbound` value is invalid: a bad origin or host, a bad duration, a route pattern that matches nothing, or an unknown key | `inbound-protection` |
| `MAIN-WEB-0007` | check | The CSP allows `'unsafe-inline'` or `'unsafe-eval'` in `script`, or `*`, `http:` or `https:` alone as a script source | `inbound-protection` |
| `MAIN-WEB-0009` | check | Code sets a cookie with `SameSite=None` without `Secure`, or `httpOnly: false` for a cookie not in `cookies.readable` | `inbound-protection` |
| `MAIN-WEB-0010` | check | `security-txt` has no `contact`, or `expires` is missing, in the past, or more than one year after the build date | `inbound-protection` |
| `MAIN-WEB-0011` | check | No `security-txt` in a production app (a warning, not a failure) | `inbound-protection` |
| `MAIN-WORKER-0001` | check | `node:worker_threads`, `node:cluster` or `child_process.fork` used outside the SDK | `parallelism` |
| `MAIN-WORKER-0009` | check | A `workers` or `cluster` value is invalid: a size, `processes` or `queue` out of range, a `memory` that isn't a size, a module glob that matches no file, a module in two pools, or an `env` name that is a secret's name | `parallelism` |
| `MAIN-WORKER-0010` | check | Workers or cluster on a target that can't run them: cluster with more than one process on serverless, edge or Deno fails; workers on a runtime without threads warn that tasks run inline | `parallelism` |
| `MAIN-WORKER-0016` | check | A worker module or `cluster.run` found in the code that `main.yaml` doesn't declare | `parallelism` |
<!-- codes:end -->

## Next

- [Errors and problem details](https://mainsdk.dev/docs/reference/errors)
- [Your first check](https://mainsdk.dev/docs/getting-started/first-check)
- [The check](https://mainsdk.dev/docs/concepts/the-check)
