MainSDK
Search⌘K
Interfaces

Platform, OS and network

App registry and link

apps

See every MainSDK app on a machine from one local folder, and let two apps that declare each other prove who they are and call each other's exposed actions.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec app-registryFree 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

Each MainSDK app on a machine writes one record to a private folder, and mainsdk apps lists them. Two apps that list each other can prove who they are and call exposed actions.

The record is readable only by its user. mainsdk apps shows who publishes each app, which version, whether its seal holds and where it sends data. Two apps that list each other can also find each other and call the actions they expose, after proving who they are. It is a local file and a local socket: nothing leaves the machine, it needs no account, and an app never sees the machine's other apps.

At a glance

apps:
  link: on
  identity: "ed25519:MCowBQYDK2VwAyEA"
  peers: [prj_01JD8X]
  expose: [orders.status]

Calls

  • apps.peer(id)
  • apps.call(id, action, { input?, signal? })
  • apps.provide(actions)
  • new AppRegistry(opts)
  • parseApps(section)
  • readRegistry(dir)
  • readSeal(doc)

Guarantees

Whatever the driver.

  • Nothing leaves the machine
  • A record holds apps and their seals, never a person
  • Apps see only the peers they declare, and a call needs both sides to agree

Drivers

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

main.yamlBuilt in (a local folder and a user-only socket) (default)
apps:
  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-APPS-0004Checkmainsdk check, before deployCode reads the registry folder directly, or permissions.disk grants it, instead of apps.peer()
MAIN-APPS-0005Checkmainsdk check, before deployA file that reads apps.peer() or apps.call() results also makes an outbound call; registry data may leave the machine
MAIN-APPS-0001RuntimeError responses and logsThe 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
MAIN-APPS-0002RuntimeError responses and logsTwo records claim one project id from different repositories
MAIN-APPS-0003RuntimeError responses and logsA record says running but was last seen more than 5 minutes ago
MAIN-APPS-0006RuntimeError responses and logsA peer call between apps that don't both list each other under apps.peers
MAIN-APPS-0007RuntimeError responses and logsA peer call to an action the callee doesn't list under apps.expose
MAIN-APPS-0008RuntimeError responses and logsThe link handshake failed: the peer's proof doesn't verify with the identity key in its seal, or its seal is invalid
MAIN-APPS-0009RuntimeError responses and logsapps.link: on without apps.identity and MAINSDK_APP_KEY; the link stays off
MAIN-APPS-0010RuntimeError responses and logsSee the spec
MAIN-APPS-0011RuntimeError responses and logsSee the spec

Features

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

FeaturePhaseMilestoneSpec ticket
Per-machine registryPhase 10.4app-registry#write
mainsdk appsPhase 10.4app-registry#apps-cmd
Peer calls on a mutual challengePhase 10.4app-registry#link

Quick start

main.yaml (app A)
apps:
  link: on
  identity: "ed25519:MCowBQYDK2VwAyEA…"   # public half of the app identity key
  peers: [prj_01JD8X]                      # B must list A back
main.yaml (app B)
apps:
  link: on
  identity: "ed25519:MCowBQYDK2VwAyEA…"
  peers: [prj_01JC8Q]
  expose: [orders.status]
access:
  roles:
    app: [orders.status]                   # a peer runs as the role app
app A
import { apps } from "mainsdk";

const peer = await apps.peer("prj_01JD8X");            // null unless it is a declared peer
const order = await apps.call("prj_01JD8X", "orders.status", { input: { id: 1 } });
app B
import { action, apps } from "mainsdk";
import { s } from "mainsdk/schema";

const ordersStatus = action({
  name: "orders.status",
  intent: "Read an order's status",
  input: s.object({ id: s.number() }),
  risk: "read",
  run: async ({ input }) => ({ id: input.id, status: "shipped" }),
});

apps.provide([ordersStatus]);   // only names under apps.expose are reachable

The private half of the identity key is the secret MAINSDK_APP_KEY. It is never in main.yaml or in the record.

Configuration

KeyWhat it does
registryon (default) or off.
dirThe registry folder; or the MAINSDK_APPS_DIR variable.
identityThe ed25519: public key of this app.
peersProject ids of the apps this app may call or be called by.
exposeAction names a declared peer may call.
portsThe loopback range used only as the fallback.

What you get

The record <dir>/<project-id>.json (default ~/.mainsdk/apps, mode 0600 in a 0700 folder) holds the served seal, the boot check state, status, environment, start time and a lastSeen rewritten every minute. It never holds a user, tenant, session or device id; a record with any other field is refused before it is written (MAIN-APPS-0010). The registry starts for an app whose main.yaml has an apps, ops or neighbours section, and stays off on serverless runtimes or with apps.registry: off. An unsafe folder (world-writable or owned by another user) logs MAIN-APPS-0001, nothing is written and the app keeps serving.

The link listens on a Unix socket at <dir>/../run/<project-id>.sock (mode 0600). Where the runtime refuses a Unix socket it falls back to loopback TCP inside apps.ports (default 47500-47599) with a user-only port file at the same path. Before any request, both sides sign a fresh challenge with their identity keys and check the other's signature against the key in the other's seal; a proof that does not verify fails with MAIN-APPS-0008. A call then needs both apps to list each other (MAIN-APPS-0006) and the action under the callee's apps.expose (MAIN-APPS-0007). The callee runs it as subject app:<caller id> with roles app and app:<caller id>, no user and no tenant, and audits both project ids. The caller's network decision log shows the call as app:<callee id>.

Command line

bash
npx mainsdk apps                         # every app on the machine, flagged
npx mainsdk apps --strict                # exit 1 when anything is flagged
npx mainsdk apps --actions --risk destructive --json
npx mainsdk apps --prune                 # delete records stopped or stale for 30 days

mainsdk check reports MAIN-APPS-0004 for code that reads the registry folder by path or a permissions.disk entry that covers it, and MAIN-APPS-0005 for a file that uses apps.peer() or apps.call() results and also calls http or share.

Errors and checks

  • MAIN-APPS-0001: the registry folder is unsafe or not writable.
  • MAIN-APPS-0002: two records claim one project id.
  • MAIN-APPS-0003: a running record was last seen over 5 minutes ago.
  • MAIN-APPS-0004, MAIN-APPS-0005: the checks above.
  • MAIN-APPS-0006: the apps do not list each other.
  • MAIN-APPS-0007: the action is not exposed.
  • MAIN-APPS-0008: the handshake failed.
  • MAIN-APPS-0009: the link stays off.
  • MAIN-APPS-0010: a record would hold a field outside the schema.
  • MAIN-APPS-0011: a declared peer is not reachable.

Swapping drivers

apps has no driver to swap: the registry is a local folder and the link is a local socket. apps.registry: off stops the record and apps.link stays off unless you turn it on.

Testing

createSim from @mainsdk/testing runs several apps on one fake clock. Pass host: "sockets" to it and link on an app to seal that app and open its link to the other linked apps, in a temporary apps folder that stop() removes. For one app, new AppRegistry(opts) and readRegistry(dir) cover the registry on its own:

CallWhat it does
new AppRegistry(opts)The registry of one app instance, for tests and custom wiring.
parseApps(section)Read the apps section of main.yaml into settings.
readRegistry(dir)Read every app record in an apps folder.
readSeal(doc)Read a seal document and say what it proves.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet