Changed
-
BREAKING: the three MCP resources moved from
epics-pv://toepics://.
epics-pv://health,epics-pv://configandepics-pv://guideare nowepics://health,
epics://configandepics://guide. Are you affected? Search your client config, your
scripts and your prompts forepics-pv://. No hit: nothing to do, because a client that
discovers resources throughresources/listpicks the new names up by itself. A hit: change
the scheme there. There is no alias period, and an old URI fails loudly rather than answering
something stale. The rename itself changed no payload, but two of the three changed elsewhere
in this release:epics://healthnow reportsserverasepics-pvinstead ofepics-mcp,
and the guide grew.epics://configis unchanged. Wherever an entry describes what an EARLIER
version served, here or in the released sections below, it keeps writingepics-pv://: those
sentences record the address of the day, so your search will hit them and they are not work. -
BREAKING, but only if you set
EPICS_MCP_READ_RATE_LIMIT:epics-doctornow exits3
where it exited0. The limit is off by default, and a run that is denied nothing is
unchanged in every field. Are you affected? Check whether that variable is set, above0,
in the environment yourepics-doctorjob runs in. Setting it is necessary for this change and
not sufficient: the exit moves only on a run whose own reads the throttle actually refused, so
the variable can stand for months and nothing move. No 0.6.0 output warns you in advance,
either way. Until now a run whose own reads that limit refused could still
exit0; it cannot any more, because a refused read means part of the run was never measured.
After upgrading, the new»lines name each plane whose probe the throttle refused.
What to do: if a CI job asserts exit0and goes red, raise the limit, or set it to0to
switch the throttle off, rather than lowering the assertion. The red is the finding: the0it
replaces was printed for a run that had not looked. Exit codes are0clean,1failed,2
usage,3inconclusive;3is not new, it already covered a failed identity probe in 0.6.0. -
find_deviceno longer calls a Data Browser trend an operator screen. A.pltopened by a
button is operator-facing, so the reverse-lookup returns it, and until now it arrived
indistinguishable from a.bob: counted among the screens, described as one by the tool, and on
an empty answer the note read "No operator-facing screen references this device/query" although
trends had never been counted apart. Each match now carriesscreens[].node_kind("display"
or"trend"), and the report carriesdisplay_countandtrend_count, counted positively
rather than one subtracted from the other, so a later third kind cannot land silently in the
display figure. The rendered header splits the same way and a trend's own row says what it is;
the empty answer denies both kinds. Additive on the wire: nothing was removed or retyped, and
the field stays calledscreens, because renaming it would break every caller in order to say
what its members now say themselves. The three capnotesfollow: they qualified "a screen" and
"the screen list" where a trend can be affected too, and now say "a screen or trend" and "the
match list". The description growth lands on the display-gated lane only; the core lane's
tools/listis unchanged. -
epics-doctorno longer blames your services for a limit you set on it. With
EPICS_MCP_READ_RATE_LIMITset tight enough to refuse the doctor's own reads, the report said
four different untrue things about a stack that was up. A refused TRANSPORT probe was
unreachable(exit1), telling you to check a host and port nothing had contacted; a refused
IDENTITY beacon wasidentity_probe_failed, pointing at a sub-path and an auth wall for a
request never sent; the cross-plane block then reported the host as dead and offered a swapped
archiver URL pair; and one read below the budget of a full run the refusal landed on the
archiver's ingest sub-probe, leaving every plane✓, every list empty and exit0under this
tool's strongest confirmation. A plane this command never asked is nowthrottled(»), listed
inthrottled_planesand NOT ininconclusive_identity_planes, with a remedy naming the limit
instead of a URL. A run denied any read reportsreads_denied, closesverification_complete
and exits3, which is the only signal for the sub-probe case. Scripts reading the exit code
alone see a0become a3on a deployment whose configuration did not change. -
An unwritable audit path no longer hands its full local path to a tool caller, at EITHER write
gate. Both refuse withSAFETY_CONFIG_INVALIDnamingEPICS_MCP_AUDIT_LOG_FILEand withholding
its value; the path stays in the exception'sdetails, which the tool boundary does not send.
Each gate is built on first use unless its own write flag is on, so the refusal is a tool ANSWER
rather than a start-up failure: on the Olog side from the four write tools, and on the PV side
fromset_pv_value, which builds its gate BEFORE it checks whether writes are allowed, so this
reached callers even with PV writes off. Operators reading a start-up failure on stderr now see
the variable name instead of its value and read the value back from the environment of the
process they started. Which case arrives how:operator_guide.mdunderSAFETY_CONFIG_INVALID,
anddocs/safety.mdfor the posture and the measurement behind it. -
The server now names itself
epics-pv, the key a client registers it under, in both places it
answers "which server am I".serverInfoon the handshake and theserverfield of
epics-pv://healthboth saidepics-mcp, which is the DISTRIBUTION name: the package on PyPI
and the console script keep it, and neither changes. Nothing hangs behaviour on either value (the
MCP specification reservesserverInfofor display, logging and debugging), so this costs a
reader nothing except the wrong trail it used to lay when two of the three servers in one client
log named themselves after something else. -
find_devicenow says that every match comes back with the roles it uses the device in.
The answer has carriedscreens[].rolesall along and nothing on the surface said so, so
"which screens can WRITE this device?" was answerable and unadvertised. Nothing changed on the
wire beyond the sentence. -
get_pvsnow names the field the engineering unit rides in. Its description referred to
get_pv_infofor the metadata, so a caller asking whether channels labelled alike carry the same
unit had to take a second hop to learn that one batch call answers it.unitsandprecision
are now named where they sit, indisplay, per PV. Nothing changed on the wire beyond the
sentence. -
set_pv_valuenow says that an operator screen writes through its widget, anddocs/tools.md
that no preset arms a write gate. Both were facts a caller needed before the call and neither
was anywhere the server delivers: an assistant asked to change a value visible on a screen
reached for the gated tool and was refused, andsandbox, the one shape with nothing left to
fill in, reads like the one where writing is on. Nothing changed on the wire beyond the two
sentences, and this change leaves theinitializeinstructions untouched (a separate entry in
this release does edit them). -
The
diagnose_pvprompt now establishes WHICH world it read from, and corroborates a negative
before concluding. The chain started atget_pv_infoand ended at the monitor, which taught a
plan that could reach a confident answer without ever saying which plane had answered it. Step 1
now says to read thereachfield of that first answer before interpreting anything else, and a
new final step counter-checks withfind_channels,get_pv_historyandsearch_logbook,
because a PV that does not connect looks the same whether it does not exist, is down, or is
simply out of reach from here; a negative from one plane is not a finding until a second plane
agrees. Every tool the prompt names is registered in every install, deliberately: an earlier
draft named the display-gatedfind_device, which on a core-only install would have been an
instruction the client cannot follow. This isprompts/list, a surface a client shows without
any tool call.setup_epics_mcpgains the throttle marker for the read rate limit as a fourth
finding to read out of anepics-doctorreport. -
The server's
initializeinstructions name all six Olog write-gate conditions. The
enumeration in the handshake header listed five and left out the named target logbook, so a
reader counting the gate from the header got a narrower picture than the operator guide, which
said six all along. Nothing about the gate itself changed; what changed is that the two agree. -
diagnose_connection's naming plane runs the same probe aslookup_device_name. Both used
to carry their own copy of the three-step lookup (config gate, reachability first, then
validate), which is how two answers to one question start to disagree. The gatherer calls the
shared query now, like its three sibling planes already did. Semantics are unchanged, including
that an unreachable service is withheld rather than read as "not registered". On the wire,
evidence.naming.notereadsNaming lookup withheld: ...where it readNaming error: ....
The older wording survives on one path and deliberately so: the gatherer keeps a wideexcept
for the failures the shared query does not catch, and a note from there still reads
Naming error: .... -
epics-initnames the variables from your own shell that reach its check. It strips what it
owns (EPICS_MCP_*plus the six EPICS search-path variables) and probes the block it just
printed, and that claim held only up to the variables it does not own. Measured: anHTTP_PROXY
turns six healthy REST planes into sixunreachable, each naming a host the block never
mentions, and anEPICS_PVA_BROADCAST_PORTdecides who answers a PV search (at the time of that
change thesearch paths:line said nothing about it; the entry below made the line resolve it). Stripping those too would break every site
behind a proxy and every site with an internal CA, so they are named instead, on stderr and
ahead of the report, each with what it does and what to do about it. Names only, never values: a
proxy URL routinely carries a password. SettingEPICS_MCP_CA_BUNDLEin the block silences the
HTTP half, because an explicit TLS decision makes the read sessions ignore the environment. -
get_guide: the error signatures are twelve addressable groups now, anderrorsserves the
section's lead. That section had become the largest single part of the guide, thirty signatures
with no subheading in it, so a question about one of them cost the other twenty-nine as well. It
is cut by plane and by kind, each group a topic key of its own:err-transport,err-pv,
err-archiver,err-alarm,err-olog,err-channelfinder,err-rest,err-displays,
err-crossplane,err-arguments,err-gates,err-guide. The last of those arrived with the
cross-plane entry below, in this same release. Theerrorskey therefore serves the section's own
lead, not the signatures below it, exactly astoolshas always served the inventory rather
than its subsections; ask for the group you need, and every key is listed in the argument
description and in every refusal. No signature's text changed; two moved so that each group is
contiguous. The largest single part of the whole guide is no longer an error group at all, and
the part that is largest now sits in a different section. -
coverage_auditannounces a capped display ABOVE the figures it invalidates, not below them.
A display that hit the per-display context cap makes the report's gap figures a lower bound
(a PV missing from the display set is reportedwithheldrather thanno), withcf_onlythe
one exception, since it is a set difference and a cap can only lengthen it. The report said
so twice at the bottom: a bare counting line under the numbers, and a note under that. Now one
warning stands in the head, names the consequence, and lists the capped displays; the counting
line below is gone, so the caveat is not split across two places. This warning claims no share
against the inventory's display list: the capped files and that list are different populations,
so a ratio of the two could exceed 100%. The share the header does carry arrived later in this
release and divides by the walked file universe instead. -
crossplane_checknow says HOW MANY non-channel references there are per protocol, not just
that four protocols exist. The note used to readN distinct non-channel reference(s) (loc/sim/sys/other), which named every protocol the tool knows and no number for any of them; on
a real repository that hid a 3-orders-of-magnitude spread. It now readsN distinct non-channel reference(s) (loc: 25154, sim: 36, other: 12), listing only the protocols actually present, and
the Markdown line changes with it. New report fieldpvs_non_channel_by_protocol, a
(protocol, count)tuple whose counts sum tolen(pvs_non_channel)exactly, so the headline
number and its breakdown cannot disagree. -
The operator guide no longer repeats the command-line walkthrough.
Two things had two homes:epics-init's whole section, and the parts of theepics-doctor
section that teach how to READ its report (the remedy rule, theunverifiedrationale, the write
gates block, the exit-code and--jsonscripting advice). All of it is written for a human at a
terminal, and all of it is in the deployment guide section 1, in
docs/tools.md, or beside the code it describes. Nothing was dropped without a
home: what the guide keeps is the half no tool description carries, the plane beacons, the
archiver ingest question, and what "reachable but not identified" means.epics://guideand
get_guidetherefore stopped serving that material twice, and the recipes section carries most
of the reduction. No byte figure is quoted for the guide's resulting SIZE on purpose: that number
moves with any sentence anyone edits, and other entries in this release add more than this one
removed, so the guide 0.7.0 ships is LARGER than the one 0.6.0 shipped, not smaller. Ask
get_guidefor atopicrather than for the whole of it. -
coverage_audit's cap warning reports a SHARE, not just a count. The cap warning gave a bare
count, in 0.6.0 as one line among the report's notes, and left the only question that matters
unanswerable: 104 capped files mean something quite different against 284 walked files than
against 3000. It now reads "104 file(s) (36.6% of the 284 files this walk visited)", and
--jsoncarriesfiles_walked. The noun moved with the number: the walk enqueues every
operator-facing file and a Data Browser.pltamong them, so "file(s)" is the numerator's honest
name and both halves of the ratio range over the same population.⚠️ The report's own notes still
say "display(s)" where they describe which screens were cut short, and that is deliberate.
⚠️ The denominator is the walked file universe and deliberately
not the display set: a capped file carrying no PV is in the count and not in that set, so that
share could exceed 100%. When no universe is reported the line falls back to the bare count
rather than dividing by something else. -
The display engine pin moves to
db19c08, and with it four measured corrections to what the
display tools say about a screen set. The pin had stood still at0ab0d73while nine commits
landed inopi_navigation, so every reachability and PV answer this server gave came out of a
state the engine had already corrected. What arrives:open_displaytargets are matched the way
Phoebus matches them rather than case-sensitively, and the legacy<path>spelling is read too.
⚠️ That one was measured LATENT, and the number belongs with it: over two corpora (5017 and 515
files, 2619open_displayactions) every spelling is already lowercase, so no jump appears or
disappears today; it protects a hand-edited or generator-written file. A Data Browser.plt
is no longer counted as an operator screen byfind_screenandchange_impact.⚠️ It named
find_devicetoo, and for that tool the sentence was wrong: the engine stopped counting a trend
as a screen, this server's projection kept doing so, and it took thefind_deviceentry above to
reach the wire;
displays are no longer pushed out of the entry-point list by guessed edges; and the engine now
exports the file universe a capped run walked, which is the honest denominator the
coverage_auditheader needs. -
An unreadable
EPICS_MCP_CA_BUNDLEis now reported asca_errornaming that variable,
instead of asunreachableon every https plane at once.requestsrefuses such a bundle with
a bareOSErrorbefore any handshake, which carries no status and no chained SSL error, so the
classifier had nothing to recognise it by and fell through to the transport verdict. An operator
was then told to check the host and port of six services that were fine. The verdict names the
variable and deliberately NOT the bundle path, because such a path routinely carries an account
name; read the value back from the environment of the process that reported it. This also
restores a diagnosis that the credential redaction had been suppressing: the original message
put the path in its text, and a path containing an@is withheld whole by the output barrier. -
Releasing now takes an approval, and the tag is no longer the point of no return. Two
repository settings arrived in front of the upload: only an administrator can create av*tag,
and thepublishjob waits for an approval on thepypienvironment, whose deployment policy
admits tag refs matchingv*only. Nothing about the published artifact changes; what changes is
the procedure inCONTRIBUTING.md, so anyone cutting a release from a checkout of this repository
needs to know that a pushed tag now stops and waits.SECURITY.mdstates the posture, including
the two things it is not: self-approval is permitted, and administrators may bypass it. -
The CI and release workflows check out with
actions/checkout@v7(was@v5), which blocks
checking out fork pull request code underpull_request_targetandworkflow_run. Neither
trigger is used here, so this is hardening with no behaviour change.
Added
-
A Data Browser trend no longer counts as an operator screen in
coverage_audit, andcrossplane_checkno longer promises it never did. A.plttrend opened by a button is an operator-facing top level, so a PV that appears on no.bobat all used to come back withhas_display: yesand drop out ofcf_only, the blind-spot count that tool exists to produce.has_displaynow answers about screens alone, the newon_trendanswers beside it, and such a PV is named in the newtrend_only(reported with or without a registry) and, when registered, incf_onlyand the newcf_trend_only.⚠️ cf_onlytherefore GREW, and that is the repair: measured, 25 PVs of one 87 388-channel dataset sit on a trend and on no screen, and every one of them used to count as visible.critical_uncoveredgrows with it only where the walk was NOT cut short by a cap; a capped walk withholdshas_displayrather than proving a gap, as it always did.display_onlyis unchanged and still measured against every operator-facing file. Each coverage row gainsscreens/trendsbesidedisplays, andcrossplane_checkgainsscreens_linked/trends_linkedbesidedisplays_linked, whose contents and name are unchanged. The description growth lands on the full lane only; the core lane is unchanged. -
A write answer and an archived sample now say what their fields mean, and one of them says
which epoch it is in. Both rode on the wire with nothing explaining them. For a write, the
costly confusion is thatstatusstays"success"on a readback mismatch (it describes the PUT,
which really did happen) andnew_valueis the value that was SENT, so the two together can read
as "it landed" when it did not; the measured pair isreadbackwithverified, andnote
carries the reason. Nothing about that behaviour changed, only its delivery. For an archived
sample,secsis UNIX epoch seconds and NOT EPICS epoch seconds: read the other way, every point
in a plot shifts by exactly 20 years and still looks plausible. The answer-field index, created
by the entry below, grows to route the fields of both answers.⚠️ toleranceis now documented as NOT
sufficient to re-derive the verdict: on the epsilon fallback the same value also feeds a relative
axis that is not reported, so a large value can match while differing by far more than
tolerance. Two description edits, on both lanes; the guide grows about 3.6 KB and none of that
reaches the wire. -
The guide now explains the words an answer uses, and a new
answer-fieldstopic maps a field
name to the topic that explains it. A caller meets a field name in a RESULT, not in a question,
and this surface has no free-text search, so a name such ascf_capped,config_msg,
default_level,next_stepsorarchive_fieldswas unreachable: it rode on the wire and nothing
said what it meant. That change explained fifteen such fields where their subject already lives
and had the new index route to them (the entry above has since added more). The cross-plane report vocabulary moved into its own signature group
err-crossplane(buckets, the_writetwins, the withheld ChannelFinder and.dbverdicts, the
coverage cells), so reading one bucket no longer costs the whole display-inventory section.
The two descriptions grow on both lanes; the guide itself grows about 12 KB and none of that
reaches the wire, since it is served on request. -
Every read answer now carries a
reachfield: which plane served it and how far that plane
reaches. Values areloopback-only(provably confined to this machine),beyond-loopback(it
can leave this machine) ornot-configured(no request was made).probed: falserides along
and means what it says: this is the configuration, read with no network call, so a reachable
plane may still be down. The field names no host and no port, so the two planes whose URL is
deliberately withheld (naming, olog) disclose nothing new. Until now the reach was only
obtainable fromepics-doctor, a console script an MCP client cannot run, or from the
epics-pv://healthresource, which an application must fetch on the client's behalf: an answer
could be given without anything having established which world it came from.discover_pvs
reports per branch, since a concrete name is a live PV read and a wildcard is a ChannelFinder
query. Every read tool's description grows for it, on both lanes. -
An empty read answer now says WHY it is empty. The
notepattern here fired on one
cause only, a plane with no URL, at every site that used it. So a plane that was never
asked and a plane that was asked and matched nothing arrived identically, as an empty
list: a misconfiguration was indistinguishable from a real zero. A configured plane that
matched nothing now says so, and names its reach, because a zero from a plane confined to
this machine and a zero from one that reaches a facility mean different things. A tool
that already knows a more specific cause keeps its own note; the general sentence never
replaces one. Coversfind_channels,search_logbook,list_logbooks,list_tags,
list_log_attachments,get_alarm_history,list_archived_pvsandget_pv_history. -
get_guide: the operator guide is now a TOOL, not only theepics://guideresource. A
resource is application-controlled, so a model never fetches one by itself, which left the guide
correct and unread. The resource stays; the tool is the channel a model pulls from, and the
server header now points at it.topicserves one named part of the guide verbatim instead of
the whole document. The keys PARTITION it, so a section key (posture,planes,tools,
recipes,errors) serves that section's own text and the subsections under it are keys of
their own:toolsis the drift-guarded tool inventory, not the whole palette that opens with it. All keys are listed in the argument description and in every refusal. An unknown
topic is refused by name, never guessed and never quietly answered with everything. The tool contacts no PV, no REST plane and no file of yours, so it is safe as the
first call of a session. New error codes on the wire:UNKNOWN_TOPICfor a bad key,
GUIDE_DRIFTif the shipped document and the topic table disagree. -
The operator guide explains every error code this server names. Eight codes could reach a
caller with no entry in the guide's "Error signatures" section, five of them absent from the
guide entirely:PV_WRITE_DENIED,PV_WRITE_OUT_OF_BOUNDS,OLOG_WRITE_DENIED,
RATE_LIMIT_EXCEEDED,READ_RATE_LIMIT_EXCEEDED,SAFETY_CONFIG_INVALID,UNKNOWN_TOPICand
GUIDE_DRIFT. They are grouped by the distinction a caller has to make rather than listed: a
write refused by the gate against a value refused by the record's own drive limits (only the
second has already spent a rate token), and an audited write-gate rate limit against the
unaudited read throttle, which can fire on the reads a write tool performs before its gate is
consulted. Nothing changed on the wire; what changed is that the codes are explained where the
server sends you to look.
Fixed
-
The two doctor changes below carry six corrections of their own, all made before this release,
so no version ever served the wrong answers below. The reach line printed the value of a port VARIABLE raw,
soEPICS_PVA_BROADCAST_PORT=70000named:70000where the client dialled:4464and=abc
named a live endpoint on a port that cannot exist; the same arithmetic now covers both sources,
and an unreadable value earns no number at all. The address parser answered for shapes nobody
had measured and was wrong in both directions ([2001:db8::1]xwas called DROPPED while the
client searches it;10.0.0.5,was shown as live while the client drops it) - a shape outside
the pinned corpus is now printed as written with nothing claimed about it. TheInstallation
block keyed its "none on it answered" finding on statuses that mean the host DID answer, so it
contradicted the report's own plane lines and once named a host nothing had ever contacted; it
counts only a real transport failure now. The pin was widened from the port helper to the
rendering the report actually calls, over a corpus that includes every falsifying shape. -
epics-doctorreports what no single plane can see. It checked each plane on its own, so
three failure shapes that only appear in the comparison went unnamed: the two archiver URLs
exchanged, every service on one host dead rather than N services broken, and a TLS failure on
some HTTPS planes but not all (which points at those hosts) as against all of them (which points
atEPICS_MCP_CA_BUNDLE). AnInstallationblock now names them, printed only when a pattern
matches, and--jsongainsinstallation.findings. It never moves the verdict or the exit
code: every pattern keys on a status that already drives one. Each finding says what it could
NOT tell apart and names a check rather than a change; the blind spots are dated in
docs/known-limits.mdsection 22. -
epics-doctor'ssearch paths:line named addresses but not ports, and a port decides who
answers. The line was word for word identical with and withoutEPICS_PVA_BROADCAST_PORT,
while the client dialled a different port. It now resolves every entry it can name to the
endpoint actually used and names the variable the default came from (EPICS_PVA_BROADCAST_PORT
for the address lists,EPICS_PVA_SERVER_PORTfor the name servers). Three measured client
behaviours are reported rather than hidden: a written:0or a number past 65535 is not the port
you wrote, an entry the client refuses is markedDROPPEDinstead of being shown as a
destination, and a list holding names carries a caveat that its effective form can be shorter. -
An entry written without a host (
EPICS_PVA_ADDR_LIST=":5076") gets no endpoint in the
search paths:line, and one line says why and what to do instead. What a client makes of such
an entry is decided by the platform resolver, measured on that token: one of the machine's own
interface addresses on Windows, a refused entry on Linux. Naming either would be right on one
platform and invented reach on the other, so the token is printed as written with nothing claimed
about it, the same rule this release already applies to shapes outside the pinned corpus, and the
report tells you that writing the host out (127.0.0.1:5076) gets a destination it can name. A
host-less entry whose PORT is unreadable ([]:abc) is unaffected and still reportedDROPPED:
that refusal happens before any host is looked up, so it holds everywhere. -
epics-doctorreportedlocalhost-isolatedfor aEPICS_MCP_PROVIDER=caserver that was
broadcasting PV searches into the local subnets. p4p offers PVAccess only; it acceptscaand
builds a PVAccess context anyway, soEPICS_CA_AUTO_ADDR_LIST=NOswitched off nothing while
EPICS_PVA_AUTO_ADDR_LIST, unset and therefore ON, decided the real reach. The live plane now
judges the provider the client can actually speak, names the configured one when the two differ,
and marks a search list of the other family[inert]instead of dropping it. The write-reach
boot assert is unchanged and still judges BOTH providers, deliberately, so it can still refuse a
write-enabled start over a variable this line calls inert; the line now says so. -
epics-doctorprinted a fragment of the Olog password for one spelling of
EPICS_MCP_OLOG_URL, on every run rather than on a failure. The write-gate block redacted its
target address by rebuilding it from the parse. Forhttps://svc:p@ss/w0rd@host/Ologurllib3
reads the host asssand the rest as a path, so the rebuild printed
https://ss/w0rd@host/Olog: part of the password, in the path, with no@left anywhere for a
structural check to catch. The block now redacts by deleting the userinfo and handing the result
back to the parser, and prints(unparseable)where it cannot prove the result names the same
address. Two visible consequences beyond the leak: the host keeps the case it was configured in
(the rebuild lower-cased it), and a URL whose spelling the parser refuses is withheld instead of
approximated. Put credentials inEPICS_MCP_OLOG_AUTHrather than in the URL either way.
Removed
epics-doctor's own credential redaction, which 0.6.0 documented as a residual exposure, is
gone rather than repaired, and the entry that tracked it overstated what it covered. The
command had a local guard substitutingscheme://***@into its cause texts. It matched only
user:password@and only up to the first@, so it left the tail of a password containing@
in the clear, passed a bare user name through untouched, and in every case produced a value
still carrying an@, which is precisely the character that says an address was never proven
redacted. No unredacted exception was found that could reach it, so four statements in
SECURITY.md,docs/safety.md,docs/configuration.mdanddocs/known-limits.mddescribed an
exposure on that route which measurement does not support, and they now say what was measured.
The cause texts go through the same barrier as every other client-facing message: a proven-clean
text is passed verbatim, a text carrying an@is withheld rather than rewritten. That withheld
form is a shape you can meet in ordinary use, soepics://guidesays what to do about it.
The one real credential leak on this command was elsewhere and is listed above.