MainSDK
Search⌘K
How-to

See every call, decision and change your app makes

Get a full picture of a MainSDK app - destinations, drivers, access decisions and a tamper-evident audit log - in your own OpenTelemetry stack or SIEM.

Building for 0.2

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

You will see what your app does, from your own tools: which outside hosts it calls, which driver serves each interface, why each access request was allowed or denied, and who changed what. Everything runs inside your app and goes to the destinations you choose. Nothing is sent to MainSDK until you log in and turn it on.

You need

  • A MainSDK app with a main.yaml.
  • Optional: an OpenTelemetry collector or a SIEM that accepts OTLP.

1. Ask the app what it is

bash
npx mainsdk info            # human-readable
npx mainsdk info --json     # for scripts, agents and reviews

It lists the project, each interface and its driver, every network destination (yours and each driver's declared hosts), the folders, server facts and programs the code uses (see Declare what your app uses), tracking settings such as email.tracking: none, and every open exception with its owner and expiry. Run it in CI and diff it to catch a new destination or driver in a pull request.

A running app can serve a signed copy at /.well-known/mainsdk.json. mainsdk info https://app.example.com checks the signature, so a buyer can verify the record without access to your code.

2. Turn on the audit log

main.yaml
logging:
  audit: true
  redact: [personal, sensitive, child, phi]
  retention: 12m

Every action and every policy decision is written as an OCSF record with the actor, the action, the resource, the decision and the policy version. Each record carries the hash of the one before it, so an edit, deletion or reorder is detectable:

scripts/verify-audit.ts
import { log } from "mainsdk";

const result = log.verifyAudit({ from: "2026-10-01" });
if (!result.ok) throw new Error(`Audit log broken at ${result.brokenAt}`);

The audit log is never sampled or dropped. If its sink is down, records buffer to local disk and MAIN-LOG-0010 is reported.

3. Find out why a request was denied

Every decision has an id. policy.explain returns the policy line that made it:

app/admin/why/route.ts
import { auth, policy } from "mainsdk";

export async function GET(req: Request) {
  await auth.require(req, { role: "admin" });
  const id = new URL(req.url).searchParams.get("decision") ?? "";
  return Response.json(policy.explain(id));
}

Blocked network calls are decisions too: each one names the destination, the rule and the calling file and line (see Restrict which hosts and IPs your app can reach).

4. Send it to your own tools

main.yaml
telemetry:
  to: [terminal, otel]
  otel:
    endpoint: https://otel.acme.dev:4318
    headers: { authorization: secret:OTEL_TOKEN }
  sample: 0.2          # traces only; errors, decisions and audit are never sampled

Logs follow the OpenTelemetry data model with W3C trace ids, so they land in Grafana, Datadog, Honeycomb or your SIEM next to the rest of your traces. Export is asynchronous and bounded: if the collector is down, telemetry is dropped and counted, and your requests never wait (ADR-0003).

What is kept out

Redaction runs in your app before anything is written or exported:

  • Fields labelled personal, sensitive, child or phi, anything resolved from a secret: reference, and known key formats.
  • Request and response bodies, cookies and authorization headers, unless you turn them on.
  • User ids are pseudonymised per project, and IP addresses are trimmed (IPv4 to /24, IPv6 to /48).

The telemetry@1 contract lists every field, its purpose and its redaction rule. A field not in it is dropped (MAIN-LOG-0005).

Verify

bash
npx mainsdk check --env prod
npx mainsdk info --json

check passes with no MAIN-LOG finding, and info lists only the destinations and drivers you expect.

If it fails

CodeCauseFix
MAIN-LOG-0003MAINSDK_DEBUG=driver was ignored in production.Use a debug session (phase 2).
MAIN-LOG-0005An attribute not in telemetry@1 was dropped before export.Remove it, or propose it for the contract.
MAIN-LOG-0010The audit sink is unavailable; records are buffering to local disk.Restore the sink.
MAIN-INFO-0002The signed record from a running app doesn't verify.Treat the record as untrusted and redeploy from a clean build.

For agents

text
Goal: give this app an audit log and send its telemetry to the team's collector.
Read: docs/specs/log-telemetry.md, this page, main.yaml.
Touch only: main.yaml, main.env.prod.yaml.
Do: set logging.audit: true with redact labels; set telemetry.to: [terminal, otel] and otel.endpoint;
keep the collector token as a secret: reference.
Verify: npx mainsdk check --json --agent && npx mainsdk info --json
Never turn off redaction or log request bodies to debug.

What comes later

  • Streaming each customer organisation's audit log to its own SIEM (phase 2).
  • One view across every app: a live destination map that alerts on any new host, and reachable-vulnerability reports, in Fleet (phase 4, spec fleet-observe).

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet