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/andnetworks/<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 withnode/andnetworks/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/reportsnetworks/production/docs/report.pdfrather than doubling the path. - A network whose name contains a space lists correctly too. Names created through
network_createare always DNS-shaped, but one renamed in the dashboard need not be, and such a name went onto the wire unescaped.
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, andnetworks/, holding one folder per network the node belongs to. A file that used to be at/mnt/nobgp/report.pdfis at/mnt/nobgp/networks/production/report.pdfnow. 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 namesnodewould 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 belownode/andnetworks/<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.
Router 0.4.56 — August 2026
Your assistant can read and write noBGP's own file storage, without going through a node.
File tools
fileand thefs_*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 sendingstorage: truealongside 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,mkdiranddeletework on both trees;edit,multi_edit,batch,fetch_urlandfs_grepremain 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: truerequires 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
deletetakes the whole subtree whether or not you asked forrecursive, and the depth-guard that refusesrm -rf /etcon a node does not apply — only the tree's own root is protected; andadminis 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 —PROPFINDto list,GETto download,PUTto upload,MKCOL,MOVE,COPYandDELETEto 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_urlinnetwork_directorynow 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:19840so 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 thedrwx------on/Volumes/nobgp. webdav-proxy-ownersbounds it to a list of numeric uids. Anything else gets403 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 inmountoutput andnet uselistings, 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.
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.mdandreadme.mdare 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-
PUTused 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.txtin one network and/notes.txtin 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
commandfromallow-toolsis confirmed before it is written. That one change takes thecommandtool, dispatched bus commands and published terminal services away together, so unlikeallow-admin: falseit 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: anobgp configissued 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. --yesanswers these prompts onnobgp 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--useron 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 everyadmin: falsecall is refused rather than narrowed — unelevated never means root — so unelevated execution stops and callers must ask foradmin: true. The clear survives restarts but not a reinstall:nobgp service installand registration re-capture the install account, since a fresh enrollment has no other way to learn one. - The
allow-admin: falsewarning 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
SIGKILLnorTerminateProcessdislodges 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
fusermounthelper binary. Only the kernel half was ever declared, and the kernel half being present is exactly what makes the missing one invisible —/dev/fuseexists, the agent correctly takes the FUSE path, and then cannot mount. The.deb,.rpm,.apkand.ipkall name the helper now (fuse3, orfuse-utilson OpenWrt). - OpenWrt gets it from the install script. The
.ipkis hand-extracted rather than installed throughopkg— OpenWrt has manyaarch64sub-architectures andopkgrejects a mismatch, while the static binary runs on all of them — so the package's dependency field is never resolved on that platform.install.shinstallskmod-fuseandfuse-utilsexplicitly instead. OpenWrt was the platform this was actually broken on:kmod-fusewas a recommendation, whichopkg(unlikeapt) does not install by default, andfuse-utilswas 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/nobgpsat 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
.rpmserves every RPM platform the install script detects, and EL7-class hosts (Amazon Linux 2, RHEL/Oracle/CentOS 7) have nofuse3in their default repositories at all — a hard requirement there would fail the whole install over an optional filesystem.dnfhonours recommendations, so modern hosts still get it;yumandrpmignore 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_directoryreportsoffline_atfor every node that is down.online: falsewas 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. Seenetwork_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-nodeservicesarrays. The compactbrief+ 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_pininnobgp statusreports the same three states as before, and a node whose transport iswssis 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
nobgpvolume 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
nobgpdrives 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 asinternal/ HTTP 500 /retryable: true. That told a well-behaved client to keep retrying a condition that is permanent until someone changes the box. It isfailed_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 notpermission_denied, whose documented retry — dropadmin— 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 failedand 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 answerspermission_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 onretryablewas 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 retryableinternal. Windows words a permission refusal its own way —Access is denied.where Unix sayspermission denied— and the router did not recognise it, so an unreadable path on a Windows node came back asinternal/ HTTP 500 /retryable: true. The same unelevatedfs_readansweredpermission_deniedon 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 ispermission_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
retryableto 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
SeBackupPrivilegeand 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
internaluntil 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
usernow runs everything as that account.command, terminal sessions and dispatched bus commands joinfs_*andfile, soadmin: falsemeans 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 withfailed_preconditionrather than running it as LocalSystem; that refusal is gone on such a node. See A second identity on Windows. allow-admin: falsenarrows 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>), whichnobgp uninstallleaves 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 withadmin: trueand fails withadmin: false.
Security
- An administrative configured account no longer reads past its own ACLs. Windows hands such an account a token with every privilege enabled,
SeBackupPrivilegeincluded, 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 whoseuserwas an administrator, an unelevatedfs_readcould 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_directorycarries it asinfo.user_is_adminfrom router 0.4.47 — this is the agent release that answers it. Where it istrue,admin: falseon 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 statusanswers the same question on the box. Theallow:block gainsunelevated_is_admin, present only when true, beside theunelevatedaccount 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_directorysays whether a node's unelevated account is administrative. The newinfo.user_is_adminqualifiesinfo.user: where it istrue, the named account is an administrator on that node — a member of the localAdministratorsgroup on Windows — soadmin: falsethere 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 Unixuser: 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 notfalse. - The
adminguidance 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: falseis 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_nameout ambiguous — you had two networks in the account's eyes, so the answer wasinvalid_argsasking 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,fileand thefs_*tools, to the selector fields on the subscribe and labelling tools, and toprovision_node,register_nodeandnetwork_delete. Name the network to use one. - A network id now binds exactly. Selector-taking tools resolved
network_idby 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'shomesilently rebound to your ownhome. Membership is checked against the id itself now, and the id reaches the network it identifies. Passingnetwork_idandnetwork_nametogether is stillinvalid_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_directorycarries it asinfo.hardware_model—DS918+,Raspberry Pi 5 Model B Rev 1.0,Macmini9,1,NUC11TNHi5. Nothing else in that block could answer it:platformandkernel_archdescribe 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,filewithop=write, andop=fetch_urlbuild the missing parent chain instead of failing on the first call, so none of them needs anfs_mkdirwithrecursive: truein 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_mkdirwas 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'sallow-rootsis refused exactly as before. - The directories belong to whoever will own the file. They are created as the identity the call's
adminflag selected, at both identities, so anadmin: falsewrite 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
useris now the whole of the second identity, on every platform. Thewindows-user-identitykey 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
userbut 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,commandand terminal sessions still refuse an unelevated call rather than running as LocalSystem, andadmin: trueis 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_directoryreports the account on any node that resolved one, Windows included — where it is whatfs_*andfilecalls 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: falseworks 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_USERis 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 auserit never chose. See Environment Variables.nobgp config --usersays whenNOBGP_USERoutranks 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_nodenow 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 anode_namesselector (a selector resolves against nodes that already exist, so it cannot name one that does not), then tail until the returned name arrives asregisteredand thenonline. Polling the directory still works and is the only option on an isolated network, which has no event bus.- It returns
network_nameandnode_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_summaryreports 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_nodeis 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_directoryno 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 emptynodesarray, and you paid context for each. Two exceptions, both deliberate: a network you named withnetwork_nameis returned even when empty, because its presence is the answer that it exists and holds no matching node; andbriefalone 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 returnsinvalid_argsnaming the cause. It was the one shape in these filters that failed quietly.
Services
service_publishsays 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 withcommand, 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/listpredates a parameter sends it untyped, and Claude-family clients serialize untyped values as strings — soadmin: "true"was rejected withtype: 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: truealongside auserin the node's config andfs_*/filecalls sent withadmin: falserun 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 configureduseris 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 answersfailed_preconditionnaming its two ways out —admin: trueto 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 ransudo. Withwindows-user-identityunset 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_directoryreports 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 statusnow reports which identity an unelevated call gets. Theallow:block carries a fourth line:unelevated, the account anadmin: falseoperation actually runs as here — orunelevated_refusalin its place, naming why nothing can.allow.adminonly 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
userkey. The two differ exactly when it matters — a configured account that has since been deleted, or auser: root, reads fine in the config file and is refused at the call.nobgp showprints what was configured; this prints what the node will do with it. - The local
statustool reports the same block, which is the point of the change:network_directoryis 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 stillnetwork_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 (
commandpresent means the node holdsmanage; absent meansobserve, where everyadmin: truereturnsforbidden), and a peer'suserandallow_admincannot be looked up from that endpoint. See Which identity a call gets, from here.
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
whoaminow reportscan_elevate— whether you may sendadmin: trueat all.falsemeans every elevated call comes backforbidden, so there is no point sending one;truemeans the router's gate passes, though a node whose owner setallow-admin: falsestill refuses. It is derived from the same check the real call runs, so it cannot drift from the gate it predicts. Together withnetwork_directory's per-nodeuserandallow_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, andcan_elevatescoped 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_directorynow returnsorg_idon each network. It is the join key fororgs[]— 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
adminis 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 tryadmin: falsefirst 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.
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: falsewhen the identity it would run as resolves to uid 0. Three shapes reach it: nouserconfigured 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), auserthat no longer resolves, anduser: rootspelled 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 isfailed_precondition, not retryable, and names both remedies:sudo nobgp config --user <account>on the box, or retry the call withadmin: 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: falseas 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: falseoutright. A Windows node offers one identity, LocalSystem, and every call has to ask for it withadmin: true. Where an unelevated call previously ran as LocalSystem anyway, it is refused. nobgp config --user rootis 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--userunset and have callers passadmin: true.--allow-admin=falseasks the resolver, not the name. The confirmation prompt and the standing warning now report which case the node is actually in, souser: rootand a deleteduserread 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'sinfo.useris empty on a node whoseuserwas 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, sinceinfo.useris the one signal a caller has before choosingadmin.user: rootis 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: falsewhen 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, oruser: rootspelled 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 withadmin: trueif the superuser really was intended. See Unelevated never means root. - An empty
useris 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 servingadmin: falseas itself. Elevating there would reach the same uid, so there is nothing to prevent. Tryadmin: falsefirst and treat a refusal as the signal to elevate. - The refusal is
failed_precondition, notpermission_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 asinternal/ 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_directorynow tells "no account configured" apart from "the agent is too old to say".info.userhas 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. Seenetwork_directory.- No derived "can this node run unelevated" flag, deliberately. It would be wrong in both directions —
user: rootis 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: trueandadmin: falseidentically — 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
recursivefs_listover an enormous tree and anfs_globreturning hundreds of thousands of paths are the calls that can reach the new ceiling, and they now come back astoo_largenaming what to narrow, where before they were attempted.limit/offsetdo not avoid it — those page a listing the node has already built in full — so narrowpath, or droprecursive. 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.
Search
- A search tells you when it did not read everything — this is the agent half of the
skipped_fileswork 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 reports0for a node that cannot answer. See A zero is only a zero whenskipped_filesis 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_grepover a directory holding a rotated log or a database dump returnedcount: 0,scanned_files: 0and 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 carriesskipped_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_subscribenames 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'sbuild_versionbefore 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 object —
service_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 runningcommandand 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_requiredis the one edit that changes the audience from your organization to anyone with the URL. It is gated onservice_publish(auth_required: false) and onservice_update, and below Admin the call returnsforbiddenwith 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 passingauthorized_emailsforces authentication back on, so it is never gated either. node_labelnow needs an Owner or Admin, the same tier asnode_grant. Labels are the selector a grant'stargetsare 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 getsforbidden; 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: truehas followed since 0.4.33. See Roles & Permissions for the full matrix, cell by cell.
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 /tmpon macOS described/private/tmp/…,fs_list /binon a usr-merged Linux said/usr/bin/…, andfs_statreported 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 — acrossfs_list(recursive and non-recursive, which was renamed without ever having had the descent problem),fs_glob,fs_grepandfs_stat, and the equivalentfileops. - A walk begins where the permission check approved.
allow-rootsis 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. Anfs_greporfs_grep_subscribepattern that is not valid RE2 — an unbalanced bracket is the usual way — came back asinternalwithretryable: true, which told a client to retry a typo forever. It isinvalid_args(HTTP 400) now, and it is recognised ahead of every other phrase, because no other agent failure sayserror 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 namedallow-roots.yamlansweredpermission_denied(403), a pattern containingdoes not exist(answerednot_found,too large(answeredtoo_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 answerinvalid_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 afilebatch. It lasted until the agent was restarted, and it was reachable by an ordinary largefs_grepor recursivefs_listwith 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: trueand 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 istoo_large, notinternal. - macOS: walking
/tmp,/etcor/varfinds the files. All three are symlinks into/private, and a walk never follows a symlink — including the one it is handed — sofs_list /tmpwithrecursivereturned{"count": 0, "entries": []}andfs_grep /etcreportedscanned_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_deleteandfs_writestill mean the final component exactly as you wrote it — afs_statof 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 reportexit_code: -1withstate: "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.143isSIGTERM,137isSIGKILL,130isSIGINT, 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 reports1. - Windows: quotes reach
cmd.exeintact. The command line was being escaped for a program that parses its own line, which is the wrong rule forcmd.exe:echo test "quoted string"came back with literal\"around the quoted part anddir /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 thecommandtool'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#< CLIXMLand 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 outsideallow-roots, a domain dropped fromallow-tools, or the agent's own configuration directory — came back asinternalwithretryable: true, so a well-behaved client retried a permanent decision forever. All four arepermission_denied(HTTP 403, not retryable) now. Two more joined them: a reply too large for the node to send in one answer istoo_large(413) rather than an unexplained 500, and a node that is not running privileged enough to become the account it was asked to isfailed_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+retryableto 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 stillnot_found, an unreadable onepermission_denied, a non-matchingold_stringstillinvalid_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'smanagegrant) and the node'sallow-admin. Earlier agents confined both identities, which is why elevating to escapeallow-rootsdid 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
adminflag, not the resolved uid. That matters on Windows, where both identities are LocalSystem: keyed on the uid, every Windows file call would have shedallow-rootsand the owner's confinement would have evaporated on the whole platform.
File search and containment
fs_grepworks. On earlier agents it did not — at all. Every call was refused before the search began, because the agent required an absolutepathand a grep carries its roots inpathsinstead, 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 releasefs_grepandfs_grep_subscribefail withpath must be absolute: "". Upgrade the agent to use either. The same guard never appliedallow-rootsto a search, since it read the empty field rather thanpaths; every entry inpathsis 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_listwithrecursive: true, andfs_glob, listeddefault.key,default.jwtanddefault.ymlwith their sizes and timestamps — one call afterfs_readrefused those same paths by name.fs_grepwas 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_globwithfollow_symlinksno longer reports targets outsideallow-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 answerfs_readalready gives for those paths, so glob stops advertising what the node will not serve.- A node with a configured
usernow applies its owner'sallow-rootsto 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 tofs_list,fs_globorfs_grepas they descended. The roots travel with the request now. Nodes with no configureduserwere never affected.
Node access control
allow-rootsstill does not boundcommand. Itsworkdiris not path-checked and is not meant to be: the command string itself is unconstrained, so a confinedworkdirwould refuse whatcdreaches anyway. Bounding what a node will execute isallow-toolswithoutcommand, orallow-admin: false.
Router 0.4.36 — July 2026
Execution identity
- Asking to run as the superuser now requires the authority to ask.
admin: trueneeds an org Owner or Admin — aMemberis refused withforbiddenrather than quietly downgraded. It applies everywhere the flag does:command,command_subscribe,file, thefs_*tools, andservice_publish/service_update. Previously the only check was the node's ownallow-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
managemay elevate on its peers; one holdingobservemay 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_subscribechecks 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
forbiddenon an elevated call is not worth retrying. Retry withadmindropped — that runs as the node's configured account and is what most work wants. Every affected tool description now says so. adminis available on the per-opfs_*tools.fs_read,fs_write,fs_list,fs_stat,fs_delete,fs_mkdir,fs_editandfs_grepaccept the flag; before this onlyfiledid, and an elevated file operation meant reaching for the multiplexed tool.fs_grep_subscribedeliberately 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 storedadmin: truehands 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
File search
fs_grepis 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
progressTokenand the response carries every hit inmatches[]. Send one — which is how a client declares it consumes notifications — and each match arrives as anotifications/progressmessage as the walk finds it, with the response settingstreamed_as_progress: trueand omittingmatches[]rather than sending everything twice. Either waycount,scanned_files,truncatedandduration_msare populated, so readcount, not the length ofmatches, to know whether a search found anything. Seefs_grep. fs_grepworks from a node's own local server too, at theobservetier, where it is always synchronous: that endpoint has no channel for a mid-call push, so it ignores aprogressTokenand 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 asubscription_id; matches arrive as events attributed bysource_idto the node each one is on, read withevent_tailand ended withevent_unsubscribe. Like a command dispatch it always ends: every node reports once, andoutstanding: 0inevent_tail's progress block means the search is complete. Available at theobservetier on a node's own server, and the only way to read search results incrementally there. See Searching a fleet. event_subscriptionsnow reportsgrepas a fourth subscription kind, alongsidefs,commandandpresence.
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: valueline under a short block explaining that uncommenting one pins it. Older agents froze every default into the file the first timenobgp configran, 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-sources→allow-tools,event-watch-roots→allow-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 installregistration had frozen into a client config, and re-picking an overlay slice.
File search
fs_grepworks again on nodes with a configureduser. Agent 0.4.34 moved file work onto a privilege-dropped worker and refused the incremental (streaming) form offs_grepthere, which is most nodes — registration setsuserto 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
adminflag oncommand,command_subscribe,fileand 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 — soadmin: trueagainst 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_readcould be denied throughcommandon 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 foregroundnobgp agentstarted by an ordinary user — a file operation that would run asuseris now refused, naming the reason, instead of running as the agent's own account. Either run the agent as root or clearuser. 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/nobgpon macOS,C:\ProgramData\nobgpon Windows) is refused to thefstools and fs watches, with symlinks resolved first. Noallow-rootsvalue opens it and neither doesadmin: 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 onefs_readaway, and a caller holdingfscould edit away the veto meant to confine it.commandis unchanged: a session running as an account that can read those files still can. See Node Access Control. allow-admin: falsenow 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 configureduseris itself a superuser account. Both are refused now. If you have such a node carryingallow-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 withsudo nobgp config --user=<account>first.- Windows:
allow-admin: falsenow 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 configwarning and theallow.warningfield innobgp statusused 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.
usernameis gone fromcommand,command_subscribe,service_publishandservice_update, replaced byadmin: a node offers exactly two identities and this picks between them. Omitted orfalseruns as the account the node's owner configured;trueruns as the node's ambient identity, which a node set toallow-admin: falserefuses rather than downgrades. Calls that still passusernameare rejected withinvalid_args— replaceusername: "root"withadmin: 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:
rootcould 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, usesuorrunasinside the command itself. - File operations now run as that identity too, and
filetakesadmin. The agent drops privilege before opening the file, so ownership and permissions are the ones that account would get — a file written withadmin: falseis 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 throughfs_readcould be denied throughcommandon the same node. The per-opfs_*tools carry noadminand always act as the configured user; usefilewhen 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 carriesadmin: trueand 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, setadmin: truewithservice_update.
Discovery
network_directoryreports whatadminwill mean on each node. Each node'sinfoblock now carriesuser— the account unelevated work runs as, empty when none is configured, which is the case worth noticing becauseadmin: falsethen resolves to root — andallow_admin,falsewhen 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 orfalse. See network_directory.
Agent 0.4.33 — July 2026
Node access control
- A node's capability settings now govern published terminal services too.
allow-toolsandallow-adminpreviously 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 setallow-admin: false. One rule applies to every execution surface now: a node that no longer serves thecommanddomain 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: falseis 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 carryingallow-admin: falsein their config will stop serving remote execution on upgrade.nobgp configchecks the two identity settings before it writes them.--usernaming an account that does not exist is refused at the point of setting, instead of failing at every session later; on Windows--useris refused outright, since there is nothing to switch to.--allow-admin=falsesays what it costs — with auserconfigured 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
--userhelp text said the opposite of what the agent does. It described--useras a restriction that only applies underallow-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 ordinarysudoinstall has never run sessions as root.allow-admin: falseadditionally 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
adminoption is gone fromcommandandcommand_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 ausernamethere outright, so it did not need to reach every caller. Calls that still passadminare rejected withinvalid_args— replaceadmin: truewithusername: "root". Everything else is unchanged: the default is still the node's configured user (not root), andallow-admin: falsestill 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 baremcp.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 logsnow requires root, like the rest ofnobgp 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 undersudo; 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
statustool for read-only state without privileges. Windows installs are unchanged: the installer still requests UAC elevation for you. nobgp listno 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 printingNo profiles found.
Agent 0.4.31 — July 2026
Installation
- Fixed:
brew install nobgp/tap/nobgpfailed 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
latestdownload path is retired. Every installer andnobgp upgradealready 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 statusreports interference it can see from the box.router.tls_interceptednames anything re-signing the control channel, and a newenvironmentblock 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.mountedinnobgp statusreports 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 statusno 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-rootsentry 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_VERSIONstopped 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 separateinstall.cmdvariant is retired — update any bookmarked command that still points at it. - RPM-based installs now prefer
dnfwhen 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 earlierevent-sourceskey, 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. Existingevent-sourcesvalues are migrated automatically, and the migration can only preserve or widen access — never narrow it.allow-roots— which filesystem roots thefstools and fs event source may touch. Replacesevent-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: falseis 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.
commandandcommand_subscribenow take the same pair of options:admin(run elevated — root on Unix, the service's Administrator context on Windows) andusername(a specific account, Unix only). Setting both is an error. - Fleet dispatches no longer run as root by default.
command_subscribepreviously ran every dispatched command as root with no way to choose otherwise; it now defaults to the node's configured user, matching the single-nodecommandtool. If you rely on dispatched commands having root, addadmin: true. nobgp notifyworks from dispatched commands. Scripts run by the event bus can callnobgp notifydirectly — 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
overflowevent 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.
archselectors accept either spelling.arm64/aarch64andamd64/x86_64now 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_tailresponse 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/resolvenow return node IDs — so there is a path from a name you know to the ID the tools take. fs_statacceptsallow_missingandfs_deleteacceptsforce, matching thefiletool they project.- Clearer errors. Your command output no longer appears in noBGP's server logs;
nobgp notifydistinguishes a permission problem from a stopped agent; failed writes name the file you asked for rather than a temporary one. nobgp showcorrected. Theallow-*settings printed twice in two different formats, and aquic-routervalue 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 statuswas unaffected.whoamino longer reports an empty email for calls arriving through a node — the field is simply absent there, withnode_contextidentifying 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/xreports 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_readis not necessarily readable throughcommand.
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.shworks 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.