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 Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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.
Look any code up offline:
$ 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.
| 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.
| 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.
| 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.
| 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.
| 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 |
Next
Was this page helpful?