Neighbour apps
neighboursLet 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.
neighboursFree 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.
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 busimport { neighbours } from "mainsdk";
const s = await neighbours.ask("prj_01JD8X", "status");
console.log(s.resources.state, s.posture);Drivers
1 drivers. Select one to see its line in main.yaml.
neighbours:
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-NEIGHBOUR-0001 | Checkmainsdk check, before deploy | neighbours 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-0005 | Checkmainsdk check, before deploy | asks.accept lists posture.raise or load.shed but apps.link is off or apps.peers is empty |
MAIN-NEIGHBOUR-0002 | RuntimeError responses and logs | See the spec |
MAIN-NEIGHBOUR-0003 | RuntimeError responses and logs | See the spec |
MAIN-NEIGHBOUR-0004 | RuntimeError responses and logs | See the spec |
MAIN-NEIGHBOUR-0006 | RuntimeError responses and logs | See the spec |
MAIN-NEIGHBOUR-0007 | 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-host key | Phase 1 | 0.4 | neighbours#host-key |
| Host signal bus | Phase 1 | 0.4 | neighbours#host-bus |
| status, posture.raise and load.shed asks | Phase 1 | 0.4 | neighbours#asks |
| Priority between neighbours | Phase 1 | 0.4 | neighbours#priority |
Quick start
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 lastimport { 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
| Key | What it does |
|---|---|
host | on joins the host bus. Off by default. |
listen | Host signal names this app accepts, such as attack.*. Default none. |
share | Signal names this app may send to the host. Default none; anything else stays in the app (MAIN-NEIGHBOUR-0007). |
priority | 1 to 9, sent with each signal so a rule can compare itself with the sender. Default 5. |
max-per-minute | Host signals accepted per neighbour. Default 60. |
ports | The loopback range used only as the fallback, 1024 to 65535. Default 47400-47499. |
asks.accept | What declared peers may ask: status, posture.raise, load.shed. Default status. |
asks.max-posture, asks.max-for, asks.max-per-hour | Ceilings 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
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 moremainsdk 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).
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
| Call | What 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 passedmax-per-minute.MAIN-NEIGHBOUR-0007: a signal outsidesharewas 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
- Signals
- Playbooks
- Network permissions
- Check codes: each code's cause and fix.
Was this page helpful?