MainSDK
Search⌘K
Interfaces

Platform, OS and network

Neighbour apps

neighbours

Let MainSDK apps on one machine hear each other's host-scope signals over user-only sockets, signed with a per-host key, so one app's attack warning raises the others' defences.

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

neighbours lets MainSDK apps on one machine, run as the same OS user, share host signals so one app's attack warning can raise the others' defences.

At a glance

neighbours:
  host: on
  listen: [attack.*, pressure.*]
  share: [attack.suspected, pressure.high]
  asks: { accept: [status, posture.raise, load.shed] }
  priority: 5                          # 1 to 9
  ports: 47400-47499                   # loopback range for the host bus

Calls

  • neighbours.ask(peer, verb, { to?, for?, reason? })
  • new Neighbours(opts)
  • parseNeighbours(section)
  • neighboursIssues(section, ctx)
  • rotateHostKey(appsDir)

Guarantees

Whatever the driver.

  • Nothing leaves the machine
  • Only three asks between declared peers; never code, data or a lowered guard
  • Apps never list the machine's apps

Drivers

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

main.yamlBuilt in (user-only sockets and the app link) (default)
neighbours:
  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-NEIGHBOUR-0001Checkmainsdk check, before deployneighbours is invalid: host: on with apps.registry: off, a share or listen name that isn't a signal name or glob, priority outside 1 to 9, max-posture: lockdown, max-for over 1h, or asks.accept with a verb outside the three
MAIN-NEIGHBOUR-0005Checkmainsdk check, before deployasks.accept lists posture.raise or load.shed but apps.link is off or apps.peers is empty
MAIN-NEIGHBOUR-0002RuntimeError responses and logsSee the spec
MAIN-NEIGHBOUR-0003RuntimeError responses and logsSee the spec
MAIN-NEIGHBOUR-0004RuntimeError responses and logsSee the spec
MAIN-NEIGHBOUR-0006RuntimeError responses and logsSee the spec
MAIN-NEIGHBOUR-0007RuntimeError 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-host keyPhase 10.4neighbours#host-key
Host signal busPhase 10.4neighbours#host-bus
status, posture.raise and load.shed asksPhase 10.4neighbours#asks
Priority between neighboursPhase 10.4neighbours#priority

Quick start

main.yaml
apps:
  registry: on
neighbours:
  host: on
  listen: [attack.*, pressure.*]            # host signals this app accepts
  share: [attack.suspected, pressure.high]  # what this app may send
  priority: 5                               # 1 yields first, 9 yields last
app/signals.ts
import { signals } from "mainsdk";

// Hear what the neighbours share. Host signals reach handlers only when you ask for them with `from`.
signals.on("attack.*", (s) => console.log(s.name, s.source.project, s.trust), { from: ["host"] });

How it works

Each app with host: on reads or creates a 32-byte key at <apps dir>/../host.key (mode 0600, created once with O_EXCL). It listens on a Unix socket at <apps dir>/../run/signals/<h>.sock (mode 0600 in a 0700 folder) and writes a <h>.peer marker beside it, where h is the first 16 hex digits of the HMAC of the project id under the key, so the file names do not reveal project ids. A host-scope signal in share goes to every other peer marker in that folder (at most 64, 250 ms each, a dead one skipped for 5 minutes after three failures), signed with HMAC-SHA256 under the host key. The receiver drops anything whose signature does not verify, whose name is not in listen, or that comes faster than max-per-minute from one neighbour.

The listener and the sends go through the network chokepoint: the app declares its own socket and the run folder under network.listen and network.allow while host: on. Where the runtime refuses a Unix socket (Windows, serverless), the bus falls back to loopback TCP inside neighbours.ports (default 47400-47499), with the marker naming the port; mainsdk neighbours shows which transport is in use.

Configuration

KeyWhat it does
hoston joins the host bus. Off by default.
listenHost signal names this app accepts, such as attack.*. Default none.
shareSignal names this app may send to the host. Default none; anything else stays in the app (MAIN-NEIGHBOUR-0007).
priority1 to 9, sent with each signal so a rule can compare itself with the sender. Default 5.
max-per-minuteHost signals accepted per neighbour. Default 60.
portsThe loopback range used only as the fallback, 1024 to 65535. Default 47400-47499.
asks.acceptWhat declared peers may ask: status, posture.raise, load.shed. Default status.
asks.max-posture, asks.max-for, asks.max-per-hourCeilings for asks: high at most, 1h at most, 6 an hour.

mainsdk check reports MAIN-NEIGHBOUR-0001 for a bad value (including host: on with apps.registry: off) and MAIN-NEIGHBOUR-0005 when asks.accept lists posture.raise or load.shed but apps.link is off or apps.peers is empty.

Command line

bash
npx mainsdk neighbours            # host bus settings, this app's socket name, the last host signals
npx mainsdk neighbours rotate-key # a new host key; running apps re-read it within 60 s and accept the old one for 120 s more

mainsdk info shows the same settings under neighbours.

Asks

Between apps that list each other under apps.peers (with apps.link: on), neighbours.ask(peer, verb, options) sends one of three asks over the app link: status returns the peer's state, resources, posture and priority; posture.raise { to, for, reason } raises its posture; load.shed { for, reason } runs its pressure.slim steps. The peer must accept the verb under asks.accept. A for longer than asks.max-for is shortened to it, to: lockdown is refused, a peer with a lower priority cannot ask a higher one to shed, and a seventh ask in an hour is refused. Each accepted ask is a playbook run with source neighbour and an audit entry naming both apps, the verb and the run. An app that reacted to a peer cannot ask it back for 10 minutes (MAIN-GUARD-0004).

ts
import { neighbours } from "mainsdk";

const status = await neighbours.ask("prj_01JD8X", "status");
const r = await neighbours.ask("prj_01JD8X", "posture.raise", { to: "high", for: "30m", reason: "attack on us" });
console.log(r.accepted, r.run);

Priority matching in rules and Windows named pipes come after go-live.

Helpers for wiring and tests

CallWhat it does
new Neighbours(opts)The host bus of one app instance, for tests and custom wiring.
parseNeighbours(section)Read the neighbours section of main.yaml into settings.
neighboursIssues(section, ctx)List what is wrong in the neighbours section, without throwing.
rotateHostKey(appsDir)Replace the host bus key in the apps folder; peers re-read it on their next call.

What you get

  • Apps that run as the same OS user look out for each other: when one is attacked or the machine is short of CPU, it shares a signal and the others can react through their playbooks.
  • It is opt in, uses no daemon and sends nothing off the machine.
  • A single serverless app has nothing to talk to, so it gets nothing from this until a second app runs beside it.

Errors and checks

  • MAIN-NEIGHBOUR-0001, MAIN-NEIGHBOUR-0005: the checks above.
  • MAIN-NEIGHBOUR-0002: the host key or folder is unsafe or unreadable; the bus stays off and the app keeps serving.
  • MAIN-NEIGHBOUR-0003: a host envelope failed the key check; dropped and counted.
  • MAIN-NEIGHBOUR-0004: an ask was refused.
  • MAIN-NEIGHBOUR-0006: a neighbour passed max-per-minute.
  • MAIN-NEIGHBOUR-0007: a signal outside share was asked to go to the host.

Related: signals, playbooks, network.

Swapping drivers

neighbours has no drivers: the host bus is built in, over a Unix socket or the loopback fallback. mainsdk neighbours shows which transport is in use.

Testing

createSim in @mainsdk/testing can run the real host bus over fake unix sockets with host: "sockets", and link: true lets neighbours.ask work between apps. See the Testing kit. For custom wiring, new Neighbours(opts) builds the bus of one app instance.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet