Skip to main content

Changelog

Notable user-facing changes to the noBGP router and agent. Router and agent are versioned independently — a router version and an agent version that share a number are unrelated.


Agent 0.4.55 — August 2026

Files that went missing from the mounted drive on macOS and Windows are back.

Shared drive

  • Directory listings under node/ and networks/<name>/ no longer lose entries on macOS and Windows. 0.4.54 moved each share below a folder on the mount, but the listings still named every entry relative to the share's own root — so the WebDAV client resolved them against the mount root, where they collided with node/ and networks/ and silently disappeared. A folder holding six files showed four, with nothing reporting an error. Linux mounts over FUSE and was never affected.
  • Subdirectories are fixed at every depth, not just at the top. Entries are named under the folder they were asked about now, so a listing of networks/production/docs/ reports networks/production/docs/report.pdf rather than doubling the path.
  • A network whose name contains a space lists correctly too. Names created through network_create are always DNS-shaped, but one renamed in the dashboard need not be, and such a name went onto the wire unescaped.

See What the mount contains.


Agent 0.4.54 — August 2026

The mounted drive holds two folders now: the machine's own storage, and one per network.

Shared drive

  • The mount root is no longer a network's share. It has two children instead: node/, this machine's own storage area, and networks/, holding one folder per network the node belongs to. A file that used to be at /mnt/nobgp/report.pdf is at /mnt/nobgp/networks/production/report.pdf now. Nothing moved in storage — the same bytes answer at the same URLs as before, and the web dashboard is unchanged; what changed is what the mount shows you.
  • node/ reaches the machine's own storage with no URL and no token. A node has always been able to reach its own area; until now the only way in was an HTTPS request you had to make yourself, and the drive shows it instead. Anything written there is waiting for you whether or not the machine is still up — reading it from elsewhere still needs Owner or Admin in the organization that owns the node, exactly as the URL does.
  • node/ is that exact path on every node, never the machine's name and never its id — so one script deployed across a fleet reaches each machine's own storage by the same path. networks/ exists for the same reason: with networks at the top level, a network someone names node would shadow the one path that has to mean the same thing everywhere.
  • The root and networks/ are a view, not storage. Writing, creating or deleting directly in either is refused as a read-only directory, rather than appearing to work and vanishing at the next mount. Everything below node/ and networks/<name>/ is read-write as before.
  • Moving a file between the two trees copies it. They are separate filesystems, so a move across them cannot be a rename — mv, Finder and Explorer do the copy-then-delete for you, and a program that only ever renames gets a plain "cross-device" answer instead of a silent wrong result.
  • Same layout on Linux, macOS and Windows — the FUSE and WebDAV backends share one mapping, so the drive looks the same whichever way your node mounts it.

See What the mount contains.


Router 0.4.56 — August 2026

Your assistant can read and write noBGP's own file storage, without going through a node.

File tools

  • file and the fs_* tools now address three places, not one. A node reference still means that node's own disk, through its agent, exactly as before. What is new is that naming a network and no node reaches that network's shared drive, and sending storage: true alongside a node's id reaches that node's own storage area. Same tools, same responses.
  • The router serves those bytes itself. Nothing has to be online: you can read a machine's stored files while it is switched off, and leave files there for it to find when it comes back. Until now the only way to reach the share from an assistant was to read the mount through a node that happened to have it up — which billed that node's bandwidth for a file it was only relaying, and failed whenever the node was down.
  • Authority is the same as for the equivalent URL. A network's share is Member-level, the access the network already implies. A node's storage area needs Owner or Admin in the organization that owns the node — a step higher, because nothing on the node filters what arrives that way; a Member is refused with a message naming the requirement.
  • Six operations, one call each, capped at 1 MiB. read, write, list, stat, mkdir and delete work on both trees; edit, multi_edit, batch, fetch_url and fs_grep remain node operations and say so rather than silently using the node's disk. There is no multi-call session on storage — the one response carries the file — and past 1 MiB the call points you at the tree's HTTPS URL, which streams a file of any size.
  • storage: true requires the node's id, not its name. Reaching a node by name is a Member-level check, and letting it stand in front of an Owner/Admin surface would put the weaker question first.
  • Two things behave differently on storage than on a disk: a delete takes the whole subtree whether or not you asked for recursive, and the depth-guard that refuses rm -rf /etc on a node does not apply — only the tree's own root is protected; and admin is ignored, since there is no second identity to pick when no agent runs the operation.

See Addressing storage instead of a node.


Router 0.4.54 — August 2026

The shared drive has a URL you can actually open.

File storage

  • A network's share answers at https://files.nobgp.com/networks/<network-id>/. Full WebDAV — PROPFIND to list, GET to download, PUT to upload, MKCOL, MOVE, COPY and DELETE to manage — authenticated with your noBGP bearer token and carrying exactly the access your account already has to that network. Nothing about the drive mounted on your nodes changes; this is the same storage, reachable from a browser or any WebDAV client.
  • files_url in network_directory now resolves. It previously advertised a name-keyed URL that served nothing, so anyone who followed it got a 404. It is the URL above now, one per network.
  • The URL is keyed on the network id, not its name. The same network name can exist in more than one organization you belong to, and a name-keyed URL would quietly answer for whichever of them was created first — a different share for different callers, with nothing to see it by.
  • Each node has a storage area of its own, at https://files.nobgp.com/nodes/<node-id>/. It follows the machine rather than a network, so a node that joins several still has exactly one, belonging to none of them. Reaching it needs Owner or Admin in the organization that owns the node — a step above the network share, because a write arriving this way passes none of the vetoes the node's owner sets on the box. A node reaches its own area and no other.

See Reaching a share directly and the role matrix.


Agent 0.4.53 — August 2026

The local proxy behind the shared drive can tell who is calling it.

Shared drive

  • The proxy asks the kernel which local account opened each connection. The agent runs a small WebDAV proxy on 127.0.0.1:19840 so the machine's own client can mount the shared drive without holding any noBGP credential — the proxy adds the node's token to every request it forwards. Until now the only thing standing in front of it was the fact that it binds loopback, which is a network control answering an identity question: any local process that could open the socket held the node's full access to the share, and on macOS that reached past the drwx------ on /Volumes/nobgp.
  • webdav-proxy-owners bounds it to a list of numeric uids. Anything else gets 403 Forbidden, including a caller whose uid the kernel will not report. It checks the connecting process, not a secret — a shared secret would have to live somewhere, and one in a mount URL shows up in mount output and net use listings, while the kernel already knows the peer's uid and cannot be lied to about it.
  • An empty list — the default — observes rather than allows. Nothing refuses anything until you set the key; instead the agent logs the uid of the first local caller it sees, once per run. Which uid the OS WebDAV client connects as is not documented on any platform, and a wrong guess would take the shared drive off every affected node at once — so the value is meant to be read out of the log on your own nodes first, then set.
  • Linux and macOS only. Windows has no peer credentials for a loopback TCP connection, so the agent cannot learn who is calling there and the key must be left unset: a list on a Windows node refuses every request and takes the drive offline.

See Shared drive proxy keys.


Router 0.4.53 — August 2026

The shared drive is now a real filesystem, and file names are case-sensitive because of it.

Shared drive

  • README.md and readme.md are two files now. The share used to match names case-insensitively, so either spelling opened the same file whichever way it had been written. It no longer does: the exact name is the name, and asking for the other casing is a 404. macOS and Windows clients are the ones that will meet it first, since their own filesystems are not case-sensitive and a client may well have been sending a casing you never typed. Nothing existing is renamed or lost — the old index made two such names impossible to create in the first place, so no share contains a pair today.
  • An upload that dies halfway no longer replaces the file it was overwriting. A write goes to a temporary and is renamed into place only once the body has arrived complete, checked against the length the client declared. A dropped connection mid-PUT used to publish however many bytes had landed, over a file that was fine. A request that declares no length — a chunked upload — cannot be checked and behaves as before.
  • A file rewritten within the same second is no longer served stale. Downloads carry the same validator the rest of the share reports, so a client that revalidates its cached copy is answered on content rather than on a timestamp with only one-second resolution. Two edits inside one second could previously leave a caller holding the first one indefinitely.
  • A lock in one network no longer blocks the same path in another. WebDAV locks are taken on the path below the share root, so /notes.txt in one network and /notes.txt in another were the same name to the lock table: a file left locked by a Finder or Explorer copy could refuse an unrelated network's write to a file its owner cannot even see. Locks are scoped per share now. They remain per-router-instance rather than fleet-wide, as they have always been.

Agent 0.4.52 — August 2026

Turning off a node's last way in now takes a deliberate answer, and stopping the service is bounded.

Node settings

  • Dropping command from allow-tools is confirmed before it is written. That one change takes the command tool, dispatched bus commands and published terminal services away together, so unlike allow-admin: false it leaves nothing to get back in with — and undoing it needs console, RDP or SSH access to the machine. An interactive run asks; a non-interactive one is refused unless it passes --yes. It is the only one of these guards that stops a scripted caller, deliberately: a nobgp config issued over MCP is non-interactive by construction, so the warning printed after the write was going out on the very session the write had just closed.
  • --yes answers these prompts on nobgp config, for the config-management run that means it. It is not written to the config file — it is a statement about one invocation, and persisting it would leave a node pre-answered for every future veto.
  • Clearing the configured account is sudo nobgp config --user "", and it says what it costs first. An empty value with no --user on the command line reads as "never set" and is re-filled with the install account, so the empty string is how you say you mean it. On a node whose agent is the superuser, clearing it means every admin: false call is refused rather than narrowed — unelevated never means root — so unelevated execution stops and callers must ask for admin: true. The clear survives restarts but not a reinstall: nobgp service install and registration re-capture the install account, since a fresh enrollment has no other way to learn one.
  • The allow-admin: false warning stops overclaiming on nodes it does not affect. Repeated at every config load, it now names which of three states the node is in: sessions narrowed to a named account, remote execution off because there is nothing to narrow to, or no effect at all — on a node whose agent is not itself the superuser, an elevated request resolves to that same unprivileged identity and is served. A warning that fires where there is no lockout teaches operators to ignore the one that fires where there is. See Confirmed before it is written.

Stopping the agent

  • Stopping the service is bounded, and the same way on Linux, macOS and Windows. Every profile's agent is signalled, given fifteen seconds to shut down cleanly, then killed — and the wait after the kill is bounded too, at two seconds, because neither SIGKILL nor TerminateProcess dislodges a process wedged on a dead mount. The whole teardown fits in seventeen seconds however many profiles are running, inside the twenty seconds the service manager allows. Windows previously had no such bound on some of its waits, so one wedged agent could hold up the stop for every other profile.
  • The fifteen seconds are the agent's own worst case, not a guess. An orderly shutdown unmounts the node's filesystem, and that can legitimately take most of it; a kill landing mid-flush loses pending uploads and strands the mount for the next start. The log also stopped reporting an agent that exited politely just as the deadline passed as one that had to be force-killed.
  • Every unmount the agent issues on Linux and macOS is now bounded at five seconds per attempt, with a lazy detach or a forced unmount as the escalation. Agent 0.4.46 bounded the startup cleanup; the two paths left — the remount that follows a failed mount health check, and the Linux WebDAV backend — are bounded now as well. A timeout is not a verdict: the mount table is re-read afterwards, so an unmount that completed after our command was killed is recorded as the success it was.

Agent 0.4.51 — August 2026

A Linux node without the FUSE userspace helper stops pretending it has a filesystem.

Shared drive

  • The packages now declare the FUSE userspace helper. A FUSE mount needs two things: the kernel module and the fusermount helper binary. Only the kernel half was ever declared, and the kernel half being present is exactly what makes the missing one invisible — /dev/fuse exists, the agent correctly takes the FUSE path, and then cannot mount. The .deb, .rpm, .apk and .ipk all name the helper now (fuse3, or fuse-utils on OpenWrt).
  • OpenWrt gets it from the install script. The .ipk is hand-extracted rather than installed through opkg — OpenWrt has many aarch64 sub-architectures and opkg rejects a mismatch, while the static binary runs on all of them — so the package's dependency field is never resolved on that platform. install.sh installs kmod-fuse and fuse-utils explicitly instead. OpenWrt was the platform this was actually broken on: kmod-fuse was a recommendation, which opkg (unlike apt) does not install by default, and fuse-utils was never named at all. Two field routers ran for weeks with an empty /mnt/nobgp.
  • The agent says so once, with the remedy. A mount that fails because the helper is absent is permanent — retrying cannot fix it — so it is now reported once as an error naming the package to install, rather than as an identical warning every sixty seconds. One field node logged 214 of them while ls /mnt/nobgp sat silently empty and nothing named the cause. Every other mount failure stays a retryable warning, and a mount that later comes up resets the report, so a helper removed months later is named again.
  • Nothing fails the install for want of it. The filesystem stays optional, so the declaration is as strong as each packager allows without ever failing the install. On RPM hosts it is a recommendation rather than a requirement, deliberately: one .rpm serves every RPM platform the install script detects, and EL7-class hosts (Amazon Linux 2, RHEL/Oracle/CentOS 7) have no fuse3 in their default repositories at all — a hard requirement there would fail the whole install over an optional filesystem. dnf honours recommendations, so modern hosts still get it; yum and rpm ignore them, so an EL7 host installs and simply runs without a shared drive, which the agent now says once with the remedy.

Fixing an affected node — install the helper for your platform and restart the agent:

# OpenWrt
opkg install kmod-fuse fuse-utils
# Debian / Ubuntu / Alpine / Amazon / RHEL
sudo apt install fuse3 # or: apk add fuse3 / dnf install fuse3

sudo nobgp service restart

Router 0.4.50 — August 2026

The directory says when a node went offline, not just that it is.

Discovery

  • network_directory reports offline_at for every node that is down. online: false was the same two bytes for a node dead six months and one that blipped ten seconds ago, so the question actually being asked — is this thing coming back? — had to be answered somewhere else. The new field is RFC3339 in UTC and names the moment the node's connection ended. See network_directory.
  • It is always the most recent disconnection. The value is cleared on every reconnect, so it never reports the first time a node ever dropped — a machine that has flapped all week is dated by its last outage, not its first.
  • Absent means there is no such instant, and that is three ordinary situations: the node is connected right now, it has never connected at all, or its row predates the field. Absence is not an error and does not mean "long ago".
  • It survives brief: true, which trims only the per-node services arrays. The compact brief + glob probe is exactly the call that wants a date on the answer.
  • Nothing to upgrade. The router has always recorded the disconnection; this release reports it. Every node already in your directory carries the field the moment it goes offline, whatever agent it runs.

Agent 0.4.50 — August 2026

The certificate pin that guards the QUIC control channel now holds on a resumed connection too.

Security

  • A resumed QUIC handshake is checked against the pin, like a fresh one. The router's QUIC endpoint presents a self-signed certificate, and the agent verifies it against the pin it holds rather than against a public certificate authority. That check ran only when the router actually sent a certificate — which a resumed TLS session does not do — so connections that reused a cached session were not re-checked. The pin is now enforced on every connect.
  • What this closes is certificate rotation. When the router rotates its certificate, the agent learns the new pin over an already-authenticated channel; a cached session could nonetheless keep being resumed against the old key until that session expired, without ever being measured against the pin the agent had just learned. It was never a way in for an impostor — a resumption is cryptographically bound to a session that a full, pin-verified handshake established, so it cannot be forged — which is why this is hardening rather than a fix for a reachable attack.
  • It heals itself in one connect, and never costs connectivity. A resumption that fails the check discards the cached session, so the very next attempt performs a full handshake and re-pins against the current certificate. That rejection is not mistaken for the silence that marks a network as UDP-blocked, so a rotation cannot push a node onto WebSocket for the hour that verdict would last.
  • Nothing to configure, and nothing to check. quic_pin in nobgp status reports the same three states as before, and a node whose transport is wss is unaffected.

Router 0.4.49 — August 2026

An MCP client working out how to sign in is pointed at exactly one authorization server.

Sign-in discovery

  • The endpoint's metadata document now names a single authorization server: noBGP's sign-in service. It used to name a second one — the MCP host itself, which serves no OAuth endpoints at all. The standard leaves the choice among the entries entirely to the client, and each entry is an independent server with its own registration, so a client that took the second had nowhere to register and nowhere to send you to sign in. Clients that take the first entry, Claude Desktop and Claude Code among them, worked throughout — which is why this went unnoticed.
  • The router serves no authorization-server metadata of its own. It is a resource server: it accepts tokens, it does not issue them. A document served here could only ever carry someone else's issuer, which a conformant client is required to reject. Discovery runs through the protected-resource document and on to the authorization server named there.
  • Nothing changes for a client that already signs in. The sign-in page, the providers offered on it, and the tokens themselves are unaffected. If you are writing your own client, the discovery contract is now written down.

Agent 0.4.49 — August 2026

A machine waking from sleep stops taking its agent down with it.

Waking from sleep

  • A network that is briefly absent is no longer a fatal error. The first moments after a wake are the ones a laptop reliably fails in: the resolver has no servers yet, the interface has not been handed its address back, there is no route. Both the initial registration and the mid-run control reconnect returned that error straight out, so the agent exited and waited for its supervisor to restart it — on a node that was about to be fine anyway. They now retry in place for up to five minutes while the only thing wrong is that the network is absent, and the node simply reconnects when the machine finishes waking.
  • The exit was the expensive part. It tears the node's filesystem mount down on the way out, and that unmount can itself fail — which is what leaves macOS showing a nobgp volume nobody unmounted — and it costs the supervisor's restart backoff on top. One laptop in the field exited seven times in a day this way.
  • What counts as absent is a short, deliberate list: a DNS lookup that cannot be answered, no route to the host or the network, an interface with no address yet, and socket timeouts. Everything else is unchanged and still stops the agent — in particular, no authentication failure is on that list, so a revoked node still exits immediately rather than retrying against a router that will keep refusing it. A connection that is refused is not on it either: something answered, so the network is present.
  • Windows retries too. The socket errors are named in their Winsock spelling there. Written with the Unix names the check compiles and then matches nothing, so a waking Windows laptop with no route yet would have exited exactly as before.
  • The five minutes are deliberately outside the reconnect flap limits. Those exist to bound the load a flapping node puts on a router that keeps dropping it; when the router cannot be resolved at all it is never reached, so there is nothing to protect. They would have given up after about 31 seconds — shorter than an ordinary sleep/wake gap.

Fixes

  • Peer sessions come back in one round trip after a reconnect that had to be retried. The set of peers that were active when the connection dropped is now carried across each retry. The first failed attempt has already torn those sessions down, so a later one cannot rediscover them — without this, a reconnect that only succeeded on the second attempt left each session to recover on its next packet instead of being re-established immediately.

Agent 0.4.48 — August 2026

A healthy mount survives a network blip, and a node that cannot become its own configured account says so.

Filesystem mount

  • A mount is no longer torn down over a single failed health probe. Every backend's liveness check reaches through the network to the router — a WebDAV check is a request through the node's own proxy — so a wake from sleep, a DNS gap or a reset connection could fail the probe on a mount that was perfectly healthy. The teardown that followed was then refused, because the mount was live; a second volume was mounted over the top, and macOS was left showing duplicate nobgp drives in Finder. A mount must now fail three consecutive probes — about a minute at the 30-second poll interval — before it is remounted. The cost when a mount really is dead is that it stays broken for up to a minute longer.
  • An orderly stop no longer delays the next mount by ten minutes. A node that died moments after mounting holds off mounting for ten minutes on the next start — the breaker that keeps a mount-driver crash from becoming a restart loop. An ordinary stop that landed inside a young mount's first health-poll cycle, such as an upgrade restart on a slow machine, left that marker behind and the next start read it as a crash. Reaching shutdown at all proves the death was not that crash, so the marker is cleared there now.
  • The unmount on the way out is given long enough to finish. On macOS the unmount escalates through two steps that can take ten seconds between them, against a five-second budget that could only ever expire — and an unmount that never completed is what leaves Finder holding a row for a volume nobody unmounted. It now waits fifteen seconds. macOS also answers is this volume present from the kernel's own mount table rather than by reaching through the mount, so the half of the check that never needed the network no longer fails with it; the liveness half still probes, and is what the three-strike rule above covers.

Error codes

  • A node that cannot become its configured account answers failed_precondition. When the identity switch is refused by the operating system at the moment a process is created — an agent that is not the superuser, or a root agent that has lost the capability to change identity, as in a container started with dropped capabilities or under a restrictive seccomp/LSM policy — command, terminal sessions and unelevated file operations came back as internal / HTTP 500 / retryable: true. That told a well-behaved client to keep retrying a condition that is permanent until someone changes the box. It is failed_precondition — HTTP 403, not retryable — now, and the message names the remedy that actually applies to that node rather than one that is already true of it.
  • Its one retry is admin: true. Elevated work assumes no account, so there is nothing left for the kernel to refuse; this is why the refusal is not permission_denied, whose documented retry — drop admin — is exactly the call that just failed.

Fixes

  • An auto-upgrade on slow hardware stops reporting failure after succeeding. The agent watches its own binary and restarts when it changes. On a slow machine — a Pi Zero spends about a minute installing a package — the watcher could see the new binary while the install was still running, restart the service, and kill the upgrade in mid-flight. The install survived, so the node came up on the new version while logging auto-upgrade failed and arming a five-minute retry cooldown on what had been a success. The watcher now waits out an upgrade that is installing. Faster machines never saw this, because the install finished first.

Agent 0.4.47 — August 2026

A permission denial says it is permanent on every platform, in every language.

Error codes

  • A node codes a permission denial itself, from the error number rather than the sentence. Router 0.4.48 taught the router to recognise the English Access is denied. a Windows node prints; the node now states the code on the wire instead, keyed on the operating system's own error number. An unreadable path answers permission_denied — HTTP 403, not retryable — on Linux, macOS and Windows alike, whatever the node's system language renders the message as.
  • It answers the same at either identity. A denial met by the node's unelevated identity — the one that does not own the file, and so the one that actually meets denials — is coded exactly like one met elevated. Before this, the two could be classified differently.
  • Nothing about what a node permits changed. The same reads succeed and the same reads fail; what changed is that the failure no longer arrives as internal / HTTP 500 / retryable: true, telling a well-behaved client to keep retrying an ACL it will never satisfy. Branching on retryable was and remains the right thing to do.
  • The router's phrase-matching stays as a fallback for nodes on older agents, so a Linux node that has not been upgraded answers as it did before. Upgrading the node is what makes its refusals self-describing.

Router 0.4.48 — August 2026

A permission refusal on a Windows node stops reading as a server fault worth retrying.

Error codes

  • An ACL denial on a Windows node is permission_denied, not a retryable internal. Windows words a permission refusal its own way — Access is denied. where Unix says permission denied — and the router did not recognise it, so an unreadable path on a Windows node came back as internal / HTTP 500 / retryable: true. The same unelevated fs_read answered permission_denied on a Linux node and, on a Windows one, told a well-behaved client to keep retrying an ACL it was never going to satisfy. It is permission_denied — HTTP 403, not retryable — on both now.
  • Nothing about what a Windows node permits changed. The same reads succeeded and the same reads failed before and after; what changed is that the failure now says it is permanent. If you were branching on retryable to decide whether to retry, that was and remains the right thing to do — the answer you get from a Windows node is simply no longer the wrong one.
  • It became visible on agent 0.4.46. Before that release an administrative configured account read through SeBackupPrivilege and the denial mostly did not arise; stripping those privileges is what made an ordinary ACL refusal a routine answer, and this is the classification catching up with it.
  • The match is on the English phrasing. A Windows node whose system language renders the refusal differently still reports such a denial as internal until its agent is upgraded to a release that names the code itself — agent 0.4.47.

Agent 0.4.46 — August 2026

Windows execution finally runs as the node's configured account, and an administrative account stops reaching past its own permissions.

Execution identity

  • A Windows node with a configured user now runs everything as that account. command, terminal sessions and dispatched bus commands join fs_* and file, so admin: false means one thing on every surface — the same one-switch rule Unix has. On 0.4.44 and 0.4.45 those three refused an unelevated call with failed_precondition rather than running it as LocalSystem; that refusal is gone on such a node. See A second identity on Windows.
  • allow-admin: false narrows such a node instead of switching it off. With an account configured, both halves now drop to it, so a Windows node can refuse the superuser and stay fully operable. Without one it is still an off switch — LocalSystem is the only identity there, and both paths refuse.
  • It fails closed. A token that cannot be obtained, a profile that cannot be loaded or an environment that cannot be built refuses the call. Nothing is ever quietly served in the service's own context. A failure that is merely transient — the profile service still starting after a boot, a hive briefly locked — is retried rather than latched for the life of the agent.
  • Two things to expect on such a node. The first unelevated command for an account that has never signed in creates its profile directory (C:\Users\<account>), which nobgp uninstall leaves behind — file operations need no profile and create nothing. And the identity is obtained by name with no stored password, so it holds no network credentials: a command that reads a file share works with admin: true and fails with admin: false.

Security

  • An administrative configured account no longer reads past its own ACLs. Windows hands such an account a token with every privilege enabled, SeBackupPrivilege included, and a read taken with backup privilege never consults the file's permissions — so on 0.4.44 and 0.4.45, on a Windows node whose user was an administrator, an unelevated fs_read could read files that account was not permitted to read. Every token behind the second identity is now stripped of its privileges before anything uses it. A node whose account is an ordinary user, and every node on any other platform, was never affected.

Reporting what unelevated is worth

  • A node says whether its unelevated account is administrative. network_directory carries it as info.user_is_admin from router 0.4.47 — this is the agent release that answers it. Where it is true, admin: false on that node drops from LocalSystem to Administrator and no further, which on Windows is the common shape rather than an exotic one.
  • It is reported only where the node actually checked. A Unix node, and a Windows node whose check could not run, say nothing rather than claiming the account is ordinary. Absent is not false.
  • nobgp status answers the same question on the box. The allow: block gains unelevated_is_admin, present only when true, beside the unelevated account it qualifies — so an operator at the machine, or an assistant on the local MCP server, does not have to read the organization-scoped directory to learn it.

Fixes

  • A terminal session on an old Windows build no longer takes the agent down. Opening one on a build without pseudo-console support — anything older than Windows 10 1809 — crashed the whole agent rather than failing the session. It now returns an error naming the requirement, and the node keeps running.
  • A Linux node can no longer run itself out of overlay interfaces. Every command the agent ran inherited the open handle to the node's overlay device, so a subprocess that wedged pinned an interface — enough of them and the agent could not start at all. Handles are no longer inherited, and if the names ever do run out the error says so instead of surfacing as a generic device-busy failure.
  • A wedged filesystem mount no longer blocks startup indefinitely. Cleaning up a half-dead mount left over from a previous run could hang forever; the cleanup is now bounded and escalates to a lazy detach, with a warning saying so.

Router 0.4.47 — August 2026

What an unelevated call is actually worth on a node, and one less platform rule to unlearn.

Execution identity

  • network_directory says whether a node's unelevated account is administrative. The new info.user_is_admin qualifies info.user: where it is true, the named account is an administrator on that node — a member of the local Administrators group on Windows — so admin: false there drops from the node's ambient identity to Administrator and no further. A name on its own overstated what unelevated bought, and on Windows the administrative case is the common one, because the installer captures the interactive desktop user.
  • It is reported, never enforced. Nothing refuses on it, and it is not a reason to avoid admin: false — that stays the right default for work that does not need elevation. Unlike a Unix user: root, the two identities remain genuinely distinct, so there is nothing incoherent to refuse.
  • Three states, like its neighbours. true, present-and-false (the node checked and the account is ordinary), and absent — nobody checked, which covers an agent too old to report it and a node with no second identity to describe. Absent is not false.
  • The admin guidance no longer states a rule about Windows. It read "on every Windows node, which has no unelevated identity at all", which stopped being true the moment a configured account became a second identity there — and it failed in the dangerous direction, sending callers to LocalSystem for work a node's owner had scoped to an ordinary account. What holds on every node is stated instead: a Windows node has a second identity only where its owner configured one, and where none is, admin: false is refused like on any other node whose only identity is the superuser. A fleet upgrades one node at a time, so the refusal from the node you actually called is what tells you which kind it is.

Router 0.4.46 — August 2026

Which network a call lands in, for the accounts where that had stopped being obvious.

Working across organizations

  • Omitting the network name resolves inside your personal organization. Joining a team organization used to make every call that leaves network_name out ambiguous — you had two networks in the account's eyes, so the answer was invalid_args asking which. Your sole personal network now wins outright, however many networks the shared orgs you belong to hold. Only an ambiguous personal side — two personal networks, or none at all beside several shared ones — still asks. Solo accounts and anyone whose networks all live in one organization are unaffected.
  • A shared organization's network is never landed in by accident. The same rule applies to the node reference on command, file and the fs_* tools, to the selector fields on the subscribe and labelling tools, and to provision_node, register_node and network_delete. Name the network to use one.
  • A network id now binds exactly. Selector-taking tools resolved network_id by looking the network up and then re-resolving its name, and a name that exists in two of your organizations resolves personal-first — so the id of a shared org's home silently rebound to your own home. Membership is checked against the id itself now, and the id reaches the network it identifies. Passing network_id and network_name together is still invalid_args.

See Which network a call lands in.


Agent 0.4.45 — August 2026

Nodes say what machine they are, and writing a file stops requiring you to build the path to it first.

Directory

  • A node now reports the model of the machine it runs on. network_directory carries it as info.hardware_modelDS918+, Raspberry Pi 5 Model B Rev 1.0, Macmini9,1, NUC11TNHi5. Nothing else in that block could answer it: platform and kernel_arch describe the software and the instruction set, so a fleet of arm64 Debian nodes read as identical rows whether the board was a Pi Zero or a NAS. It is what decides whether a workload belongs on a machine.
  • Empty is a legitimate answer. A VM, a container, or a whitebox board whose vendor never filled in its DMI fields has nothing to report, and OEM placeholders (To be filled by O.E.M., Default string) are reported as empty rather than passed through as though they named the hardware. Nodes below 0.4.45 report empty too, so read it as a hint rather than a filter.

Files

  • Writing to a path whose directories don't exist yet creates them. fs_write, file with op=write, and op=fetch_url build the missing parent chain instead of failing on the first call, so none of them needs an fs_mkdir with recursive: true in front of it any more. One round-trip less on every write into a directory tree you are also creating.
  • It grants nothing new. That recursive fs_mkdir was always available to the same caller, and the destination is still checked against its deepest existing ancestor before anything is created — a path outside the node's allow-roots is refused exactly as before.
  • The directories belong to whoever will own the file. They are created as the identity the call's admin flag selected, at both identities, so an admin: false write is not handed a root-owned tree to land in.

Agent 0.4.44 — August 2026

The Windows second identity loses its separate switch, and a freshly provisioned node stops refusing every unelevated call.

Execution identity

  • A configured user is now the whole of the second identity, on every platform. The windows-user-identity key that 0.4.43 introduced is retired, and an upgrading agent removes it from the config file. Windows follows the same one-switch rule as Unix: name an account and the node has a second identity; name none and it has one. See A second identity on Windows.
  • A Windows node that recorded an install account gains that identity when it upgrades. 0.4.43 captured the signed-in desktop account into user but did nothing with it unless the key was on. Nothing gains privilege in the flip: unelevated file calls that were refused now run as that unprivileged account, command and terminal sessions still refuse an unelevated call rather than running as LocalSystem, and admin: true is unchanged.
  • nobgp config --user <account> is accepted on Windows. It no longer needs a key to be on first, and naming an ordinary local account is no longer mistaken for naming the superuser. An account that does not exist on the machine is still refused at the point of setting.
  • network_directory reports the account on any node that resolved one, Windows included — where it is what fs_* and file calls run as, while execution there still refuses.

Provisioning

  • A provisioned node serves unelevated calls from its first boot. The guest image now ships an unprivileged account and names it, so admin: false works immediately. Before this a fresh container's only identity was root, every unelevated call was refused, and the fix was to create an account by hand on a node you had provisioned precisely so you would not have to.
  • NOBGP_USER is honoured on an installed agent, not only an env-only one. Naming the unelevated account in a container image or task definition used to stop working on the node's first restart, once registration had written a config file. The environment outranks the config file for the agent's process lifetime, and the value is not copied into the profile — a provisioned node's config stays free of a user it never chose. See Environment Variables.
  • nobgp config --user says when NOBGP_USER outranks what you just saved. The saved value is the one that survives the variable being removed, so it is written either way — but it does not take effect while the environment names a different account, and that is now stated at the write instead of discovered later.

Installing and upgrading

  • The Windows installer no longer reports a registered node as a registration failure. A node that registers but whose service does not come up exits 3, which the installer read as any other failure and answered with "Registration failed. To get started, register the agent" — directly contradicting the line above it. It now says the node is registered and names the one command that fixes the service.
  • Re-running the installer over a running agent no longer calls the service down. An upgrade swaps the binary under a live service, which schedules its own restart; the installer's own start attempt then raced it and was refused. It waits out that window before declaring anything, instead of failing an install that succeeded.
  • Errors print once, and flag mistakes print at all. Some commands printed their message twice; others — a mistyped flag or the wrong number of arguments — printed nothing, because the failure happened before the command ran.

Router 0.4.45 — August 2026

A pass over the lifecycle tools, from an end-to-end run that provisioned a node, built something on it and published it — every step of which worked, and three of which cost a call they should not have.

Provisioning

  • provision_node now says how to wait for the node it just created. The node row is minted when the container's agent registers, which is after the call returns, so a directory lookup issued immediately finds nothing — previously the only way forward was to sleep and retry blind. The tool now names the readiness recipe: subscribe to presence on the network without a node_names selector (a selector resolves against nodes that already exist, so it cannot name one that does not), then tail until the returned name arrives as registered and then online. Polling the directory still works and is the only option on an isolated network, which has no event bus.
  • It returns network_name and node_name. The resolved network and the normalized name the container will register under — which can differ in case from the name you asked for, and is therefore the one to match on. Together they are the readiness subscription and the directory filter, both of which previously had to be reconstructed by hand.
  • resource_summary reports the shape actually allocated. CPU and memory round up to the next power of two within the provider's limits, and the summary now reflects the post-rounding result rather than echoing the request. It is what the node costs.
  • There is still deliberately no node_id, and the description says so rather than leaving its absence to be discovered. Nothing can return an id for a row that does not exist yet.
  • deprovision_node is idempotent. Tearing down a task the provider no longer runs — stopped out of band, or aged out of its task store — now succeeds with a note instead of returning an internal error. Already-stopped is the desired state reached earlier, on a resource that is not billing; the failure shape invited retries that could never succeed. The call still records the completion the out-of-band stop skipped.

Directory

  • A filtered network_directory no longer returns networks that matched nothing. A glob aimed at one node used to come back with every network you belong to, all but one carrying an empty nodes array, and you paid context for each. Two exceptions, both deliberate: a network you named with network_name is returned even when empty, because its presence is the answer that it exists and holds no matching node; and brief alone prunes nothing, since it narrows what is said about a node rather than which nodes match.
  • Brace expansion in a glob is refused rather than matching nothing. {web,db}* is not shell syntax to the matcher — it is a pattern for a node literally named {web,db}… — so it returned zero nodes and no error, which reads as "none of your nodes are online". It now returns invalid_args naming the cause. It was the one shape in these filters that failed quietly.

Services

  • service_publish says what a successful publish does and does not prove. It proves the route was recorded, not that the backend is alive — and the public URL cannot settle it either, since with authentication on it answers a redirect to the login flow whether the backend responds or not. The tool now points at the two checks that do work: run the target fetch on the node with command, and read the backend's own log once a browser has traversed the proxy.

Clients holding a stale tool list

  • A boolean sent as a string is now accepted. A client whose cached tools/list predates a parameter sends it untyped, and Claude-family clients serialize untyped values as strings — so admin: "true" was rejected with type: true has type "string", want "boolean", an error that blames the type and reads as a bug in the tool. "true", "false", "1" and "0" on a boolean parameter are coerced. Booleans only: their string form is unambiguous, while a stringified array or integer is not, and for those the fix is a refreshed schema rather than a lenient server.
  • Every argument-validation rejection now names the actual fix — refetch the tool list — and the router version that refused you, so a mismatch you can correlate. A field removed since your snapshot still fails, but it now costs one call instead of a wrong mental model designed around a field that appears not to exist. Ordinary caller mistakes are left unadorned. See When your client's tool list is stale.

Agent 0.4.43 — August 2026

Windows gets the second identity every other platform has had — for file operations, and only where its owner asks for it.

Execution identity

  • A Windows node can now offer a second identity, opt-in and file-only. Set windows-user-identity: true alongside a user in the node's config and fs_* / file calls sent with admin: false run as that account, a genuine privilege drop rather than an authorization check. The account is obtained by name — nothing prompts for a password and no credential is stored on the machine. (The separate key is gone in 0.4.44, where a configured user is the whole of it — see A second identity on Windows.)
  • Execution on such a node refuses rather than falling back to LocalSystem. command, terminal sessions and dispatched bus commands still spawn in the service's own context, so an unelevated call there answers failed_precondition naming its two ways out — admin: true to run as LocalSystem deliberately, or turn the key off. Serving it would have run as LocalSystem while the node had just said the work would run as the configured account, which is the silent elevation the whole rule exists to remove.
  • The key had never taken effect on any node. It shipped in 0.4.35 but the account name it needs was cleared on every Windows node at config load, so turning it on changed nothing. It is reachable now, and it is still off by default — no node's behaviour changes on upgrade.
  • nobgp config --user <account> is accepted on Windows when the key is on. It stays refused while the key is off, where the node has one identity and the name could not be honoured. Naming an account that does not exist is still refused at the point of setting.
  • A Windows install records the signed-in desktop account in user. The same thing the Unix installer does with the account that ran sudo. With windows-user-identity unset the name is ignored, and it is captured anyway so that enabling the second identity later does not require finding out who installed the machine. A headless or unattended install captures nothing.
  • network_directory reports that account on a node where the key is on, and empty where it is off — the resolved identity, as on every other platform, never the raw config string.

Diagnostics

  • A node that fails to start now says why in the record. An agent that dies during startup — after it has registered but before it serves anything — closes its control connection with the reason instead of a fixed string. Such a node reconnects and fails again indefinitely and cannot be reached to be asked, so that line was the only thing it ever said about itself.

Agent 0.4.42 — August 2026

Router 0.4.43 below made both halves of the elevation gate answerable before a call — on the public server. This is the same answer for a node standing on its own box, where the tool that reports it fleet-wide is deliberately out of reach.

Execution identity

  • nobgp status now reports which identity an unelevated call gets. The allow: block carries a fourth line: unelevated, the account an admin: false operation actually runs as here — or unelevated_refusal in its place, naming why nothing can. allow.admin only ever answered "may anyone be the superuser here", which is half the gate; the other half was reachable only by making a call and reading the refusal. See The fourth line is not a veto.
  • It is read from the gate, not from the user key. The two differ exactly when it matters — a configured account that has since been deleted, or a user: root, reads fine in the config file and is refused at the call. nobgp show prints what was configured; this prints what the node will do with it.
  • The local status tool reports the same block, which is the point of the change: network_directory is organization-scoped and deliberately absent from a node's own local MCP surface, so an assistant standing on the machine had no way to ask at all. It answers for that node only — a peer's identity is still network_directory's to report, from the public server.
  • The local endpoint now states the identity rule at the handshake. A node's MCP server carries the same choice-first guidance as the public one, plus the two things that are true only there: your own tool list says whether you may elevate (command present means the node holds manage; absent means observe, where every admin: true returns forbidden), and a peer's user and allow_admin cannot be looked up from that endpoint. See Which identity a call gets, from here.
Reconnect your MCP client

The instructions and the status response fields are read at connection time, so a client holding a cached session will not see them until it reconnects.


Router 0.4.43 — August 2026

Both halves of the elevation gate are now answerable before a call, and the advice on choosing admin is stated the way round that does not cost a wasted call.

Execution identity

  • whoami now reports can_elevate — whether you may send admin: true at all. false means every elevated call comes back forbidden, so there is no point sending one; true means the router's gate passes, though a node whose owner set allow-admin: false still refuses. It is derived from the same check the real call runs, so it cannot drift from the gate it predicts. Together with network_directory's per-node user and allow_admin, the two gates can now both be read up front instead of discovered one refusal at a time.
  • It also reports orgs[] — the organizations you belong to, your role in each, and can_elevate scoped to that organization. Elevation is decided per the organization that owns the target node, so a caller who is an Admin in one organization and a Member in another gets the per-organization detail and no top-level verdict: a single answer would be wrong for some of their nodes, and a prediction that disagrees with the gate is worse than none. A caller whose role is the same everywhere — which includes every solo account — gets the straight answer.
  • A caller who belongs to no organization is told false, not "it depends". There is nothing for it to depend on: such a caller resolves no node to elevate on in the first place.
  • network_directory now returns org_id on each network. It is the join key for orgs[] — without it the roles were a fact you could read and not use, since nothing said which organization owned the node you were aiming at.
  • The guidance on choosing admin is now choice-first: send it for work that needs the superuser, omit it otherwise. The previous wording, in the tool descriptions and here, was to try admin: false first and treat the refusal as the signal to elevate — which guarantees one failed call on every node whose only identity is the superuser, and as agents reach 0.4.41 that is a large share of them (service installs, bare-root containers, all of Windows). Each of the two refusals names its single retry, so a wrong guess still costs one call and never a loop. See Choosing which to send.
  • The same rule now arrives at the MCP handshake, before any tool has been chosen. Tool-field descriptions are only read after a client has picked a tool, which is already after it has picked an identity.
Reconnect your MCP client

can_elevate, orgs[] and org_id are new response fields, and a client holding a cached schema will not show them until it refetches the tool list. Reconnecting is enough.


Agent 0.4.41 — August 2026

The node half of unelevated never means root — router 0.4.42 below classified the refusal, and this is the agent release that makes it. Until a node runs 0.4.41 it still runs unelevated work as root rather than refusing, so the change arrives node by node as agents upgrade.

Execution identity

  • A node refuses admin: false when the identity it would run as resolves to uid 0. Three shapes reach it: no user configured on a node whose agent runs as root (a service install, a bare-root container, and every Windows node, where LocalSystem reports as uid 0), a user that no longer resolves, and user: root spelled out. The old behaviour was a silent fall-through to the agent's own identity — root — on exactly the nodes whose owners had asked for something lesser. The refusal is failed_precondition, not retryable, and names both remedies: sudo nobgp config --user <account> on the box, or retry the call with admin: true. See Unelevated never means root.
  • It binds on the resolved uid and nothing else. An agent that is already unprivileged is untouched: with no account configured it keeps serving admin: false as itself, because elevating there would reach the very same uid and demanding the flag would prevent no elevation. Windows needs no rule of its own for the same reason — LocalSystem is uid 0, so the same predicate catches it.
  • Windows nodes now refuse admin: false outright. A Windows node offers one identity, LocalSystem, and every call has to ask for it with admin: true. Where an unelevated call previously ran as LocalSystem anyway, it is refused.
  • nobgp config --user root is refused at the point of setting. Naming the superuser (or any second uid-0 account) configures a node that refuses every unelevated call while looking like it narrowed them, so it is rejected where the mistake is still in front of you — alongside the existing refusal of an account that does not exist. Name an unprivileged account, or leave --user unset and have callers pass admin: true.
  • --allow-admin=false asks the resolver, not the name. The confirmation prompt and the standing warning now report which case the node is actually in, so user: root and a deleted user read as the lockout they are rather than as the benign "sessions will run as that account" narrowing.

Directory

  • A node reports the account it resolved, not the string in its config. network_directory's info.user is empty on a node whose user was deleted or came off another machine, where it previously echoed a name nothing on that box could become — reassuring in exactly the shape that should alarm, since info.user is the one signal a caller has before choosing admin. user: root is still reported by name, and still refused.

Router 0.4.42 — August 2026

Groundwork for one rule: unelevated never means root. A node that would have to be the superuser to run "unelevated" work now says so instead of quietly being it.

Execution identity

  • A node refuses admin: false when the identity it would run as is uid 0. No account configured on a node whose agent runs as root — a service install, a bare-root container, Windows, where LocalSystem reports as uid 0 — an account that no longer resolves, or user: root spelled out. The old behaviour was to fall back to ambient root: silent, and in the escalating direction. Both remedies are in the refusal: configure an account on the box (sudo nobgp config --user deploy), or retry with admin: true if the superuser really was intended. See Unelevated never means root.
  • An empty user is a warning, not a verdict. The rule binds on the resolved uid and nothing else, so a node whose agent is already unprivileged — a rootless container, an agent started by an ordinary account — keeps serving admin: false as itself. Elevating there would reach the same uid, so there is nothing to prevent. Try admin: false first and treat a refusal as the signal to elevate.
  • The refusal is failed_precondition, not permission_denied, and not retryable. It is not a verdict on the caller — reading it as one sends an org Owner off to widen a role that was never the problem. This release is what classifies it; without it the refusal would arrive as internal / 500 / retryable and a well-behaved client would retry a permanent decision forever. The tool descriptions state the invariant too, so an AI caller retries elevated instead of reading the refusal as a dead end.
  • This is not a privilege reduction, and it is not sold as one. A node with no account moves from everything implicitly root to only admin: true, explicitly root — same privilege, reachable by the same people. What it buys is intent and auditability: an elevation appears in the record as an elevation.
  • Enforced by the node, from its own config, at the moment of the operation. It therefore arrives per node as agents upgrade; an agent that predates it runs unelevated work as root instead of refusing.

Directory

  • network_directory now tells "no account configured" apart from "the agent is too old to say". info.user has three states — a name, present-and-empty, and absent — and the empty one used to be stored as absent, so a UI had no way to distinguish them. No new field: an empty string means no account, a missing field means the node never reported one. See network_directory.
  • No derived "can this node run unelevated" flag, deliberately. It would be wrong in both directions — user: root is a non-empty name that still refuses, and an empty one on an already-unprivileged agent is served — and only the node can answer it.

Agent 0.4.40 — August 2026

File tools

  • The ceiling on a node's one-shot answer is a number that can actually fire. A node builds a directory listing, a glob match set or a search result whole before it sends it, and the ceiling on that payload was 256 MiB — above anything a search can arithmetically produce (100 000 matches at the 2 KiB per-line cap is ~207 MB), so it never refused anything, which is the same as having no ceiling. It is 64 MiB now: above any legitimate search (100 000 matches of ordinary source lines is ~15 MB) and survivable on a 512 MB single-board node, where a one-shot answer peaks at about 2.2× its own size. It binds admin: true and admin: false identically — it is a property of the machine, not of the account a node runs work as.
  • What that changes in practice: the two big walks. A recursive fs_list over an enormous tree and an fs_glob returning hundreds of thousands of paths are the calls that can reach the new ceiling, and they now come back as too_large naming what to narrow, where before they were attempted. limit / offset do not avoid it — those page a listing the node has already built in full — so narrow path, or drop recursive. A refusal that names the fix beats an allocation the node cannot survive.
  • A large answer no longer costs a node twice its size in memory. A reply too big for one frame is reassembled into a buffer sized once from the length the node declares, rather than grown frame by frame — which peaked at better than double the reply. On a small node that was the difference between fitting in RAM and swapping onto an SD card while the control channel went with it.
  • A search tells you when it did not read everything — this is the agent half of the skipped_files work in router 0.4.41 below, and 0.4.40 is the agent release that reports it. An agent skips any file over its 16 MiB per-file cap; below this release it said nothing about having done so, and the router reports 0 for a node that cannot answer. See A zero is only a zero when skipped_files is 0.

Router 0.4.41 — August 2026

Search

  • A search tells you when it did not read everything. An agent skips any file over its 16 MiB per-file cap without reading it, and said nothing about having done so — so an fs_grep over a directory holding a rotated log or a database dump returned count: 0, scanned_files: 0 and no error, which is exactly what "nothing matched" looks like. Measured on a 30 MB corpus: 0 matches; the same bytes split into three 10 MB files: 15 000. The response now carries skipped_files, counting the files skipped for size only — binary files and noise directories are skipped by design and are not counted — and the message says so in words too (0 match(es) across 0 file(s), 1 file(s) too large to search). fs_grep_subscribe names it in each node's completion event. Read it before believing a zero.
  • A zero from an old node means "not reported". The field is always present in the response, so its absence is not how you spot a node that cannot report it: an agent below 0.4.40 sends nothing and the router reports 0. Check the node's build_version before trusting a zero from an old node.

Router 0.4.40 — August 2026

Organization roles now actually bind on the tools that write a node or a service. Solo accounts and personal organizations are unaffected — you are the Owner of your own org, so nothing here can refuse you.

Roles

  • The Member tier is enforced where it was previously only described. The role matrix has always said Owner/Admin/Member, but the tools that write an objectservice_publish, service_update, service_delete, service_share, register_node, provision_node, deprovision_node, node_label — checked only that you belonged to the organization that owns the target, never which role you held there. "Member or above" is the same set as "in the org at all", so the Member rows had never refused anyone. They are checked now. If you are a Member of a shared organization, the operational set is unchanged: registering and provisioning nodes, publishing, updating, sharing and deleting services, and running command and the file tools all stay Member actions.
  • Turning a service's authentication off now needs an Owner or Admin. Publishing is the operational act any Member does, and everything a Member publishes is reachable only by people who can authenticate; clearing auth_required is the one edit that changes the audience from your organization to anyone with the URL. It is gated on service_publish (auth_required: false) and on service_update, and below Admin the call returns forbidden with nothing published or changed. The gate is on the transition only: raising authentication is never gated, editing a service that is already public is never gated, and passing authorized_emails forces authentication back on, so it is never gated either.
  • node_label now needs an Owner or Admin, the same tier as node_grant. Labels are the selector a grant's targets are written in, and targets are evaluated live, so relabelling a node moves it in or out of a granted node's reach with no grant touched. A tool that can do that belongs at the grant's tier. A Member gets forbidden; where a selector spans several organizations, a refusal in any one of them fails the whole call before a single label is written.
  • Nothing was quietly downgraded. Every one of these refusals is an error you can see, not a silent demotion — the same rule admin: true has followed since 0.4.33. See Roles & Permissions for the full matrix, cell by cell.
Breaking for organizations with Members

A Member who turns a service's authentication off, or who labels a node, now gets forbidden (HTTP 403) — service.unauthenticated requires a higher role in this organization, or node.role requires a higher role in this organization. The check applies from this release with no compatibility window. If either is part of someone's routine, either raise their role or move the step to an Owner or Admin.


Agent 0.4.39 — August 2026

Corrections to two things 0.4.38 changed: what a walk of a symlinked root calls its results, and when a Windows command's stderr reaches you.

File tools

  • A walk names its results under the root you asked about. Resolving a symlinked root is what makes the walk descend at all, but 0.4.38 also let the resolved path stand in for the requested one in the answer: fs_list /tmp on macOS described /private/tmp/…, fs_list /bin on a usr-merged Linux said /usr/bin/…, and fs_stat reported a symlink's target instead of the link. Anything comparing an entry against the root it requested stopped matching. The two roles are separate now — descend the real directory, report under the path you sent — across fs_list (recursive and non-recursive, which was renamed without ever having had the descent problem), fs_glob, fs_grep and fs_stat, and the equivalent file ops.
  • A walk begins where the permission check approved. allow-roots is matched after cleaning a path and then resolving its symlinks; a walk that resolved the caller's string as sent asked a different question, because resolution drops a trailing .. relative to a link's target rather than to its lexical parent. Both halves clean first now, so a walk cannot start somewhere the check never vetted. Nothing about a correctly-formed path changes.

Command execution

  • Windows: a finished line no longer waits behind an unfinished one. Every Windows command session's stderr passes through the filter that decodes PowerShell's CLIXML, and that filter held completed lines whenever the stream happened to sit mid-line — so a command that printed a prompt or a progress line and then worked quietly showed nothing until it wrote another newline or exited. Output was delayed, never lost. This is the whole stderr leg, not only shell: "powershell", so anything that prompts without a trailing newline is the sharp case.

Router 0.4.38 — August 2026

Error codes

  • A search pattern that will not compile is invalid_args. An fs_grep or fs_grep_subscribe pattern that is not valid RE2 — an unbalanced bracket is the usual way — came back as internal with retryable: true, which told a client to retry a typo forever. It is invalid_args (HTTP 400) now, and it is recognised ahead of every other phrase, because no other agent failure says error parsing regexp.
  • Your own words are no longer read as a node's refusal. An agent error quotes the path and the pattern it was given back, so a phrase you typed could be classified as a decision the node never made: searching for allow-roots( or stat-ing a file named allow-roots.yaml answered permission_denied (403), a pattern containing does not exist( answered not_found, too large( answered too_large. A node's vetoes are matched on the node's own full wording now, never on a bare token a caller can type. All of these answer invalid_args.
  • This is the classification path used for agents below 0.4.38, and for any agent's ordinary argument errors — a node states its own code for what it refuses, not for a pattern that failed to compile. See When the failure came from the node.

Agent 0.4.38 — August 2026

This is the agent half of the error-code work that shipped in router 0.4.37, together with fixes for several ways the file and command tools could answer wrongly. The execution-identity model itself did not change — everything here is around it.

File tools

  • One oversized answer no longer wedges a node's file tools. On a node with a configured user — which is most of them, since registration sets one — a single reply too large to send left fragments behind in the agent's privilege-dropped worker, and every later unelevated file call on that node read those fragments as its own answer and failed: fs_stat, fs_list, fs_read, fs_write, every sub-op of a file batch. It lasted until the agent was restarted, and it was reachable by an ordinary large fs_grep or recursive fs_list with no elevation involved. A large reply is now split across frames, and any framing failure retires the worker instead of poisoning the next call.
  • The size ceiling is the same at both identities. The same search over the same files returned 14 MB with admin: true and failed with an over-cap error without it — so the account a node runs work as was directly visible in what its file tools could return. One ceiling now binds both, and the refusal you get for genuinely exceeding it is too_large, not internal.
  • macOS: walking /tmp, /etc or /var finds the files. All three are symlinks into /private, and a walk never follows a symlink — including the one it is handed — so fs_list /tmp with recursive returned {"count": 0, "entries": []} and fs_grep /etc reported scanned_files: 0, both successfully. A caller could not tell "nothing matched" from "we never looked", on the three most commonly walked roots on the platform. The walk root is resolved now. Nothing else about the never-follow rule changed: what a walk finds below its root is unaffected.
  • An operation acts on the path its permission check approved. The check resolved symlinks to decide containment and then discarded the answer, leaving the operation to re-walk the string the caller sent. Every directory above the final component is now addressed as the check resolved it. fs_stat, fs_delete and fs_write still mean the final component exactly as you wrote it — a fs_stat of a link still describes the link, not its target.

Errors

  • A node names its own refusal code on the wire. Only the agent knows whether it refused or faulted; previously the router inferred that from the wording of the node's message, which is complete only for the refusals that existed when the table was written. Router 0.4.37 classified the six known cases for every agent, and from this release the node states the code directly, so a refusal phrased in a way that table does not anticipate no longer arrives as a retryable internal. The code is advisory — the router validates it against the codes an agent may name.

Command execution

  • A signalled command reports 128 + the signal number. It used to report exit_code: -1 with state: "exited" and nothing naming a signal, which is indistinguishable from a program that genuinely returned -1 — so a killed command and a failed one read the same, including to whatever ran next. 143 is SIGTERM, 137 is SIGKILL, 130 is SIGINT, the encoding every shell already uses. It also makes a node agree with itself: a command whose child was signalled already reported this, because the shell does that translation. Windows is unchanged and still reports 1.
  • Windows: quotes reach cmd.exe intact. The command line was being escaped for a program that parses its own line, which is the wrong rule for cmd.exe: echo test "quoted string" came back with literal \" around the quoted part and dir /b "C:\Program Files\nobgp" failed outright. The line is handed over verbatim now, so the quoting rules that apply are the ones you would get at a Command Prompt — and the command tool's standing promise that backslashes are literal is true again.
  • Windows: shell: "powershell" output is plain text. PowerShell serializes its error, warning, verbose and progress streams as CLIXML whenever stderr is a pipe, and no switch turns that off, so real output arrived sandwiched between #< CLIXML and an <Objs …> blob on every call. The agent decodes that stream rather than stripping it — a real error's text lives inside the blob, and dropping it would trade garbled output for silent failure.

Router 0.4.37 — August 2026

Error codes

  • A node's refusal now says it is a refusal. A node owner's veto — allow-admin: false, a path outside allow-roots, a domain dropped from allow-tools, or the agent's own configuration directory — came back as internal with retryable: true, so a well-behaved client retried a permanent decision forever. All four are permission_denied (HTTP 403, not retryable) now. Two more joined them: a reply too large for the node to send in one answer is too_large (413) rather than an unexplained 500, and a node that is not running privileged enough to become the account it was asked to is failed_precondition. See When the failure came from the node.
  • The code is the same wherever the refusal lands. A veto that surfaces partway through a read, a write, a directory listing or a search is now coded exactly as one that surfaces when the call opens — previously only the opening case was classified, so the same refusal read differently depending on when the node got around to saying it.
  • Nothing to change on your side. If you were branching on internal + retryable to decide whether to retry, that was the right thing to do and still is; the population of nodes that answers wrongly is what shrank. Ordinary failures are untouched: a missing path is still not_found, an unreadable one permission_denied, a non-matching old_string still invalid_args.

Agent 0.4.37 — July 2026

Execution identity

  • An elevated call is no longer bound by the node's allow-roots. Path confinement binds the identity the node's owner configured; the ambient identity is not bound by it, so an elevated file operation reaches paths an ordinary one is refused. Reaching it needs both gates first — an org Owner/Admin (or a node's manage grant) and the node's allow-admin. Earlier agents confined both identities, which is why elevating to escape allow-roots did nothing before this release. See Node Access Control.
  • The agent's own configuration directory stays refused at both identities. It holds the node's key, its JWT and the settings the vetoes are read from, so elevating is not a way to read a node's credentials. Unchanged, and now pinned at both identities.
  • The decision follows the admin flag, not the resolved uid. That matters on Windows, where both identities are LocalSystem: keyed on the uid, every Windows file call would have shed allow-roots and the owner's confinement would have evaporated on the whole platform.

File search and containment

  • fs_grep works. On earlier agents it did not — at all. Every call was refused before the search began, because the agent required an absolute path and a grep carries its roots in paths instead, leaving that field empty. Nothing surfaced it while the tool was REST-only and unlisted; router 0.4.35 put it in every client's tool list, so on an agent below this release fs_grep and fs_grep_subscribe fail with path must be absolute: "". Upgrade the agent to use either. The same guard never applied allow-roots to a search, since it read the empty field rather than paths; every entry in paths is vetted now, and a permitted root cannot smuggle a forbidden one in beside it.
  • A recursive walk no longer enumerates the agent's own directory. fs_list with recursive: true, and fs_glob, listed default.key, default.jwt and default.yml with their sizes and timestamps — one call after fs_read refused those same paths by name. fs_grep was worse: it returned the node's key and JWT line by line. Each op checked containment once for the path it was handed and never again as it descended. All three now re-ask for every directory they enter, and prune. The directory itself is still listed, because a non-recursive listing of its parent already names it; nothing inside it is.
  • fs_glob with follow_symlinks no longer reports targets outside allow-roots. Following a link could leave the node owner's confinement entirely, which is the one option that explicitly asks to leave the walk root. This narrows a documented behaviour deliberately: it is the answer fs_read already gives for those paths, so glob stops advertising what the node will not serve.
  • A node with a configured user now applies its owner's allow-roots to walks. Unelevated file work runs in a privilege-dropped worker, and that worker could not read the node's own config — so it fell back to the permissive default and the owner's confinement was not applied to fs_list, fs_glob or fs_grep as they descended. The roots travel with the request now. Nodes with no configured user were never affected.

Node access control

  • allow-roots still does not bound command. Its workdir is not path-checked and is not meant to be: the command string itself is unconstrained, so a confined workdir would refuse what cd reaches anyway. Bounding what a node will execute is allow-tools without command, or allow-admin: false.

Router 0.4.36 — July 2026

Execution identity

  • Asking to run as the superuser now requires the authority to ask. admin: true needs an org Owner or Admin — a Member is refused with forbidden rather than quietly downgraded. It applies everywhere the flag does: command, command_subscribe, file, the fs_* tools, and service_publish / service_update. Previously the only check was the node's own allow-admin, which defaults to permissive, so anyone who could reach a node could be root on it. A Member keeps every ordinary capability — commands, files, services — at the node's configured account. See Execution identity.
  • From a node's own local MCP server, the node's grant decides. A node holding manage may elevate on its peers; one holding observe may not, whatever role its owner holds. That endpoint is reachable by anything that can read the node's token, so it is bounded by its grant rather than by the person who granted it.
  • An elevated cohort dispatch is all-or-nothing. command_subscribe checks authority once against the network's organization before selecting any node, so you never get a fan-out that ran as root on some peers and was refused on others.
  • A forbidden on an elevated call is not worth retrying. Retry with admin dropped — that runs as the node's configured account and is what most work wants. Every affected tool description now says so.
  • admin is available on the per-op fs_* tools. fs_read, fs_write, fs_list, fs_stat, fs_delete, fs_mkdir, fs_edit and fs_grep accept the flag; before this only file did, and an elevated file operation meant reaching for the multiplexed tool. fs_grep_subscribe deliberately does not — a subscription outlives the call that created it, so its identity is not the caller's to choose.
  • Updating an already-elevated terminal service needs the same authority as creating one. The gate is on the identity the service will run as after the change, not on whether the request mentioned admin: a stored admin: true hands root to whatever command, workdir or auth setting you put in it. Lowering it never needs authority.

Agent 0.4.36 — July 2026

Configuration file

  • An upgraded node now tidies its config file on the first restart. 0.4.35 changed the file to record choices rather than defaults, but only rewrote it the next time the agent had something of its own to record — the mount point, a learned QUIC value, the local MCP port. On a settled node none of those fire again, so a node upgraded to 0.4.35 kept its pre-upgrade file: every default still frozen in as an explicit setting, retired keys still present. The agent sweeps the file when it starts now. No effective setting changes either way. See Configuration File.
  • The file is touched only when its contents would differ. A rewrite that would produce the same bytes is skipped, so on a node whose config is already correct — the usual case once it has been swept — startup changes nothing, not even the file's timestamp. Removing retired keys is atomic too, closing the same truncate-then-write window 0.4.35 closed for the main write path.

Router 0.4.35 — July 2026

  • fs_grep is on MCP. It had been reachable only over REST — registered and in no client's tool list. AI clients can now search a node's files with an RE2 regex directly. Reconnect your MCP client after the upgrade so it picks the tool up. ⚠ Needs agent 0.4.37+: on earlier agents the search is refused before it starts (path must be absolute: "") — a bug that had been invisible for as long as no client could call the tool.
  • Results arrive one of two ways, and you choose. Send no progressToken and the response carries every hit in matches[]. Send one — which is how a client declares it consumes notifications — and each match arrives as a notifications/progress message as the walk finds it, with the response setting streamed_as_progress: true and omitting matches[] rather than sending everything twice. Either way count, scanned_files, truncated and duration_ms are populated, so read count, not the length of matches, to know whether a search found anything. See fs_grep.
  • fs_grep works from a node's own local server too, at the observe tier, where it is always synchronous: that endpoint has no channel for a mid-call push, so it ignores a progressToken and returns the complete result — the same thing a caller that sent no token gets. The two surfaces differ by authority, never by transport.

Event bus

  • New tool: fs_grep_subscribe — the same RE2 walk across a whole cohort, delivered through the event bus instead of one big response. It returns immediately with a subscription_id; matches arrive as events attributed by source_id to the node each one is on, read with event_tail and ended with event_unsubscribe. Like a command dispatch it always ends: every node reports once, and outstanding: 0 in event_tail's progress block means the search is complete. Available at the observe tier on a node's own server, and the only way to read search results incrementally there. See Searching a fleet.
  • event_subscriptions now reports grep as a fourth subscription kind, alongside fs, command and presence.

Agent 0.4.35 — July 2026

Configuration file

  • The config file records choices, not defaults. A key is written only when its value differs from the default; everything else appears as a commented # key: value line under a short block explaining that uncommenting one pins it. Older agents froze every default into the file the first time nobgp config ran, so a node carried explicit values nobody chose — and a later change of default could never reach it. A 0.4.35 agent tidies that on its next write, with no change to any effective setting. The file is agent-generated: hand-written comments do not survive a rewrite, and keys the agent no longer reads are swept out with a note naming their successor (event-sourcesallow-tools, event-watch-rootsallow-roots). See Configuration File.
  • The file is replaced atomically. It now holds things a node cannot re-derive — the overlay slice, the local MCP port and token, the learned QUIC endpoint — and the old write truncated before it wrote. An upgrade restart or a power cut inside that window brought the node back without them, re-minting an MCP token every nobgp mcp install registration had frozen into a client config, and re-picking an overlay slice.

File search

  • fs_grep works again on nodes with a configured user. Agent 0.4.34 moved file work onto a privilege-dropped worker and refused the incremental (streaming) form of fs_grep there, which is most nodes — registration sets user to the installing account. The call now falls back to returning the whole result in one response instead of being refused; the only difference from a search that runs as the node's own identity is that matches arrive when the walk finishes rather than as they are found.

Agent 0.4.34 — July 2026

Execution identity

  • This is the agent release that implements the two identities. The admin flag on command, command_subscribe, file and published terminals shipped router-side in 0.4.33; a node has to be on agent 0.4.34 for it to mean anything. An older agent ignores it and runs the work as its configured user — so admin: true against a node you have not upgraded does not elevate, it just fails to. The skew is safe in both directions: neither side ever elevates on its own.
  • File operations really do drop privilege now. The agent changes identity before it opens the file, so ownership and permissions are the ones that account would get, and the atomic rename that finalises a write is checked against it too — a caller cannot create bytes as one account and land them somewhere only another could write. Until this release the file tools ran as the agent itself and merely checked whether the account could have done it, which is why a path readable through fs_read could be denied through command on the same node.
  • An agent that is not running as root cannot act as user. Assuming another account is a superuser privilege. On a node where the agent runs unprivileged — a foreground nobgp agent started by an ordinary user — a file operation that would run as user is now refused, naming the reason, instead of running as the agent's own account. Either run the agent as root or clear user. Command sessions already behaved this way, and service installs are unaffected.

Node access control

  • The file tools never serve the agent's own directory. Every path inside /etc/nobgp (/usr/local/etc/nobgp on macOS, C:\ProgramData\nobgp on Windows) is refused to the fs tools and fs watches, with symlinks resolved first. No allow-roots value opens it and neither does admin: true. That directory holds the node's network key, its JWT and the config the capability keys are read from — with the permissive defaults, all of it was one fs_read away, and a caller holding fs could edit away the veto meant to confine it. command is unchanged: a session running as an account that can read those files still can. See Node Access Control.
  • allow-admin: false now refuses the two cases it always claimed to. The setting is documented as refusing anything that resolves to uid 0, but a request that asked for no particular privilege was still served as root on the nodes where that is the fallback — a container, a bare-root install — and so was work on a node whose configured user is itself a superuser account. Both are refused now. If you have such a node carrying allow-admin: false, remote execution on it stops working on upgrade, which is what the setting has been describing all along; give the veto somewhere to land with sudo nobgp config --user=<account> first.
  • Windows: allow-admin: false now covers file operations too. Since 0.4.33 it refused commands and terminal sessions there, but every file operation was still served as LocalSystem. It is one answer for all of them now.
  • The lockout warnings say what is actually true. The nobgp config warning and the allow.warning field in nobgp status used to claim a published terminal service was unaffected by these keys. It has not been since 0.4.33; both now state plainly that terminal services are refused along with the tools and that the way back in is on-device.

Router 0.4.33 — July 2026

Command execution

  • One boolean now decides who work runs as. username is gone from command, command_subscribe, service_publish and service_update, replaced by admin: a node offers exactly two identities and this picks between them. Omitted or false runs as the account the node's owner configured; true runs as the node's ambient identity, which a node set to allow-admin: false refuses rather than downgrades. Calls that still pass username are rejected with invalid_args — replace username: "root" with admin: true, and drop the field everywhere else. Reconnect your MCP client after the upgrade so it refetches the schemas. See Execution identity.
  • Why the account name went away. It never meant one thing: root could be a spelling or a user id, and on Windows the agent ignored the name entirely and ran as LocalSystem — so a session asking for another account had never once run as it. Two identities is also what makes Windows parity reachable. To run as some third account, use su or runas inside the command itself.
  • File operations now run as that identity too, and file takes admin. The agent drops privilege before opening the file, so ownership and permissions are the ones that account would get — a file written with admin: false is owned by the node's configured user, not by root. Previously the file tools always ran as the agent's own account, which is why a path readable through fs_read could be denied through command on the same node. The per-op fs_* tools carry no admin and always act as the configured user; use file when an operation needs elevation.

Published terminals

  • Terminal services that named an account have moved to one of the two identities. A terminal published with username: "root" now carries admin: true and behaves exactly as before. Every other named account now runs as its node's configured user — less privileged than what it asked for, which is the safe direction, and on Windows the direction it was already running in. Terminals that named no account are unaffected. If one of yours needs elevation, set admin: true with service_update.

Discovery

  • network_directory reports what admin will mean on each node. Each node's info block now carries user — the account unelevated work runs as, empty when none is configured, which is the case worth noticing because admin: false then resolves to root — and allow_admin, false when the node's owner refused elevation outright. Both are set on the node only, and both are absent on agents too old to report them; absent is not the same claim as empty or false. See network_directory.

Agent 0.4.33 — July 2026

Node access control

  • A node's capability settings now govern published terminal services too. allow-tools and allow-admin previously bounded only the MCP tools and event-bus sources; a terminal service published to run as root therefore handed out root on a node whose owner had set allow-admin: false. One rule applies to every execution surface now: a node that no longer serves the command domain refuses the browser terminal, and an elevated terminal is refused rather than quietly run as another account. If you narrow a node behind CGNAT, plan for on-device access first — see Node Access Control.
  • allow-admin: false is honoured on Windows. Windows has no unprivileged identity to drop to, so earlier agents logged that they could not apply the setting and kept admin enabled. It is now honoured as the only answer the platform can give: remote execution on that node is refused entirely. Existing Windows nodes carrying allow-admin: false in their config will stop serving remote execution on upgrade.
  • nobgp config checks the two identity settings before it writes them. --user naming an account that does not exist is refused at the point of setting, instead of failing at every session later; on Windows --user is refused outright, since there is nothing to switch to. --allow-admin=false says what it costs — with a user configured it states that sessions will run as that account, and where it turns remote execution off entirely it asks for confirmation (a scripted run proceeds with a warning in the log).
  • The --user help text said the opposite of what the agent does. It described --user as a restriction that only applies under allow-admin: false. It is the default account for command sessions, dispatched bus commands and file operations whenever the caller names none — registration sets it to the installing account, so an ordinary sudo install has never run sessions as root. allow-admin: false additionally pins work to it. The CLI reference has the corrected description.

Two user-facing messages shipped in this release still describing the old scope — the nobgp config warning and the allow.warning field in nobgp status both said a published terminal service was unaffected. Corrected in agent 0.4.34; on 0.4.33 treat on-device access as the way back.


Router 0.4.32 — July 2026

Command execution

  • One field now decides who a command runs as. The admin option is gone from command and command_subscribe; naming the superuser (username: "root") is how you ask for elevation. The pair existed because "Administrator is not a user" on Windows — a distinction the agent already enforces by refusing a username there outright, so it did not need to reach every caller. Calls that still pass admin are rejected with invalid_args — replace admin: true with username: "root". Everything else is unchanged: the default is still the node's configured user (not root), and allow-admin: false still refuses an elevated request rather than downgrading it. See Execution identity.

Landing pages

  • Opening an API host in a browser now lands on the documentation instead of a 404. https://mcp.nobgp.com/mcp — an MCP endpoint, not a web page — sends a browser to the MCP section, as does the bare mcp.nobgp.com; other API hosts send it to the docs home. Only requests that ask for HTML are redirected, so MCP clients and API callers are untouched.

Agent 0.4.32 — July 2026

Command-line privileges

  • nobgp service logs now requires root, like the rest of nobgp service. Run unprivileged, it asked the systemd journal for logs as the calling user — which returns only that user's own messages, so the agent's lines were missing with nothing to say they were missing rather than absent. On Linux and macOS the command re-executes itself under sudo; on Windows run it from an Administrator terminal. The full rule, and which commands need root, is now written down in Root privileges.
  • Windows privilege refusals now say what to do. Commands that need Administrator rights on Windows do not self-elevate — a console cannot raise its own privileges, and a UAC re-launch would move the output and the exit code to another window. The refusal now names the exact command to repeat from an Administrator terminal and points at the local MCP server's status tool for read-only state without privileges. Windows installs are unchanged: the installer still requests UAC elevation for you.
  • nobgp list no longer reports a configured node as having no profiles. When the profile directory cannot be read, the command now fails naming the directory and the reason instead of printing No profiles found.

Agent 0.4.31 — July 2026

Installation

  • Fixed: brew install nobgp/tap/nobgp failed on macOS. The formula pointed at a download path that is no longer published, so every Homebrew install failed to fetch the binary. Re-run the install command — nothing needs cleaning up first. Installs through the install script were unaffected.
  • The floating latest download path is retired. Every installer and nobgp upgrade already resolves the version to install from the router's release channel, so nothing changes for a current agent. Agents older than 0.3.80 used the retired path for manual upgrades and will now fail with a download error — reinstall those hosts with the install script.

Diagnosing interference from other software

  • Certificate errors now name what caused them. When antivirus HTTPS scanning or a corporate proxy re-signs a connection, the install scripts and the agent report the certificate's owner — products identify themselves there — and point at the new Security software and VPNs guide, instead of failing with a bare validation error.
  • nobgp status reports interference it can see from the box. router.tls_intercepted names anything re-signing the control channel, and a new environment block lists other products' active VPN adapters (tunnels) and third-party antivirus/firewall software (security_products). Both are deviation-only: on a clean machine nothing is added to the output.

Fixes

  • Windows: the network drive no longer disconnects and remaps every 30 seconds. Drive mappings belong to the logon session that created them, so the agent's health check could not see its own mapping once it moved into the desktop session and tore down a perfectly healthy mount, over and over. The check now runs in the session that owns the mapping, and fs.mounted in nobgp status reports the real state.
  • Linux: applications with strict DNS resolvers no longer fail on public names. On hosts where the agent has to sit as the first nameserver in /etc/resolv.conf, non-overlay lookups are now forwarded to the machine's own nameservers instead of being refused. Resolvers that treat a refusal as fatal (Node.js, Bun) could not resolve anything on those hosts. See DNS & hostname resolution.
  • Fewer false alarms in the new environment diagnostics. A DSL or mobile-broadband uplink and the Windows Teredo/ISATAP pseudo-adapters are no longer reported as third-party tunnels, and nobgp status no longer waits on the operating system's security-product query, which could stall the whole command on an unhealthy Windows host.

Router 0.4.31 — July 2026

Reliability

  • A single message the router can't act on no longer drops a node's control channel. A malformed request, or a message type a newer agent sends that an older router doesn't handle yet, is now logged and skipped instead of ending the connection. Only a channel that genuinely can't do its job — a read failure, or a write the router can't complete — disconnects a node.
  • Router-side disconnects are no longer shaped like credential rejections. When the router closes a connection for an operational reason, the agent receives it as a normal close that still carries the reason into the agent's local log, so a routine disconnect can't be misread as a deleted node or a revoked key and stop the agent from reconnecting. Genuine rejections — revoked credentials, an unsupported agent version, and the duplicate-instance flap guard — are unchanged and still reported as rejections.

Agent 0.4.28 – 0.4.30 · Router 0.4.28 – 0.4.30 — July 2026

Node access control

  • Fixed: a whole-filesystem allow-roots entry matched nothing on Windows.
  • Node grants no longer carry an expiry — a grant stands until it is revoked.

Installation

  • Fixed: the Windows installer failed before downloading anything. Any install that didn't pin NOBGP_VERSION stopped while looking up the version to install, after printing the banner and the detected architecture. Re-run the one-line installer — no cleanup needed.
  • One published command, every shell. The single powershell -NoProfile -Command "irm ... | iex" line is the only Windows installer; it runs as-is in Command Prompt, PowerShell 5.1 and PowerShell 7. The separate install.cmd variant is retired — update any bookmarked command that still points at it.
  • RPM-based installs now prefer dnf when available.

Reliability

  • Fixed: one offline peer could disrupt other nodes' control channels on the same router.

Agent 0.4.19 – 0.4.27 · Router 0.4.24 – 0.4.27 — July 2026

Node access control

Three settings let a node's owner narrow what the node serves remotely, enforced on the box itself. See Node Access Control for the full model.

  • allow-tools — which capability domains the node serves (fs, command). Replaces the earlier event-sources key, which governed only the event bus; the new setting governs the MCP file and command tools as well, so one answer covers every programmatic caller. Existing event-sources values are migrated automatically, and the migration can only preserve or widen access — never narrow it.
  • allow-roots — which filesystem roots the fs tools and fs event source may touch. Replaces event-watch-roots, with the same migration behavior.
  • allow-admin (new) — whether work may run as root / Administrator. When false, execution drops to the account named by --user, and an explicit request for root is refused rather than silently downgraded. On Windows, where the service runs as LocalSystem with no unprivileged account configured, allow-admin: false is refused at startup with an error and admin stays enabled — use the router-side node grant to restrict access instead.

All three default permissive, and all three appear in nobgp show and nobgp status.

Scope, stated plainly: these settings govern the programmatic surface — the MCP tools your assistant calls and the event-bus sources the router dispatches. They do not govern a published terminal service, whose command and user are fixed when you publish it. That exemption is deliberate: it keeps a locked-down node recoverable through its terminal instead of requiring physical access.

Command execution

  • Choosing who commands run as. command and command_subscribe now take the same pair of options: admin (run elevated — root on Unix, the service's Administrator context on Windows) and username (a specific account, Unix only). Setting both is an error.
  • Fleet dispatches no longer run as root by default. command_subscribe previously ran every dispatched command as root with no way to choose otherwise; it now defaults to the node's configured user, matching the single-node command tool. If you rely on dispatched commands having root, add admin: true.
  • nobgp notify works from dispatched commands. Scripts run by the event bus can call nobgp notify directly — the agent's directory is on their PATH, and the local API socket is reachable by the account those commands run as.

Event bus

  • Overflow says which side overflowed. When a node's buffer or the kernel's event queue fills, the terminal overflow event now carries a reason instead of only a status.
  • Subscriptions report the nodes they matched. Every subscribe call returns the resolved node list, not just a count — so you can see what a selector actually matched without creating a throwaway subscription.
  • A zero-match selector says so rather than returning a success-shaped empty result.
  • arch selectors accept either spelling. arm64/aarch64 and amd64/x86_64 now mean the same thing; previously the value shown everywhere else in the product matched nothing here.
  • Command results always carry exit_code, including zero. Previously a clean exit reported no exit code at all, which was indistinguishable from "still running".

Tools and reliability

  • Fixed: array and number arguments were rejected from some clients. Tools taking lists (node_names, watches) or numbers (idle_timeout) could not be called from Claude Code. Reconnect your MCP client to pick up the corrected schemas.
  • Fixed: reading events could lose them. A failed event_tail response discarded the batch it could not deliver; the events are now restored and redelivered on retry.
  • Node references are easier. Tools accept a node name without a network name when the network is unambiguous, and status / resolve now return node IDs — so there is a path from a name you know to the ID the tools take.
  • fs_stat accepts allow_missing and fs_delete accepts force, matching the file tool they project.
  • Clearer errors. Your command output no longer appears in noBGP's server logs; nobgp notify distinguishes a permission problem from a stopped agent; failed writes name the file you asked for rather than a temporary one.
  • nobgp show corrected. The allow-* settings printed twice in two different formats, and a quic-router value left behind by older versions was displayed as though it were an active override — it is ignored by the agent, and the line contradicted the endpoint reported directly beneath it. Both fixed; nobgp status was unaffected.
  • whoami no longer reports an empty email for calls arriving through a node — the field is simply absent there, with node_context identifying the node. Reconnect your MCP client after upgrading the router, or it will reject the new response shape until it refetches.

Documented behavior worth knowing

  • Filesystem events differ by platform. On macOS, event paths come back with symlinks resolved (/tmp/x reports as /private/tmp/x), and a file created then immediately written may report only the create. Linux reports distinct operations on unresolved paths.
  • Commands run through the login shell on Unix (bash, sh or zsh depending on the host), not a fixed sh.
  • File tools and command tools may run as different users on the same node, so a file readable through fs_read is not necessarily readable through command.

Platform fixes

  • macOS: fixed service installation, which could leave a freshly installed agent without a running service.
  • Windows: installer now forwards all NOBGP_* environment variables when elevating; file replacement retries when the target is briefly open.
  • Linux: install.sh works on headless hosts and quotes its environment passthrough correctly.
  • Upgrades: an unpublished version now says so instead of reporting a permissions error, and the download is checked before you are prompted.