MainSDK
Search⌘K
Architecture

Security architecture

MainSDK's trust boundaries, the STRIDE threat model per interface and driver class, how CI keeps the models current, and the limits we close.

Planned for 0.4

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

This page is about internals: where the trust boundaries sit, how threats map to controls inside the SDK, and how the threat models themselves are kept honest. For what protects your app day to day (secrets, network permissions, redaction, signed delivery), read the security model.

We publish a STRIDE threat model for every interface and every class of driver, kept next to its contract and versioned with it. The models are published per contract version from milestone 0.4.

Trust boundaries

flowchart LR
  user(["User or agent"]) -->|"B1 · request"| app
  subgraph app["Your app process"]
    iface["Interface + guarantees"] --> pe["Policy engine"]
    pe --> drv["Driver"]
  end
  drv -->|"B2 · TLS, network allowlist"| prov["Provider API"]
  tuf["TUF repository"] -->|"B3 · signed packs"| app
  app -.->|"B4 · telemetry, after login"| cloud["MainSDK Cloud"]
  dev(["You and your agent"]) -->|"B5 · CLI, MCP server"| repo["Repo and CI"]
  repo --> app
Diagram

Threats and controls by layer

Each layer of the four-layer call path and each driver class has its own threats. The model files cover four driver classes (remote HTTP, in-process, local and WebAssembly-sandboxed) plus three platform subjects: TUF delivery, the CLI and the MCP server.

STRIDE: S spoofing, T tampering, R repudiation, I information disclosure, D denial of service, E elevation of privilege.

LayerThreatSTRIDEControl
Interface and guaranteesA user reads another user's recordsIOwner scope on every query (db.orders.for(user)), rule MAIN-DATA-0007
Interface and guaranteesA key in code or the browser bundleIsecret: references resolved on the server; MAIN-SECRET-* rules and MAIN-CONFIG-0004
Interface and guaranteesA retry charges or sends twiceTIdempotency key in every contract that changes the world
Policy engineA repo or overlay quietly loosens an org ruleELayers only tighten; MAIN-POLICY-0004 on load
Policy engineA broken or unsigned policy set leaves the app openE, DDeny by default; last good set kept, MAIN-POLICY-0009 (ADR-0003)
Policy engineA decision can't be traced laterREvery decision logged with id, policy version, layer and mode, in a hash-chained OCSF audit log
Remote HTTP driverA poisoned dependency sends secrets to a new domainINetwork allowlist at the SDK chokepoint, MAIN-NET-0002; DNS answers checked, redirects to undeclared hosts refused
Remote HTTP driverA driver touches data it never declaredI, EManifest of data labels, regions and hosts; the engine refuses the rest
In-process driver (for example Cedar)A malformed input crashes the appDPanics never cross the binding; typed errors with stable codes
Local driverLocal setup reaches productionTMAIN-CHECK-0026 local driver in production
Wasm-sandboxed driverDriver code reaches beyond its permissionsEWebAssembly sandbox where possible, plus manifest permissions
Delivery (TUF)Rollback, freeze or mix-and-match of packsTTUF roles and expiry; MAIN-TUF-0001 rollback refused; last good set on expiry (ADR-0005)
CLIA tampered release of MainSDK itselfTSLSA level 3, reproducible builds, Sigstore, no install scripts
CLIinfo shows a forged app record as genuineSSignature checked on /.well-known/mainsdk.json; MAIN-INFO-0002
MCP server and actionsTool poisoning or an agent running a destructive actionS, ETools exposed only when marked; signed tool descriptions; a person's yes for destructive actions; short-lived, narrow tokens through OAuth token exchange
MCP server and actionsA second door with weaker checksEOne action() runs the same access check, owner scope, rate limit and validation on every surface
Cloud (phase 2)The project id used to send fake dataSThe id is never a credential; only an ingest key or platform OIDC token sends
Cloud (phase 2)Personal data leaks through telemetryIRedaction in process; telemetry@1 field list; no bodies, cookies or auth headers
Cloud (phase 2)Cloud outage takes apps downDNever in the request path (ADR-0003)

How a threat model is written

contracts/email/threat-model.yaml
schema: threat-model@1
subject: { interface: email, contract: "email@1.4" }
assets: [message-body, email-address, provider-key]
boundaries:
  - app process → driver → provider API (TLS 1.3, network allowlist)
threats:
  - id: EMAIL-T-003
    stride: tampering
    threat: Marketing email sent without an unsubscribe link
    mitigations: [{ guarantee: email.unsubscribe }, { rule: MAIN-EMAIL-0001 }, { test: "email.AC4" }]
    residual: low
reviewed: { by: [security-reviewer-id], on: 2026-12-01 }
  • Every model covers all six STRIDE categories, or says why one doesn't apply.
  • Every mitigation points to something that exists: a guarantee, a check rule id or a tested acceptance criterion. A broken link fails CI.
  • A contract version change without a threat model update fails CI.
  • Each model records who reviewed it and when; the reviewer is never the only author of the contract change.

Models live beside the code they describe:

text
contracts/email/email.tsp            the contract, email@1.4
contracts/email/threat-model.yaml    its threat model, same version
threat-models/drivers/               remote-http, in-process, local, wasm-sandboxed
threat-models/platform/              tuf-delivery, cli, mcp-server

mainsdk contract check runs the coverage report in CI. A new interface without a model fails with MAIN-CONTRACT-0021 missing threat model: <interface>. Each model renders on the docs site at /security/threat-models/<interface>/<version>, with older versions kept.

Still open: who reviews threat models in phase 1, before outside maintainers exist.

Limits we close on purpose

  • Code that bypasses the SDK. A raw socket or another HTTP library can escape an in-app check. mainsdk check blocks raw network use outside the SDK at build time (MAIN-NET-0001), and mainsdk network export writes the same allowlist into the platform's own egress controls as a second, independent wall.
  • No remote shell. We never offer a shell or a debugger attached to production. Live tail, debug sessions and replay answer the same questions without opening that door (phase 2).
  • Production access. Cloud access to production data needs SSO or a passkey with phishing-resistant MFA, and live tail and debug sessions need a fresh passkey no older than 5 minutes (phase 2). Fleet critical actions reuse the same step-up and need two people (phase 4).
  • Fleet push. Fleet sends only signed, declarative policy and data, signed with keys the tenant holds, never code (phase 4).

Independent review

An independent security audit of the TypeScript core and SDK happens before 1.0 and is published in full, then yearly. Our own release signing and vulnerability handling are in supply chain.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet