MainSDK
Search⌘K
Interfaces

Security and privacy

Incident controls

incident

Switch off one action, route, interface, driver or agent, put the app in read-only mode, or sign everyone out, in seconds and without a deploy.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec incident-controlsFree in your app, Apache-2.0

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.
Stability levels
Updated 2026-10-28

Switch off one action, route, interface, driver or agent, put the app in read-only mode, or sign everyone out, in seconds and without a deploy.

At a glance

incident:
  kill: []
  read-only: false
  read-only-allow: [/api/auth/*, /api/webhooks/*]
  sessions-after: null
  tenants: {}
  expires: {}

Calls

  • incident.status()
  • incident.set(req, change, { reason, expires? })

Guarantees

Whatever the driver.

  • A control only tightens what main.yaml allows; a push cannot open anything
  • Reaches every instance through the config store and survives restarts
  • Audited with who, why and when; cleared by deleting it
  • Never reads the store in a request; fail static

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (pushes through the config driver) (default)
incident:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-INCIDENT-0004Checkmainsdk check, before deployA kill target is unknown or malformed, or names a protected interface
MAIN-INCIDENT-0007Checkmainsdk check, before deployA control with no expires has been in force for 24 hours (warn)
MAIN-CONFIG-0020Checkmainsdk check, before deployA config value fails its schema; the last good value is kept
MAIN-INCIDENT-0001RuntimeError responses and logsSwitched off by an incident control (503, retryable)
MAIN-INCIDENT-0002RuntimeError responses and logsThe app is read-only during an incident (503 for requests; thrown by writes)
MAIN-INCIDENT-0003RuntimeError responses and logsThe session was issued before the incident sign-out cut-off (401)
MAIN-INCIDENT-0005RuntimeError responses and logsmainsdk incident could not write: no --reason, or the config driver has no write
MAIN-INCIDENT-0006RuntimeError responses and logsincident.set refused: no incident.manage, or no fresh phishing-resistant session (403)
MAIN-CONFIG-0001RuntimeError responses and logsUnsupported schema value in main.yaml
MAIN-CONFIG-0002RuntimeError responses and logsmain.yaml can't be read: bad indent, a tab, an anchor or tag, or a duplicate key; the error names the line
MAIN-CONFIG-0003RuntimeError responses and logsA value has the wrong format: money ("$0.05"), a rate ($5/day), a size (25MB) or a duration (1h)

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
incident section in main.yaml and the tighter-only push rulePhase 10.4incident-controls#schema
Kill switch for actionsPhase 10.4incident-controls#kill
Read-only modePhase 10.4incident-controls#read-only
Sign-out cut-off for the app or one tenantPhase 10.4incident-controls#sessions-after
Config driver writePhase 10.4incident-controls#driver-write
mainsdk incident onPhase 10.4incident-controls#cli
incident.set for an admin screenPhase 10.4incident-controls#in-app
Controls in the status header and mainsdk infoPhase 10.4incident-controls#visibility
Expiry and the 24 hour warningPhase 10.4incident-controls#expiry

MainSDK Cloud

Everything above is free in your app. Cloud adds these on top, by plan:

PlanWhat it adds
FleetOne switch across many apps (spec fleet-defend)

Quick start

main.yaml
incident:
  kill: []
  read-only: false
  read-only-allow: [/api/auth/*, /api/webhooks/*]
  sessions-after: null
  tenants: {}
  expires: {}
app/example.ts
import { incident } from "mainsdk";

declare const req: Request;   // the request of your admin route

// what is switched off right now, from memory
const state = incident.status();
console.log(state.controls.length);

// from an admin screen: needs incident.manage and a fresh passkey sign-in
await incident.set(req, { kill: ["action:orders.refund"] }, { reason: "refund abuse, ticket 812", expires: "4h" });

Configuration

KeyTypeWhat it does
incident.killlistTargets switched off: action:<name>, route:<glob>, interface:<id>, driver:<id>, agent:<name>, tool:<name>. Defaults to []
incident.read-onlybooleanRefuse writes in db, files and jobs, and unsafe requests. Defaults to false
incident.read-only-allowlistPaths that may still take unsafe requests while read-only
incident.sessions-afterRFC 3339 string or nullSessions and refresh tokens issued before this instant end
incident.tenants.<tenant>.sessions-afterRFC 3339 stringThe same cut-off for one tenant
incident.expires.<entry>RFC 3339 stringInstant after which one control stops applying

Deployed values are usually empty; controls arrive as main.incident.* pushes. See the incident.* rows in main.yaml reference.

What you get

  • A control only tightens what main.yaml allows; a push cannot open anything
  • Reaches every instance through the config store and survives restarts
  • Audited with who, why and when; cleared by deleting it
  • Never reads the store in a request; fail static

Calls

CallWhat it does
incident.status()Returns the controls in force or expired, with when, who and why. Reads memory, never the config store.
incident.set(req, change, { reason, expires? })For an admin screen. Needs the incident.manage permission and a fresh passkey sign-in, else MAIN-INCIDENT-0006 (403) and nothing is written. Makes the same push and audit record as the CLI.

What a control does

ControlEffectCode
killThe named action, route, interface, driver or agent is refused: 503, retryable, Retry-After: 60. The problem names the target, never the reasonMAIN-INCIDENT-0001
read-onlyWrites in db, files and jobs, and unsafe requests, are refused. Reads, sign-in and the audit log go on. read-only-allow lists paths that may still take unsafe requestsMAIN-INCIDENT-0002
sessions-afterSessions and refresh tokens issued before the instant end (401; sign in again). Set it for the app or for one tenant. API keys keep working; revoke a leaked key with apikeys.revokeMAIN-INCIDENT-0003

A kill target is action:<name>, route:<path>, interface:<name>, driver:<id>, agent:<name> or tool:<name>. Some interfaces cannot be killed, because the app needs them to run and to recover. An unknown or malformed target is MAIN-INCIDENT-0004.

Only tighter

Controls arrive as pushes of main.incident.* through the config driver, which needs a write call. A push is accepted only if it is tighter than or equal to what main.yaml allows: adding kill targets, read-only: true, a later sessions-after, and expiry instants that do not move earlier. A push that would open something is refused with MAIN-CONFIG-0021, and a malformed one with MAIN-CONFIG-0020; the app keeps the value it had. So a leaked push credential cannot use incident controls to loosen the app.

Every change is audited as an incident.control record with the control, target, reason, actor and env, and is also written as a security event. A control with no expires that has been in force for 24 hours is MAIN-INCIDENT-0007 (a warning in mainsdk check, and in the log once an hour).

CLI

text
mainsdk incident on kill action:orders.refund --reason "refund abuse, ticket 812" --expires 4h --env prod
mainsdk incident on read-only --reason "session store copied"
mainsdk incident on sign-out --tenant tnt_01JD --reason "leaked token"
mainsdk incident off kill action:orders.refund --reason "fixed"
mainsdk incident status --json

The command refuses without --reason (MAIN-INCIDENT-0005) and prints when the change will have reached every instance: within 5 seconds for a driver with a change stream, else within the driver's refresh interval.

Fail static

A control is held in memory on each instance and the request path reads memory only. If the config store is down, instances keep the controls they last had, and a restart reloads them from the last good value. No incident control ever makes a request wait on the store.

Errors and checks

CodeWhenWhat to do
MAIN-INCIDENT-0001A killed target was used (503, retryable, Retry-After: 60)Clear the control when the incident is over
MAIN-INCIDENT-0002The app is read-only and a write was attempted (503)Clear read-only, or list the path in read-only-allow
MAIN-INCIDENT-0003The session was issued before the sign-out cut-off (401)Sign in again
MAIN-INCIDENT-0004A kill target is unknown or malformed (check, runtime)Fix the target; a pushed one is refused with MAIN-CONFIG-0020
MAIN-INCIDENT-0005mainsdk incident could not write: no --reason, or the config driver has no writePass --reason; use a config driver that can write
MAIN-INCIDENT-0006incident.set refused: no incident.manage or no fresh passkey sign-in (403)Grant the permission and sign in again with a passkey
MAIN-INCIDENT-0007A control with no expires has been in force for 24 hours (warning)Clear it, or set --expires

A push that would loosen what main.yaml allows is MAIN-CONFIG-0021; a malformed one is MAIN-CONFIG-0020. See check codes and errors for all of them.

Swapping drivers

There is one driver, builtin, and it pushes through the config driver. Controls reach other instances through whichever config driver the app uses (files, local or redis); a driver with no write method makes mainsdk incident on fail with MAIN-INCIDENT-0005. See Swap a driver.

Testing

createTestApp accepts a configDriver, so two apps can share one in-memory store. Push main.incident.kill to it, refresh both apps and the killed action is refused with MAIN-INCIDENT-0001. The spec's criteria are covered this way in packages/sdk/test/incident-controls.test.ts.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet