v0.6.0
Added
-
epics-pv://healthanswers the posture questions an approver asks after the write gates.
Four additions, every one a boolean or a count and none of them an address:rest_tls(whether
the REST planes verify certificates, whether a CA bundle is configured, and whether any plane
speakshttpsat all, since verification is on by default and says nothing where there is no
certificate to verify),
rest_read_rate_limit(the opt-in REST GET throttle),allowed_roots_set(whether the opt-in
file boundary holds a root) andchannelfinder_redaction(how many ChannelFinder owner accounts
and property names the redaction discloses, and whether each allowlist came from the site or from
the built-in default). Each is named for what it MEASURES rather than for the question that
brings a reader to it, and three of them needed that.rest_tls.verification_enabledresolves
the precedence instead of mirroringEPICS_MCP_TLS_VERIFY, becauseEPICS_MCP_CA_BUNDLEwins
over that switch: a server with the switch off and a bundle set does verify, and a field
mirroring the switch would have reported it as unverified. The throttle carries itsrest_
prefix because a p4p PV read runs past it, so "reads are limited" without the prefix would be an
all-clear for the reads that load an IOC. And the redaction counters say DISCLOSED, because an
allowlist is the set of what passes through: zero is the most private posture, not a broken one.
allowed_roots_setis decided by the same predicate the boundary itself asks, so a value of;
or of blanks, which resolves to no root at all, reports false rather than claiming a boundary
that no file argument is held to. Deliberately absent, because a client keeps this payload: the
CA-bundle path, the roots, and the allowlist entries.epics-doctorprints the allowlist
entries; the bundle path and the roots are printed by no surface at all and stay in the
environment the server was started with. -
epics-testpv, a seventh command: a test PV without a control system. It serves
TEST:Temperature, an analogue reading with a unit, andTEST:Heater, a writable switch, over
PVAccess until Ctrl-C. The quick start previously began withsoftIocPVA, which ships with EPICS
Base and which no page told you how to obtain, so the promise of a working PV without a facility
was not keepable. This one needs nothing beyond the install, since p4p is already a dependency.
It binds loopback unless--interfacesays otherwise, because a PVA server is a network
service and its switch accepts writes, and it prints the port it actually bound, which is not the
default one when that is already taken. -
epics-init --out PATHwrites the configuration file itself, with--forceto replace an
existing one. A shell redirect cannot promise an encoding, and this block is JSON a client has to
parse: in Windows PowerShell 5.1,>writes UTF-16 with a byte-order mark and
Set-Content -Encoding utf8writes UTF-8 with one, and a strict parser rejects both.--out
writes UTF-8 with LF on every platform. It refuses an existing file by default, because a client
configuration usually holds other servers, and it writes nothing at all while placeholders remain,
so filling them in and running again still works. -
epics-init --absolute-commandputs the resolved path of the installed server into the block
instead of a bare command name. A client launched from a desktop icon does not inherit your
shell'sPATH, which is the commonest reason a correct-looking configuration reports only that
the server did not start. An unresolvable command is now an error rather than a silent fallback. -
epics-initwarns when the configuration it emits arms a write gate. The check that follows
prints the resulting posture (see the entry below) but does not evaluate whether such a server
would START, and with--no-checkno check runs at all. The warning also no longer claims that
the loopback-only search reach is a start condition of both gates: it is one of the PV gate
alone, while the durable audit path is required by both. It only points at "the check below"
when one actually follows: the warning is deliberately emitted before the two branches that
return without running a check (--no-check, and a block with placeholders left in it), which
are the two cases it exists for, and on both of them it used to promise output that never came. -
epics-doctorprints what each write gate would allow, and where a write can go. A new
Write gatesblock, in the human report and as awrite_safetykey in--json, covering both
the PV gate and the Olog logbook gate. A gate that is OFF gets one line saying so; an ARMED one
adds its allowlist (a PV name pattern, or a set of logbook names), its rate limit, and where a
write would actually go, which is the EPICS search reach for the PV gate and the target URL for
the logbook gate. The audit log is named either way, with whether it can be appended to, cannot,
or could not be decided without creating the file.--jsonalways carries every field.
Informative: it changes neither the verdict nor the exit code. It reports the
environment of the command you ran, not necessarily that of a running server, and the heading
says so. Four states are spelled out rather than left to the reader, because the obvious reading
of each is its opposite: an empty PV pattern on an armed gate makes the server refuse to start,
an empty logbook allowlist denies every write, an allowlisted remote target reaches a real
logbook rather than a sandbox, and an audit verdict that could not be decided is not a "no". The
audit check opens an append handle and writes nothing; where a file does not exist yet it says
the answer cannot be determined rather than guessing. A PV pattern that does not COMPILE is named
as such instead of being shown like a working allowlist, since that too refuses the start; and
the line about a pattern's width says what was CHECKED, a comparison against a fixed list of
allow-everything spellings, rather than calling the pattern narrow, because a pattern can admit
every name and still be written outside that list. -
Every
validate_pvsfile-mode answer now names the file it is about. Thefile_pathecho used
to appear on the empty-result answer only, so one mode came back with two different key sets and a
client reading both had no stable key to match a result to its call. It now travels with
shown_by_displayandshown_by_display_cappedas one group of file-mode fields, present together
on both file-mode answers or on neither. Passing a NON-EMPTYpv_namesstill drops all three, and
that is the point rather than an omission: the list wins, no file is opened, and echoing a path
there would say the answer came from a file that was never read. An EMPTY list does not win, so
the file is read as usual and the fields come back. The value is the argument as passed, not the
resolved path, so it matches what the caller sent; with a symlink the counts describe the target
while the field names the link. Purely additive: no existing field changes meaning, and the only
visible difference on the empty-result answer is where the key sits in the object. -
validate_pvsreports the glob cap, so the absence of a lower-bound note stops meaning
"complete". The display walk has two ways of running out of budget, and this tool only ever
named one of them. Beside the per-display context cap there is a glob cap: a<file>reference
that still carries a macro is resolved by globbing the known displays, and past the cap the
surplus matches are dropped, which removes whole embedded SCREENS rather than instances. It can
therefore shrink either view while bothcappedverdicts stayfalse, and a caller reading the
quiet answer as complete was reading it wrong. A separatenotesentry now names it, worded as a
statement about the walked dataset rather than about the queried file, because the engine records
the source display of a capped glob and this tool does not turn that into a per-file verdict.
coverage_auditandcrossplane_checkhave reported the same signal since they shipped; this
brings the third display tool in line and adds the regression testcoverage_auditnever had.
How much this is worth is measured rather than assumed: on a 2878-display dataset the diagnostic
holds 16 distinct source/target pairs across 4 source displays, and lifting the cap grows 7
display views (the largest by 651 channels). On four datasets between 13 and 485 displays it
never fires at all. Those same figures are why the note names no file: only 3 of the 7 growing
views appear among the reported sources, so a per-file flag built on that field would miss the
majority of the damage while looking precise.
All three notes also stop calling the dropped references "template" ones. Measured against the
engine, the diagnostic is fed by every glob-resolved<file>reference EXCEPT the template
ones (embedded, navtabs, open_display and the rule edges), which is the opposite of what the
wording said; the sentence has been wrong incoverage_auditandcrossplane_checksince they
shipped and is corrected in all three. -
Python 3.14 joins the supported set, and CI tests it. The trove classifiers, which ship
in both the wheel and the sdist, now advertise 3.12, 3.13 and 3.14, and the CI matrix runs the
suite on all three rather than on the two it used to.requires-pythonis unchanged at
>=3.12, so nothing an installer resolves moves; what changes is that the top of the
advertised range is now tested rather than merely permitted, which is the gap a classifier
cannot express on its own.
Fixed
-
A caller could write fabricated records into the audit trail. Both write gates build their
audit records out of values a caller chooses (a PV name; logbook and level names), and the trail
is one record per line, so a newline in such a value ended the record and started the next one.
Measured on a server with the PV write gate OFF, i.e. a caller permitted to write nothing at all:
aset_pv_valuecall that was refused before any network access left three lines in the file, the
middle one a complete, timestamp-bearingevent=ALLOWrecord naming a PV nobody wrote. After that
anALLOWline no longer implies a write happened, which is what the trail exists to say. Every
record is now escaped as it is FORMATTED, in the audit handler itself, so it also covers any
field added later and any caller that never heard of the rule. The escape covers every
character some reader treats as the end of a line, which is wider than it first looks: C0 and
DEL for a byte-oriented reader such asgrep, plus C1 (which contains U+0085 NEL) and U+2028
and U+2029 forstr.splitlinesand any Unicode-aware log reader. No legitimate record
changes: a real record's fields are identifiers, error codes andrepr-formatted scalars,
and the non-ASCII characters one does carry (units, accented names) all sit above U+009F. A
record that DID carry a separator now shows it asrather than breaking the line, so
nothing a caller sent is lost. -
epics-testpv's writable switch stopped accepting writes after the first garbage collection.
TEST:Heateris documented as a writable switch, anddocs/mcp-clients.mdshows a write-enabled
configuration whose allowlist is^TEST:.*so that a documented write has a target. The command
passed its PV provider to the p4p server as an anonymous temporary, and p4p keeps no Python
reference to one, so the first cyclic collection deallocated the shared PVs and removed the write
handler from a server that kept serving. Reads were unaffected, which is why nothing looked
broken. The refusal also named the wrong operation: the pinned pvxs answers an unhandled write
withRPC not implemented by this PV. Writes toTEST:Heaternow work for the life of the
process.⚠️ No published version was affected: the command itself is new in this release
(see Added), so this repairs it before it ships rather than after. It is recorded because it
is what a user would have met, and because it is the cause of four red CI runs on this
repository that had been read as a flaky test. -
An Olog write refused by the URL boundary no longer hands the caller the configured
EPICS_MCP_OLOG_URL. That field acceptshttps://user:password@host/Olog, and the refusal
reaches a caller verbatim, so a credential written into it was disclosed on the gate's most
ordinary path: "remote and not allowlisted" is the boundary's normal state, and no service has to
be unreachable for the message to be produced. Measured before the fix, all four write tools
(create_log_entry,reply_to_log,add_log_attachment,update_log_entry) answered with the
password in clear text. Wire change: the message now names the variable instead of its value,
points atolog_write.target_allowedinepics-pv://healthfor the gate's own verdict, and ends
with an instruction not to route around the refusal; a client matching on the old text must be
updated. The configured address is disclosed to a caller on no surface, which is the posture
epics-pv://healthandepics-pv://configalready held for this field; an operator reads it from
epics-doctor'sWrite gatesblock, which needs that command's own environment to arm the gate.
The logbook-allowlist refusal still names the logbooks it refused, which cannot carry a credential.
⚠️ This closes the refusal, and the other route out for the same value is closed separately in
this release. A credential inEPICS_MCP_OLOG_URLalso travelled with an ordinary HTTP failure
of a PERMITTED target, including a loopback sandbox URL spelled with a userinfo; that is the
shared REST layer, and the entry BELOW, "A credential in a service URL reached the client
through error text", closes it. The server LOG stays unredacted by decision. -
epics-doctor's final verdict named one problem and hid the others. The line an operator
reads last named only the highest-ranking of the three honest-but-not-healthy categories
(inconclusive identity probes, degraded planes, unverified planes) and said nothing about the
rest, so someone who fixed what it named then discovered the next one: two problems read as
almost finished. Measured over every state the tool can report, eleven of sixteen printed a
verdict that hid at least one plane NAME. Every category present is now named, with its planes,
and the earlier(N other plane(s) also unverified)clause is gone: it covered one of the two
categories it outranked and gave a count where the rest of the report gives names. ThePROBLEM
verdict, which had named no plane at all, now names the planes that FAILED before it names the
others, so the last line does not put the harmless ones first. Exit codes,--jsonfields and
the per-plane block are unchanged; this is the verdict line only. -
A password in a service URL was handed to the client in the clear.
epics-pv://config
printedchannelfinder_url,archiver_urlandalarm_urlexactly as configured, and those
fields are unvalidated strings:https://user:password@host/pathis an ordinary spelling for
them, so a deployment that used it published its password into whatever transcript the client
keeps. A userinfo is now removed from each of them. Everything else is kept CHARACTER FOR
CHARACTER, because the documented use of this resource is to compare the running server's
configuration with the block in a client's configuration file, and the redaction that already
existed for the logbook URL cannot serve that: it rebuilds the address from the parse, which
lower-cases the host, drops a query and a fragment and percent-encodes a space. The boundary is
urllib3's, the parserrequestsconnects through, so a password containing@loses its whole
tail rather than only the part before the first one. An address whose userinfo cannot be removed
provably isnullinstead of printed: a URL the parser refuses, one with no scheme or no host,
a spelling in which the@is not a userinfo at all, one where an@survives the removal, and
a cut whose result no longer names the same address."(disabled)"still means the plane is not
configured, and the two are different answers.⚠️ A token in a QUERY STRING is not removed; that
is the price of the character-for-character promise, and an error message drops the query for
exactly the opposite reason (it names an address rather than being compared against one).
⚠️ This covers the resource, not every route out of the process; the error route was a second one
and is closed in the entry below, while the server LOG is deliberately not. Credentials belong in
the
EPICS_MCP_*_AUTHheader variables of the four planes that have one (ChannelFinder, Archiver,
Alarm, Olog), never in a URL. -
A credential in a service URL reached the client through error text, and through
notefields
of payloads that were returned SUCCESSFULLY. Measured 2026-08-13 over every REST-backed tool:
each one disclosed a userinfo configured into anEPICS_MCP_*_URL, most in the error envelope,
and four in a payload a client keeps (diagnose_connection,lookup_device_name,
list_log_levels, andsearch_logbookwith alevelfilter). A server answering 401 disclosed
it twice per message, becauserequestskeeps the userinfo in the prepared URL that its own
error text quotes. Wire change, three shapes: an address is now printed without its userinfo
and without its query, or as(unparseable)where that cannot be proven; a served status reads
HTTP <code> <phrase>from this client's own table instead of the responding server's words; and
the three Olog listing labels and the two ChannelFinder ones name their ROUTE (GET /levels)
rather than a full URL. A client matching on the old text must be updated.⚠️ The transport cause
is passed through unchanged, deliberately: it is the only place "connection refused", "name not
resolved", "timed out" and a TLS failure are distinguishable, and it carries no userinfo.
⚠️ Two routes are unchanged and one of them has no remedy: the server log is deliberately
unredacted, and the Naming plane has noEPICS_MCP_*_AUTHvariable, so a credential it needs can
only live in its URL.epics-doctor's own pattern-based redaction is tracked separately. -
epics-doctorprinted a rebuilt Olog address next to a verdict about the configured one. The
Write gatesblock prints the write target with its userinfo, query and fragment removed, which
also lower-cases the host and percent-encodes a space, while the verdict beside it comes from the
gate's own comparison, and that one readsEPICS_MCP_OLOG_URLexactly and case-sensitively.
Measured: with the host configured in mixed case the report prints, character for character, the
string already inEPICS_MCP_OLOG_WRITE_URL_ALLOWLISTand still says the target is not
permitted, so an operator repairing the allowlist from that line ends up comparing two values
that read identically and stays denied. Both target verdicts that can rest on the allowlist, the
refusal andREMOTE and allowlisted, now say that the line is shown for reading and that the
gate works fromEPICS_MCP_OLOG_URLexactly as configured. The allowlisted one is not
decoration: an operator tidying a working allowlist to match the printed address turns the gate
into a deny-all, since the gate keeps comparing the mixed-case original. The note deliberately
claims neither a repair nor that a comparison took place, and the second half of that had to be
learned: seven states reach the refusal without the allowlist deciding anything, five of them
vetoed as unparseable, one short-circuited by an unsetEPICS_MCP_OLOG_WRITE_ALLOW_REMOTEand
one denied by thehttpsrule after the comparison had SUCCEEDED. The loopback verdict gets no
note, because being loopback is a property of the address itself. The printed address is
unchanged, and so are the exit code,--jsonandepics-pv://health: this is two lines of the
human report. -
Editing a logbook entry you had just read destroyed it, and nothing said so. A read gives a
body back in two shapes, the rawsourceits author wrote and thedescriptionthe server
rendered from it, and nothing marked which of the twoupdate_log_entrywants. Since that
tool'sdescriptionREPLACES the whole body, the obvious sequence, read the entry, add a line,
write it back, replaced the raw body with its own rendering, and whatever the rendering had
dropped was gone for good: this server cannot reach the archived version. All three tool
descriptions now namesourceas the field a round trip reads.update_log_entryadditionally
returns awarningsentry when the new body starts with the entry's own rendering and NOT with
itssource, which covers writing the read value straight back and appending to it. A body
rewritten in the middle, or prepended to, is the same mistake and is not detectable that way, so
the warning is a safety net rather than a gate; and it says nothing about how MUCH was lost,
since the renderer rewrites plain text too. -
A configured value could still forge a line of the
epics-doctorreport, outside the write
block. Control characters were escaped in the write block and nowhere else, while the report
builds four more lines from values it did not author: the two ChannelFinder redaction allowlists
and a plane's detail, which carries the raw EPICS search-path values. A newline in one of them
put a complete secondWrite gatesblock, readingPV write: OFF, above the real one reading
ARMED, together with a secondOverall:line; a raw escape byte reached the terminal, where a
conceal sequence hides everything printed after it. Every line the report builds from a
configured value is escaped now. Long values keep their full text where they are instructions: a
remedy is no longer at risk of being cut in the name of this. -
The shipped operator guide listed four of the Olog gate's six checks. The list had already
been corrected once, from "four" to "SIX", with a sentence saying the two easily-missed ones
"are listed here"; the two were not added, so the sentence announcing the correction sat above
the incomplete list it was correcting. The non-empty-target-logbooks check and the attachment
size cap are bullets of their own now, in the order the gate applies them. -
An ABSOLUTE audit path was reported as relative. The
Write gatesblock warned that the
server would resolve the path against a different working directory, for paths whose meaning does
not depend on one. The test was whether the resolved string differs, andos.path.abspathalso
normalises, so an absolute path written with forward slashes, or a POSIX one containing a/./
segment, took the warning. It now asks whether the path is absolute; a merely respelled one is
named as the same file. -
A broken
EPICS_MCP_AUDIT_LOG_FILEcould kill a write-enabled server with a bare
traceback. Both write gates promise that an unusable audit path fails as a named
configuration error, and two inputs escaped that promise because the clause caught only
OSError: a NUL byte in the path raisesValueError, and a non-string raisesTypeError.
Both now produce the named refusal. Not reachable through the environment, since an
environment value cannot carry a NUL, and reachable through a configuration built in
process. -
The documented
epics-doctor --probe-pvexample could only ever fail. It named
SIM:PS-01:Cur-RB, a synthetic placeholder that looks exactly like a real PV name, so following
the deployment guide produceddisconnected, exit 1, and a remedy pointing at the IOC of a PV
that never existed. -
The ChannelFinder privacy example switched the default off while appearing to extend it. It
showedEPICS_MCP_CHANNELFINDER_SAFE_OWNER_ACCOUNTS=recsync,ioc-svc; the built-in default is
recceiver, and a value REPLACES the default rather than adding to it, so copying that line
redacted every real owner. The example now repeats the default beside the addition and the
replace-not-merge semantics are stated. -
The quick start's second step told you to run the server by hand, which is the one thing its
own--helpsays not to do: started that way it waits silently for JSON-RPC, which reads as a
hang, and nothing consumed it, because the client starts the server itself. -
The quick start described the wrong output for
epics-diagnose. It promised "four lines
beginningPV:and ending inconnected, value=21.5", and neither half holds: a connected PV
always gets a next-step line as well, so the report cannot stop at the live line, and where the
PV reports an alarm severity that line carries it after the value. Measured against a real PV,
the connected case prints seven lines. The page now describes the shape of the report rather than
counting its lines, including the per-plane line every consulted service adds, so the next reader
compares the right thing. -
Two surfaces overstated what a shell redirect breaks. The quick start and the setup prompt
said it produces "bytes no JSON parser accepts". Measured on the three files in question:
Python'sjson.loadsreads all of them from raw bytes, becausedetect_encodingrecognises the
UTF-16 and UTF-8 byte-order marks; Node and Python in text mode reject the two that carry one.
Both surfaces now say what the four other places describing this already said, that a STRICT
JSON parser rejects them, and they name the shell it applies to: Windows PowerShell 5.1, not
Windows. Measured on the same machine, a redirect in PowerShell 7 and incmd.exewrites
BOM-free UTF-8 that every parser reads. The remedy is unchanged: let--outwrite the file. -
The refusal on an install without the display engine named three of the five commands that
still work.epics-crossplaneandepics-coverageare the only two that need the
opi_navigationengine; asked to run without it they refuse and tell the reader what else is
available. That sentence still said "the other three commands" and omittedepics-initand
epics-testpv, both added after it was written, so a reader on a published install was told
that two commands they can actually run are unavailable.docs/tools.mdcarried a different
incomplete set, omittingepics-mcp. Both now name all five. -
Writing a switch by its LABEL is verified again: landed and not-landed no longer give the same
answer.set_pv_valuereads every write back, but on an enum PV the value read back is the
numeric index while the label rides in a separate block, so a written label such asOnmatched
neither comparison: a landed and a not-landed write both came backverified: nullwith a
READBACK_UNVERIFIEDaudit line, which is one answer for opposite facts. A written label is now
resolved against the record's own choices, case-sensitively and first match wins, and compared by
index:verified: truewhen it landed,verified: falsewhen it did not. Writing the index
instead was already correct and is unchanged, andreadbackstill carries the index, exactly as
get_pv_valuereports it. This matters most on a command or reset record, which declares no drive
limits: the pre-write bounds check does not cover it either, so the readback was its only value
safety net. One consequence to expect there: such a record often clears itself after the pulse,
and if it has already cleared when the readback arrives it reads back its idle state, so the write
is reported as a mismatch rather than as unverified. That is what the readback saw; judge the
effect of such a command from the record's own status. -
The display tools no longer miss what sits inside a tabbed widget. All four display-aware
tools (validate_pvs,coverage_audit,crossplane_check,find_device) read the display tree
through the shared navigation engine, and that engine walked a display's widgets flatly. A tabbed
widget does not hang its content as a direct child: the display format nests it one level deeper,
under the tab container. Everything inside a tab was therefore invisible, so navigation targets
reached only from a tab were reported as unreferenced, and PVs that live only on a tab page were
absent from the answer. Measured on a 97-display set, restoring the descent raises the edge count
from 1493 to 1605 and theopen_displayedges from 92 to 204; across a larger corpus it recovers
253open_displayactions and 36 navigation widgets spread over 23 files, one of them an operator
entry point. Nothing warned about it, and that is the part worth knowing: a target made
unreachable this way still counts as having no incoming link, and a display with no incoming link
is seeded as an entry point, so the reachability ratio stayed at a clean 1.0 while the edges were
missing. Expect a display set to report MORE references and MORE PVs than before, not fewer.
Two further engine fixes ride along: a display whose glob-resolved reference matched its own file
no longer loses its entry-point status over that guessed self-link, and the inventory walk itself
got substantially faster on large sets. Server behaviour and every wire field are unchanged; only
the completeness of the underlying analysis improves.
Changed
-
BREAKING:
epics-initrefuses--probe-pvtogether with--no-check. That call used to
exit0and emit the block; it now exits2with a usage error, so a script passing both
breaks. It was accepted in silence while--probe-pvwas never read, because the run returns
before the check that would have used it.--probe-pvis the option that turns "nothing is
misconfigured" into "something actually works", and the quick start recommends it for that
reason, so a user who passed both believed a PV had been probed when nothing had. Nothing else
is lost: the emitted block is byte-identical with and without--probe-pvunder--no-check.
Drop one of the two options; without--no-checkthe PV is read as before. -
BREAKING:
epics-init --listrefuses every option it used to swallow.--list --probe-pv NAME,--list --no-check,--list --set NAME=VALUE,--list --absolute-commandand
--list --out ""used to exit0and print the preset listing while the option they carried was
never read, because--listreturns before any of those values is used. All five now exit2
with a usage error, so a script passing one breaks. Nothing is taken away: the listing is built
from the presets alone, so none of them could have changed it. The fifth is the one an earlier
draft of this entry got wrong:--list --out PATHwas already refused, but the older rule tested
--outfor TRUTH, so an empty value slipped past it and printed the listing. It keeps its own
sentence, which says something the general one cannot, whenever--outis the only dead option
in the call. The refusal is ONE rule that holds the parsed options against their defaults rather
than a named rule per option, so an option added later is covered the day it is added, which is
the gap this closes: these had accumulated one at a time. Calls that already exited2may say
why differently.--list --forceused to be answered with "add--out", a repair--list
refuses as well;--list --no-check --probe-pvused to be answered as if those two were the
problem; and--list --out PATHalongside another dead option now names all of them at once
instead of sending you back for one more round per rule. -
A failing
archiver_retrievalplane no longer sends you to the variable that just passed.
WithEPICS_MCP_ARCHIVER_RETRIEVAL_URLempty the plane probes the MGMT URL, which is right for a
single-JVM appliance, but a finding then opened withEPICS_MCP_ARCHIVER_URLand the remedy
promises that the variable to edit is the one named at the start. On a split deployment that is
the variable which had just been reported healthy on the line above, so following the advice
broke the working half and left the broken half broken, while the setting that actually helps was
never named. The finding now opens with the empty retrieval variable and says that the plane fell
back; the MGMT variable is still named, because that URL really was the one probed. It says so
only where the host ANSWERED and just the webapp is in doubt: when nothing answered at all, the
MGMT plane has failed on the line above too and its address is what needs repairing, so that
finding reads as before. Only the fallback case changes, and only itsdetailtext: a plane with
its own retrieval URL is untouched.--jsonconsumers matching the old opening words of this one
finding will not find them. -
The
api_errorremedy no longer names one webapp as the right one for every plane. It ended
"for an Archiver Appliance the mgmt port and not retrieval", and the remedy table is keyed by
status and read by every plane, so onarchiver_retrievalit recommended the endpoint the probe
had just failed against, which is the signature of a split deployment. It now states which
question to ask (which webapp does this plane read, and from which variable) instead of answering
it for one plane. Affects thedetailtext of everyapi_errorfinding. -
epics-pv://healthnow says whether the server may write, and which planes it has. It
described the PV write gate only, so a server whose LOGBOOK gate was armed, with a service
account and an allowlist behind it, reportedwrite_enabled: falseand nothing anywhere
contradicted it. New fields:any_write_gate_armed(the whole write answer in one field, because
deriving it fromwrite_enabledis the mistake this fixes), anolog_writeblock with that
gate's allowlist, rate limit and target predicates,
write_pattern_is_a_known_allow_all_spelling(named for its METHOD, since true means
certainly wide while false does NOT mean narrow: it compares the pattern against a closed
set of spellings rather than reading the expression),naming_enabledandarchiver_retrieval_enabled(the payload named four of the seven planes the
doctor probes), and apv_searchblock saying whether PV searches broadcast into the local
subnets. Deliberately absent, because a client keeps this payload: the Olog URL, the audit path,
and the raw address lists. Those stay withepics-doctor.
⚠️ Breaking, in one field each:write_pattern(health) andpv_write_pattern(config) are
nownullwhen no pattern is set, where they used to be the string"(none)". That string
claimed a state the server refuses to start in, since an armed gate with an empty allowlist
raises at construction, and nothing distinguished it from a pattern whose text is that word. -
The deployment guide answers the questions that come AFTER it starts. Its troubleshooting
section was entirely bring-up; it now also covers an instance that runs and may be pointed at the
wrong facility, how to stop one (you do not, directly: an stdio server belongs to the client that
launched it), what a runtime write refusal says, and the honest answer to "what has it read out
of my facility", which is that no read is logged anywhere on this side. A new section covers
uninstall and downgrade, including the four things an uninstall leaves behind: the block in your
client configuration, the audit log on the path you chose, the framework's update-check cache,
and any logbook entry a sanctioned Olog write created. And there is a plain statement about
service versions, with the reasoning for not publishing a tested-versions table: what the server
expects is documented, andepics-doctorplusget_appliance_infomeasure what YOUR services
actually are. -
set_pv_valueno longer reads as the authority over whether a write lands. Its description
said "the load-bearing, client-independent guard" and closed by calling the safety layer "what
actually gates the write". Both are now scoped to what this server decides, which is whether it
ATTEMPTS the put; the IOC's own access security decides whether the value lands, and this server
neither reads nor models it. The description points atepics-pv://guidefor the detail rather
than growing, which is this repository's own convention for that. Nothing about the gates or the
wire changed.⚠️ What a refusal at the IOC looks like from here is stated in the guide as not
measured: no live test in this project has an IOC decline a write. What is measured, and what
the guide sends you to instead, is the always-on readback: where the server can read the value
back and compare it, a value that did not land comes backverified=false. Where it cannot,
verifiedisnullwith a note saying why, which is the answer on three measured paths (the
readbackpv_getitself failed, the readback carried no live reading, or the written value is
not comparable with what came back).falseis a measurement andnullis the absence of one;
neither is a statement about the IOC's reason. -
The alarm reply says which of its two levels answers "why".
get_pv_infonow states in its
own description thatalarm.status_textis the coarse pvData NT category of the alarm SOURCE
(DEVICE, RECORD, DB and the like) while the fine CA STAT condition (HIHI, LOLO, UDF, SIMM) is
plain text inalarm.message. Nothing about the payload changed; what changed is that an
assistant reading a PV over its HIHI threshold no longer has to guess why it says
status_text=RECORD, and no longer reports a threshold breach as a record fault.get_pv_value,
get_pvsandmonitor_pvpoint at the same paragraph, since their descriptions already delegate
the alarm block toget_pv_info. -
A refused PV write now says what NOT to do next. Both
PVWriteDeniedErrormessages, the
disabled gate and the allowlist miss, carry an instruction not to route around the refusal by
writing a different PV or taking another route, and to report it to the operator on duty. The
allowlist miss got it too, although only the gate was asked for: that message names a PV, and
naming one PV is what invites trying its neighbour. The gate message still says which variable
arms it, the escalation stands beside that remedy rather than replacing it. -
Documentation reorganised around getting it running. The deployment guide gains a
troubleshooting section (symptom first: the client says nothing, the tools do not appear, the
saved file is rejected, a write-enabled block will not start), a layout of what an install puts on
your machine versus what only exists in a checkout, and the fact thatepics-doctorreads the
environment of the process YOU start rather than a client configuration file. The MCP client page
now says where that file lives per platform, that thecommandis a bare name something has to
resolve, and that the client must be restarted afterwards, which no page previously mentioned. -
docs/safety.mdstates what leaves your machine for every plane, not just for logbook reads,
including one outbound call that is not ours: the MCP framework checkspypi.orgfor a newer
version of itself when it prints its startup banner. SetFASTMCP_CHECK_FOR_UPDATES=offto stop
it, which matters in a segmented network where it has nowhere to go. -
validate_pvsaccepts a.pltData Browser trend, where it used to refuse one. The
display-PV engine collects two kinds of file,.boboperator screens and.plttrends, and this
server was pinned to a revision from before that second kind existed. While that pin stood, the
refusal "the inventory reads.bobfiles only, so this call can only come back empty" was simply
true. The pin has now moved, and with it the refusal would have become a malfunction wearing the
clothes of a safety check: a rejection whose stated reason had stopped being the case. Passing a
trend now returns its trace channels with their connectivity, exactly like a display.
Nothing that worked before changes: a.bobbehaves as it always did, and every other suffix
is still refused up front withINVALID_INPUT, before the inventory walk, for the same reason as
before. The refusal message now names both readable kinds instead of one, so a client that
guessed wrong learns what else it could have passed.Which view finds a trend depends on how the trend is REACHED, and that is worth knowing before
the call. A trend embedded in a screen through adatabrowserwidget has its traces attributed
to that screen, so only the trend's ownview="file"finds them here; a trend opened by an
open_filebutton is a top level in its own right and answers under either view. A trend is not
a screen and is not reported as one: the inventory carries the kind as its own field.Two consequences for the other display-aware tools, neither of them a change to those tools:
coverage_auditandcrossplane_checknow see the trace PVs of trends under their
displays_dirroot, andfind_devicecan return a button-opened trend among the screens that
show a device. -
The four display tools now call the inventory walk's context cap by ONE name, and two of them
stop hiding its second limit. The same cap was named three ways across the four
(per-display context cap,per-instance context cap, and the barethe context cap), and a
fourth way turned up insidecrossplane_checkbeside its own, so one service named it two ways.
This breaks a reader rather than a computation: an assistant that has read a tool'scontext_cap
description and then searches the notes for that wording did not find it, on the very tool it had
just read. One of the four was also wrong.per-instanceis whatcrossplane_checkcalls the
INVENTORY, while the cap counts reachability contexts per FILE, so that note named the thing the
cap shortens instead of the cap. Every note now readshit the per-display context cap, the
wording all four argument descriptions, both CLI help texts and the shipped operator guide
already used. Separately, thecontext_capdescriptions ofcrossplane_checkand
coverage_auditnow also name the GLOB cap: both tools emit a note about it, but neither
description mentioned it, so the only two limits a caller could learn about from those two tools
were one each. No behaviour change, no field changed, and the counts are untouched: this is the
wording beside the numbers that were pinned together in the previous release entry. Wording is
now pinned too, across every place a tool names the cap. -
find_deviceno longer claims its screen list is complete, because it cannot know that. Three
notes and the tool description said "the screen list is complete" beside a capped live read. What
they meant is true and now says so: the LIVE cap does not shorten the screen list. What they
claimed is not: the screen list comes from the same inventory walk asvalidate_pvs, that walk
has two caps of its own, and this tool reads neither, so a screen dropped by the glob cap is
missing with nothing saying so. Wording only, no behaviour change; the caps themselves are now
reported, see the next entry. -
find_devicenow reports the two caps of the inventory walk, like its three sibling display
tools. Its screen list comes from the same macro-aware walk asvalidate_pvs,
coverage_auditandcrossplane_check, and that walk has a per-display context cap and a glob
cap.find_devicewas the only one of the four that read neither, so a screen left out by
either cap was simply absent from the answer. Two newnotesentries name the count and state
that the screen list is a lower bound. Both are statements about the run rather than a verdict
on the query, because neither cap records the screen a device lookup returns, and the absence of
a note means no cap fired on that run, never "complete". -
validate_pvsno longer calls a file's PV list a lower bound when that list cannot grow.
Under the defaultview="file", thenotesentry warning that the macro expansion hit the
per-display context cap now also requires that the file declares a macro-templated PV of its
own. A PV carrying no macro resolves to the same channel under every binding and is already
enumerated at every cap, so no larger budget can add anything through it and the file's answer is
exact. On a 257-display dataset the note stops firing on four files and no file whose list can
actually grow loses it. Note for anyone comparing against 0.5.0: that release said no file
which carried the note would lose it. Those four do, and losing it is the point.
Unchanged, and deliberately so:shown_by_display_capped, and the same note under
view="display", carry the DISPLAY verdict, which gets no such test and stays the more cautious
of the two. Read atruethere as "cannot be ruled out" rather than "known to be incomplete".