Repository navigation
Releases: stanislav-testhub/openwrt-mcp
Release list
openwrt-mcp 1.5.0 -- Reach and resilience
Reach routers the 25.12-only server could not, run it from a plain shell, let a client ask for less than its grants allow,
and survive the things a router does (reboots, restarts, dropped sessions). The one behaviour change to know about: adding
or removing a WireGuard peer is now a normal rollback-armed change (see Changed).
Added
- opkg and 24.10 (ROADMAP 5.1). The package tools use
opkgwhen the router has noapk(pkg_query,pkg_change,
pkg_config_diff,pkg_config_resolve, the doctor's new-defaults and kernel checks).system_statusshows
packages: apk|opkg; firewall: fw4|fw3.- New doctor finding
opkg-new-pending(the apk one staysapk-new-pending); the audit skips the package check on opkg. firewall_showon an fw3 router:rulesetandrendered; other views are refused with a reason.- CI: a
real-targetjob runs the package and uci tools in the openwrt/rootfs 24.10 and 25.12 containers. - Catalogue budget raised from 23,500 to 23,650 bytes (prose 12,450 to 12,650) for the descriptions that now name both managers.
- New doctor finding
openwrt-mcp calland a skill file (ROADMAP 5.3).ssh ROUTER call TOOL '{json}'runs one tool and prints its text:
no MCP client needed.call --listnames the tools of the session,call TOOL --helpprints a tool's schema. It runs
through the same server as an MCP session, so policy, audit, redaction and rollback are the existing path. A refusal goes
to stderr with theallowline, exit status 1.skills/openwrt-mcp/SKILL.mdis generated from the catalogue
(UPDATE_SKILL=1 go test -run TestSkillFileIsCurrent).- Client-chosen tool profile (ROADMAP 5.4).
--read-onlyand--toolset diag,config,pkg,wgnarrow one session: read-only
is the@readonlypreset intersected with the client's grants; a toolset lists only its tools (system_statusis always
there,execis in none). It is enforced when a tool is called, not only intools/list.connect --read-onlyand
connect --toolsetwrite the profile into the client entry (after--, because OpenSSH reads--read-onlyas its own option). - Entry grammar. The words after the forced command (
SSH_ORIGINAL_COMMAND) are parsed by the daemon against an
allow-list:--toolset,--read-onlyandcall, with a length cap and no control characters, never through a shell.
Anything else is refused and audited. Existingauthorized_keyslines keep working. - Deadlines and a workload cap (ROADMAP 5.7). Each tool has a deadline above its own waits; at most 8 calls run at once,
the rest queue for up to 30 s and then get abusyrefusal (audited DENIED, codeTIMEOUT).uci_confirm,uci_rollback
andmfa_unlocknever queue. A cancelled call kills the command it started and is audited ascancelled by the client; a
deadline hit as<tool> hit its <limit>. - Session resilience (ROADMAP 5.6).
- The bridge waits up to 10 s for the daemon's socket (reboot, upgrade, crash) instead of failing on the first refused dial,
and ends with[code: TIMEOUT; next: ... wait a few seconds and retry]when it gives up. - Audit: a
stdio session closed after <duration>: <reason>line per stdio session and adaemon startedline per start. status(text and--jsonlast_disconnect) shows the last disconnect and its reason; a session still open when the
daemon started again is reported as cut by the restart.
- The bridge waits up to 10 s for the daemon's socket (reboot, upgrade, crash) instead of failing on the first refused dial,
- Add-on status (ROADMAP 5.2).
service_listtakesdetail=<service>: a read-only status for installed add-ons
(tailscale, AdGuard Home, podkop). The list ends with the names that apply on the router. Scope<service>.detail. Fixed
fields only, no credentials. Catalogue budget raised from 22,100 to 22,300 bytes (prose 11,500 to 11,550): one parameter
serves every adapter, instead of a tool per add-on. The other six adapters (adblock, adblock-fast, https-dns-proxy, mwan3,
pbr, ddns) are in 1.6. - Cross-config references (ROADMAP 5.10).
uci_gettakesrefs=<name>: finds what defines an interface, zone, device,
radio or mwan3 member/policy of that name and every reference field in network, firewall, dhcp, wireless, sqm and mwan3
that uses it.confignarrows the search (and is optional withrefs). Scoperefs. Catalogue budget raised from 23,650
to 23,900 bytes (prose 12,650 to 12,875);uci_getgets a per-tool budget of 1,650 bytes. - Warnings in a dry run (ROADMAP 5.9).
uci_applydry run listswarnings from this change: the audit's firewall, SSH,
LuCI, UPnP and Wi-Fi checks run on the live and on the staged configuration, and only what the change adds is reported, with
the audit's next steps and wiki pages. Two new findings:ref-removed/ref-unknown(a deleted, renamed or misspelled name
that other sections still use, with a case-mismatch hint) andradio-in-use(the change turns off the Wi-Fi network this
session is connected through). A real apply reads no warnings. - The management path follows the session. The stdio and
callbridges send the client address fromSSH_CLIENT, the daemon
resolves it withip route get, and the interface it leaves by is treated like the LAN, so a session over a WireGuard tunnel
is protected by the probe andforcerules too. A WireGuard interface'saddresses,listen_portandprivate_keyare
management options.
Changed
wg_new_clientwithreveal=truehas its own scope,wireguard.<iface>.reveal(wireguard.revealwithout an
interface), so a policy can allow the tool without ever putting the private key in the conversation. A grant for
wireguard.<iface>alone no longer coversreveal=true;wireguard.*,*and the presets are unchanged.wg_new_clientandwg_remove_clientno longer hot-add or hot-remove withwg set. They save the peer in the network
config through the standard rollback path (snapshot, armed rollback, reload, read-back) and needuci_confirmwithin 90 s;
otherwise the change is reverted (a new client's key file is deleted too).- netifd loads the peer; if it has not after a short wait, the interface is asked to renew (
ubus call network.interface.X renew), and a rollback does the same. - A commit that fails restores the snapshot and reverts the staged edit. One apply can be pending at a time
(ROLLBACK_PENDING); stagednetworkedits are refused (CONFLICT). - Removal refuses the peer the session arrives through unless
force=true. A peer that only the running interface knows is
still removed withwg set(no rollback possible).
- netifd loads the peer; if it has not after a short wait, the interface is asked to renew (
- Busy and deadline refusals carry
TIMEOUT, toolset and read-only refusalsPOLICY_DENIED. wg_remove_clientdeletes the removed peer's uncollected client config (it holds the private key) instead of leaving it
for the 24 h sweep.- A command the daemon refuses (
--toolset bogus) now ends the bridge withcommand refused: <reason>, not
daemon closed the connection, which read like a crash.
Tests
- The write-path table covers both
wg_*tools. Every item was mutation-tested (80 to 90 mutants per item, survivors killed or
explained as equivalent);FuzzParseEntryand the adapter parsers were fuzzed.
Install on the router (it picks the archive for the architecture and checks SHA256SUMS):
wget -O /tmp/install-router.sh https://github.com/stanislav-testhub/openwrt-mcp/releases/latest/download/install-router.sh
sh /tmp/install-router.sh
or from a PC with ./install.sh install --release. Verify where a file was built with
gh attestation verify <file> --repo stanislav-testhub/openwrt-mcp.
On your PC (Windows, macOS), openwrt-mcp connect --client <name> --host <router> makes the
SSH key and sets up your MCP client; unpack the windows_* or darwin_* archive for it.
openwrt-mcp 1.4.0 -- Diagnose
Answer "is it healthy, is it exposed, what is going on" in a few calls, and check that a change landed.
Nothing here adds a way to change the router: the new modes are read-only, and the existing write tools
now read back what they wrote.
Added
- Verify after write (ROADMAP 4.9). A write the router accepts and then does not keep is
NOT_APPLIED, a new error code, instead of a success the caller has no reason to doubt. Each
write path reads back what it changed:uci_apply: after the commit and before any service is reloaded, each config is re-read
withuci showand every change is compared with what the batch must have left. The last
write to a key wins; asetto the empty string means unset;add_list,del_listand
set_listare checked against the list (order included);createagainst the section type;
deleteagainst absence. A difference returnsNOT_APPLIEDwith the keys, reloads nothing and
leaves the rollback armed; the message namesuci_rollbackand says not to confirm. A
success saysVerified: re-read dhcp after commit, 3 of 3 change(s) match.- Sections addressed by position (
@rule[3]) or by a generated name (cfg0a1b2c) are not
compared, because an earlier add or delete in the same batch moves them. The result counts
them. The value of a secret option (key,private_key,psk,password, ...) is never
printed in a mismatch. wg_new_client: after the commit the peer is looked up by its public key, with its name
and address, before anything touches the running interface. Afterwg setit must be on the
interface. A failed hot-add stays the note it was (the interface may simply be down); a
wg setthat exits 0 and leaves no peer isNOT_APPLIED, and the client's config file is
kept foropenwrt-mcp wg-show.wg_remove_client: the peer must be gone fromwg showbefore the config is deleted
(otherwise a live peer would be listed nowhere), and the section must be gone after the commit.pkg_change: after anaddordelwithcommit=true,/etc/apk/worldmust list the
package, or no longer list it. Version constraints and repository tags in the file are
ignored;upgradeis not checked because it changes versions, not what was asked for.- A read-back that cannot run is not a failed write: the result says
Not verified:and why. TestEveryWritePathVerifiesAfterWritelists every tool that is not read-only. Each one runs
against a backend that accepts the write and keeps none of it and must answer
NOT_APPLIED, or is exempt with a written reason (uci_confirm,uci_rollback,
pkg_config_resolve,sysupgrade,exec,ubus_call,mfa_unlock). A tool added later
fails the test until it is in one list.
net_diagwifi_survey, traffic and usage; airtime innetwork_clients(ROADMAP 4.4, 4.5).
All read-only, so the existingnet_diaggrants cover them without widening.wifi_survey[.<device>]: per radio, busy, rx and tx airtime and noise for the channels the
radio has stayed on for at least 10 s, fromiwinfo survey. The driver only keeps real figures
for the channel it is on (every other channel holds the milliseconds of a boot-time scan), so
the result says the rest are not measured and does not recommend a channel: that needs an
active scan, which moves the radio off its channel and is not read-only. The noise byte is a
signed dBm value read as unsigned (170 is -86 dBm). One survey per radio, however many SSIDs
share it.traffic[.<interface>]:/proc/net/devsampled twice (countseconds, default 3, max 10),
rates per interface (loopback and idle interfaces left out, a counter that went backwards
counts as no traffic), and the top five sources in the conntrack table. Sources are ranked by
bytes when the kernel counts them (nf_conntrack_acct), otherwise by connections, and it
says which. The source is the original direction's, so through NAT it is the LAN client.usage[.<YYYY-MM-DD>]: nlbwmon's totals per device (summed over its addresses), the 25
largest, with the accounting periods it has. nlbwmon keeps a period per month by default, so
this is "this month", not "today" or "this week". Without nlbwmon the error names the package
andpkg_changeas the way to install it.network_clients: anAIRcolumn, each Wi-Fi client's share of its radio's airtime
(receive plus transmit, fromhostapd). A dash when hostapd does not report it. Retries are
not here: neitheriwinfonorhostapdexposes them.- Not in this release: the active neighbour scan and the channel recommendation that would
need it, and Wake-on-LAN. Both change what the radio or the LAN does, so asnet_diagactions
they would silently reach every storednet_diag '*'grant. net_diagand its untrusted-text label now also cover host names (leases).- Tool catalogue: 23,254 bytes;
net_diagmay be 1,700 bytes (was 1,500); the budgets move to
23,300 and 12,300.
system_status mode=doctorandmode=audit(ROADMAP 4.2, 4.3). Two ranked lists of
findings, severity first (high,medium,low,info), advisory only: nothing is changed.
Each finding has a stable id, a message, the evidence, a next read-only call and an OpenWrt
wiki link (every link was opened and read for content before it went in; the wiki answers 200
for pages that do not exist). Each list ends with what was checked, what could not be
(not checked: radios (ubus ...)), and where a check sees only part of the picture.- Doctor:
radio-down,iface-no-address,iface-error,service-crashed,
conntrack-high(80%, high at 95%),overlay-fullandtmp-full(10% free, high at 5%),
apk-new-pending,ntp-unsynced,clock-unset,reboot-needed.- A service counts as crashed only when procd has it with instances, none running and a
non-zero exit code.rc listalone cannot tell a stopped daemon from a one-shot init
script that ran and exited. On the router this was developed against,rc listshows 24
of 45 enabled scripts as stopped: mostly one-shots, and one (AdGuard Home) that procd
shows running. A daemon that is enabled but was never started has no procd instance and
is not reported; the result says so on itslimits:line. - NTP is asked of chrony (
chronyc -c tracking) when it is installed. Otherwise only
"was the clock ever set" is checked, and the limits line says so.
- A service counts as crashed only when procd has it with instances, none running and a
- Audit:
wan-zone-input-accept,wan-zone-forward-accept,wan-port-open,
wan-forward-open,wan-redirect,ssh-wan,luci-wan,ssh-password-auth,
luci-all-addresses,upnp-on,wps-on,ssid-open,ssid-wep,ssid-weak-cipher,
ssid-wpa1,root-no-password,apk-audit-modified,wg-stale-peer.- The internet-facing zones are the ones named
wan*and the ones holding an interface
with a default route. The stock DHCP, ICMP and IGMP rules are not reported. An
explicit SSH or web-interface port is reported asssh-wanorluci-wanonly when the
service listens on every address. - The root password is checked by reading whether its field in
/etc/shadowis empty.
The hash is never stored or shown, and an error never carries file contents. apk auditchanges underetc/,tmp/,var/,overlay/,root/andmnt/are
configuration, not tampering, and are ignored; a changed or removed file elsewhere is
reported.
- The internet-facing zones are the ones named
- Not in this release: the upstream-DNS check (it needs an active probe, which would
makesystem_statusleave the router) and a "reboot needed after a kmod update" beyond the
kernel version. - Scopes.
doctorandauditare scopes ofsystem_status. A*grant, which is what
both presets give, covers them; a grant for one of them does not cover the other, and the
denial prints the exactallowline. Plainsystem_statusis unchanged. - Untrusted text.
system_statusnow carries the untrusted-text label and says so in its
description, because audit findings quote SSIDs. They are quoted, cut to 32 bytes and kept
to one line. - Tests: a healthy router that raises nothing, one fixture per finding that triggers it and
one per boundary that must not (one below, at and one above each threshold), the
not-checked path, ranking, a test that every next step names a real tool and every link is
in the verified list, the read-only command oracle over everything the two modes run, and
27 mutation rows. - Tool catalogue: 22,967 bytes; the budget moves to 23,000 and the prose budget to 12,050.
- Doctor:
logread mode=summaryand baselines (ROADMAP 4.1). After a change the useful question is
"what is new in the log", not "show me 500 lines".mode=summarycollapses the filtered log to its distinct messages, grouped by process
(worst severity first, then busiest). Each row has the severity, a count, the first and last
time and the message with MACs, IPv4/IPv6 addresses, clock times, hex ids, durations and
numbers replaced by<mac>,<ip>,<time>,<hex>,<dur>and<n>. A duration is
digits and a unit that stand alone (90s,12h,8d7h34m): an uptime in a periodic message
would otherwise make every occurrence a new message in a baseline diff, which the first
check on a router showed. A number that is part of a name
(phy0-ap1,eth1) is kept, so two radios are never merged.lineslimits the messages
andoffsetpages through them.baseline=savereturns an 8-hex-digit token for the messages in the log now.
baseline=<token>returns only the messages that were not there (in either mode, with the
same filters), headedsince baseline <token>: N of M distinct messages are new.- A bas...
openwrt-mcp 1.3.0 -- Easy to adopt
Install from a release, set a client up with one command, a smaller and portable tool catalogue, and
no credentials in the conversation. Nothing here adds a way to change the router.
Changed
- Tool titles and hints (ROADMAP 3.4).
- Every tool now has a display
title, set both at the top level and inannotations.title
for older clients. - Every tool states
openWorldHintexplicitly; the spec default istrue, so every tool
used to claim it. It istruefor the seven tools that can reach past the router:net_diag;uci_apply, whose probes ping and resolve;pkg_query, whoserefreshrunsapk update;pkg_change;sysupgrade, whosecheckrunsowut;ubus_call;exec.
wg_remove_clientis now marked idempotent: removing the same peer again changes nothing.- A contract test pins the title and the four hints of every tool.
- Every tool now has a display
- Expired grants no longer pile up.
allowreplaces the client's expired grant with the same tools and scopes instead of
adding another block, so a dailyallow claude-code @operator 2hkeeps four blocks, not
four more each day.statusfolds expired grants into one count line;status --alllists them.--jsonis
unchanged and still carries every grant with itsexpiredflag.- Expired grants were already ignored when authorising, so this is cleanup, not security.
- MCP Go SDK 1.6.1 to 1.8.0 (ROADMAP 3.10).
- It still negotiates
2026-07-28at most; nothing here narrows the protocol versions. tools/listdiffers from 1.6.1 only in annotations:readOnlyHintandidempotentHintare
now sent whenfalseinstead of being left out. Their meaning is unchanged, because
falseis the spec default. Input schemas and descriptions are byte for byte the same.- We set none of the
MCPGODEBUGflags the SDK removed in 1.8.0. The HTTP Origin check is
ours (server.go), not the SDK's, and its test passes unchanged. - The per-frame cap on inbound messages is 16 MiB by default; requests are a few hundred
bytes, so it never applies.
- It still negotiates
- Portable input schemas (ROADMAP 3.3).
- Five optional or nested slices were advertised as
"type": ["null","array"]:
uci_apply.changes, itsvalues,uci_apply.probe,pkg_change.packagesandexec.argv.
They are now a plain"type": "array". Type arrays break Gemini's OpenAPI subset and
older VS Code. - A call that sends
nullfor one of them (OpenAI-style clients do, for "not set") is still
accepted and treated as omitted. Members the schema declares are dropped when null before
validation; free-form objects such asubus_call.argskeep their nulls. TestToolSchemasArePortablefails on a type array,$ref/$defs/$dynamicRef, a
top-levelanyOf/oneOf/allOf, a bare{"type":"object"}, or a server plus tool name
over 60 characters.ubus_call.argsanduci_apply.expected_revisionsare free-form maps
on purpose and pass because they stateadditionalProperties.
- Five optional or nested slices were advertised as
- Smaller tool descriptions (ROADMAP 3.5).
tools/listwent from 23,731 to 21,982 bytes (23 tools; 22,086 oncewg_new_client
gainedrevealbelow). The text for operators is gone
from the descriptions: the "Policy scope: ..." sentences,exec's warning about broad
grants, and theuci_applyparagraphs that repeated its own parameter descriptions
(ops,probe,restore,expected_revisions). A refusal already prints the exact
scope to grant, and the README Tools table lists the scope syntax.uci_getno longer says its output "includes secrets such as Wi-Fi keys": they are masked
by default, so the sentence told a model the wrong thing.TestCatalogueStaysWithinBudgetfails above 22,100 bytes in total, above 11,500 bytes of
descriptions, or above 1.5 KB for one tool (4 KB foruci_apply).- The limit is not the 18 KB ROADMAP 3.5 first named. That figure was set before 3.4's
titles and hints and the SDK's explicitfalsehints, which together add about 2 KB that
no description edit can remove; reaching it would have cut about 38% of all prose. TestDocsOnlyNameToolsThatExistfails when a description, the server instructions or the
README name a tool-like identifier that is not a tool.
- Credentials stay off the transcript (ROADMAP 3.6). Behaviour change.
wg_new_clientno longer returns the client's private key, preshared key or QR code. It
writes the config to a file the owner alone can read, in RAM beside the daemon socket
(/var/run/openwrt-mcp/wg/<name>.conf), and returns the public key and the command
openwrt-mcp wg-show '<name>'. The roadmap named/tmp, which any local user can write
into; the runtime directory is0700, and the file is created withO_EXCL, so it never
replaces a file or follows a planted symlink.reveal=truerestores the old result, key and QR included, and writes no file. Use it
only when the key may enter the conversation and the provider's logs.- The file is created before the peer is committed, and removed again if anything after
that fails; a name that already has a waiting file is refused. Files nobody collected are
swept after 24 hours (at the nextwg_new_client) and vanish at reboot. openwrt-mcp wg-show <name> [--keep], run on the router, prints the config and the QR
code in the operator's own terminal and deletes the file.sysupgrade action=backupcreates the archive0600beforesysupgradewrites into it
(it used to take the default umask), refuses to replace an existing file, treats an empty
archive as a failure, and removes the archives this tool made earlier. Only files named
exactly like its own (backup-<host>-<date>-<time>.tar.gz) are removed, and only
regular files; the result says how many went.mfa_unlockis unchanged: the operator still types the code into the chat. Moving it to
elicitation is ROADMAP 6.1.
- Shell-equivalent
execgrants need a flag (ROADMAP 3.7).openwrt-mcp allow <client> exec <programs> <duration>now refuses a grant that lets the
client run a program that runs other programs, and says so. Add--shell-equivalentto
grant it anyway; the grant is made and a warning is printed.- The list:
sh ash bash dash busybox env nice flock timeout nohup setsid chroot su watch time start-stop-daemon taskset ionice chrt find awk sed xargs tar ssh dbclient lua ucode apk opkg ubus. The roadmap named the first dozen; the rest are BusyBox wrappers and shells of
the same class. A test pins the list, so adding a name is a recorded decision. - Names are judged by base name, so
sh,/bin/shand/usr/bin/../bin/share the same
grant, and by glob:*,s*,?shand/bin/*can reach a listed program and count. - A
ubus_callgrant that can reachfile.exec(file.*,*, ...) counts too. policiesprintsshell-equivalent: ...under such a grant,statusmarks it, and
status --jsoncarriesshell_equivalent(additive; empty grants omit it). That also
covers a policy file written by hand, which theallowgate never sees.- Existing grants are not changed or revoked.
Fixed
- The stdio bridge outlived the daemon (ROADMAP 5.6). After a daemon restart, each
openwrt-mcp stdioprocess stayed alive until its client's next request, blocked reading
stdin. It now exits as soon as the daemon closes the socket, with "daemon closed the
connection" on stderr.
Added
openwrt-mcp prune [--older-than <duration>]deletes expired grants, writes one
pruneline to the audit log when it removes any, and refuses a negative age (that would
reach live grants). Comments, the server section and other blocks are kept byte for byte.- Release workflow (ROADMAP 3.1).
- A pushed
vX.Y.Ztag builds all ten router architectures. Each archive holds the binary
and thefiles/payload. - The workflow writes
SHA256SUMS, attests the build (gh attestation verify), and
publishes a GitHub Release with the CHANGELOG section as notes. - It refuses a tag that disagrees with
main.go. - CI now also cross-builds
mips64andmips64le, whichinstall.shalready supported.
- A pushed
- Install without Go (ROADMAP 3.1).
install-router.shruns on the router and is published with every release. It downloads
the archive for the router's architecture, checks it againstSHA256SUMS, refuses on a
mismatch, checks the overlay has room, then installs.install.shnow hands its own payload to the same script, so there is one install
procedure.install.sh install --release [vX.Y.Z]has the router fetch a release instead of
building one. It is also the fallback when no Go toolchain is found.install-router_test.shexercises the download and verification against a local mirror
in CI. It covers: latest, a pinned version, a malformed version, a missing architecture,
a missingSHA256SUMS, and an archive swapped for another architecture.- CI checks that
install.sh,install-router.shand the release workflow list the same
architectures.
openwrt-mcp connectandconnect doctor(ROADMAP 3.2). They run on the operator's PC.connect --client claude-code|claude-desktop|codex|cursor|gemini|vscode --host <router>:- makes an ed25519 key with the PC's
ssh-keygenif there is none (never overwrites); - prints the two commands to run on the router (
authorize-key,allow ... @readonly 30d),
built from<type> <base64>of the public key only, because the key's comment is free text
that would otherwise end up inside a pasted command; - shows the client entry, and with
--writeadds it: Claude Code and Codex through their own
mcp add, Claude Desktop, Cursor, Gemini CLI and VS Code by merging oneopenwrtentry into
thei...
- makes an ed25519 key with the PC's
openwrt-mcp 1.2.0 -- Reliable changes
Closes ROADMAP milestone 2 (items 2.1 to 2.6). No tool is added: every item is a parameter or a
mode of an existing tool.
Added
- Validation before reload (2.1).
uci_applyruns each config's own checker before staging
(the baseline) and after (the candidate) and reportsvalidationin the dry run. For the
firewall that isfw4 check. Measured on the router: it sees changes staged in/tmp/.uci, but
it exits 0 even for an invalid value and only prints[!]lines for what it will ignore, so the
verdict is those lines, compared as counted lines with section indexes normalised (a warning
that merely moved is not new). A real apply is refused when the candidate has a problem the
baseline did not, unlessforce=true; a config that was already broken can still be fixed.
Configs without a checker (dnsmasq has none that sees staged changes:dnsmasq --testreads a
file generated at service start) are reported "not checked". - Revision locking (2.2).
uci_getends with# revision of <config>: <12 hex>, the digest of
the committed file, even for a narrowed read.uci_applytakesexpected_revisions
({config: revision}) and refuses withCONFLICTif a config changed since; a revision for a
config the call does not change, or one that is not 12 hex digits, is an error rather than
ignored. Dry runs print the revisions to pass; applies print the new ones. - Probes and management-path detection (2.3).
probe(up to 5 of{kind: ping|resolve, target, server?}) runs after the reload, each retried forprobe_waitseconds (default 15, max 60) and
never longer than half the rollback window. A failed probe is reported and the change stays
armed: the timer undoes it, as for a caller that lost its connection. The staging lock is
released before probes run. A change to the LAN interface or its bridge, the SSH listener, or
the firewall zone and rule that let SSH in is refused unless it names a probe orforce=true,
and its default rollback window is 180 s. The rule set is static: it does not know which
interface the current session arrived on, because the stdio bridge's origin address does not
reach the tool handlers (a session-aware rule is future work). - History and restore (2.4). Confirming a change keeps the config as it was before, per config,
newesthistory_keep(default 5,0off, at most 20), under/etc/openwrt-mcp/history/with
0600files.uci_get history=listandhistory=diff:<id>read them;uci_apply restore=<id>
puts one back through the same snapshot, checks, rollback timer and probes.uci importwrites
the file at once (measured), so a restore replaces the file aspkg_config_resolvedoes, and
runs the checkers on the installed file before anything reloads; its dry run shows the settings
diff only. A rolled-back change leaves no entry.wg_new_clientandwg_remove_client, which
commit without a rollback, record the file just before they commit. - Polling instead of sleeping (2.5).
service_controlreads the state every 500 ms until it
has been the same for three reads, orwaitseconds (default 10, max 60) pass, and says which
happened. It also notes a final state that contradicts the action (a service still running after
stop). - Write-path parity (2.6).
add_listskips an element already in the list: libuci appends a
duplicate (measured).del_listof a missing element already succeeds as a no-op (measured), and
a test pins it.set_listnow refuses an empty element likeadd_listanddel_listdid, and
one table test runs every option-writing op against the same hostile names and values.
Security
- A probe has the router ping or resolve a name for the caller, which a grant on
uci_applynever
allowed, so each probe target is a policy scope of its own,probe.<kind>.<target>. Targets
that could read as an option are refused. A restore replaces a whole config, so its scope is
<config>, which<config>.*does not cover. - History entries hold whole config files, secrets included. They get the snapshots' protection
(0700directory,0600files, state-path guard), are masked when shown, and are in
sysupgradebackups becausekeep.dkeeps the whole state directory.
Changed
uci_apply'schangesis optional in the schema (a restore has none).wg_new_clientandwg_remove_clienttake the calling client's name, for the history entry.
Not done
- Out of scope: the
wg_*tools still commit with no snapshot or rollback. History in RAM for
low-flash boards (ROADMAP open question) is not built.
Install on the router (it picks the archive for the architecture and checks SHA256SUMS):
wget -O /tmp/install-router.sh https://github.com/stanislav-testhub/openwrt-mcp/releases/latest/download/install-router.sh
sh /tmp/install-router.sh
or from a PC with ./install.sh install --release. Verify where a file was built with
gh attestation verify <file> --repo stanislav-testhub/openwrt-mcp.
On your PC (Windows, macOS), openwrt-mcp connect --client <name> --host <router> makes the
SSH key and sets up your MCP client; unpack the windows_* or darwin_* archive for it.