App registry and link
appsSee 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.
app-registryFree in your app, Apache-2.0Status 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.
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]import { apps } from "mainsdk";
const order = await apps.call("prj_01JD8X", "orders.status", { input: { id: 1 } });Drivers
1 drivers. Select one to see its line in main.yaml.
apps:
driver: builtinChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-APPS-0004 | Checkmainsdk check, before deploy | Code reads the registry folder directly, or permissions.disk grants it, instead of apps.peer() |
MAIN-APPS-0005 | Checkmainsdk check, before deploy | A file that reads apps.peer() or apps.call() results also makes an outbound call; registry data may leave the machine |
MAIN-APPS-0001 | RuntimeError responses and logs | 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 |
MAIN-APPS-0002 | RuntimeError responses and logs | Two records claim one project id from different repositories |
MAIN-APPS-0003 | RuntimeError responses and logs | A record says running but was last seen more than 5 minutes ago |
MAIN-APPS-0006 | RuntimeError responses and logs | A peer call between apps that don't both list each other under apps.peers |
MAIN-APPS-0007 | RuntimeError responses and logs | A peer call to an action the callee doesn't list under apps.expose |
MAIN-APPS-0008 | RuntimeError responses and logs | The link handshake failed: the peer's proof doesn't verify with the identity key in its seal, or its seal is invalid |
MAIN-APPS-0009 | RuntimeError responses and logs | apps.link: on without apps.identity and MAINSDK_APP_KEY; the link stays off |
MAIN-APPS-0010 | RuntimeError responses and logs | See the spec |
MAIN-APPS-0011 | RuntimeError responses and logs | See the spec |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Per-machine registry | Phase 1 | 0.4 | app-registry#write |
| mainsdk apps | Phase 1 | 0.4 | app-registry#apps-cmd |
| Peer calls on a mutual challenge | Phase 1 | 0.4 | app-registry#link |
Quick start
apps:
link: on
identity: "ed25519:MCowBQYDK2VwAyEA…" # public half of the app identity key
peers: [prj_01JD8X] # B must list A backapps:
link: on
identity: "ed25519:MCowBQYDK2VwAyEA…"
peers: [prj_01JC8Q]
expose: [orders.status]
access:
roles:
app: [orders.status] # a peer runs as the role appimport { 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 } });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 reachableThe private half of the identity key is the secret MAINSDK_APP_KEY. It is never in main.yaml or in the record.
Configuration
| Key | What it does |
|---|---|
registry | on (default) or off. |
dir | The registry folder; or the MAINSDK_APPS_DIR variable. |
link | on opens the link. Off by default; without identity and MAINSDK_APP_KEY it stays off (MAIN-APPS-0009). |
identity | The ed25519: public key of this app. |
peers | Project ids of the apps this app may call or be called by. |
expose | Action names a declared peer may call. |
ports | The 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
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 daysmainsdk 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:
| Call | What 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?