Skip to content

v0.7.0

Latest

Choose a tag to compare

@github-actions github-actions released this 24 Aug 11:11
· 71 commits to main since this release

Changed

  • BREAKING: the three MCP resources moved from epics-pv:// to epics://.
    epics-pv://health, epics-pv://config and epics-pv://guide are now epics://health,
    epics://config and epics://guide. Are you affected? Search your client config, your
    scripts and your prompts for epics-pv://. No hit: nothing to do, because a client that
    discovers resources through resources/list picks 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://health now reports server as epics-pv instead of epics-mcp,
    and the guide grew. epics://config is unchanged. Wherever an entry describes what an EARLIER
    version served, here or in the released sections below, it keeps writing epics-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-doctor now exits 3
    where it exited 0.
    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, above 0,
    in the environment your epics-doctor job 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
    exit 0; 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 exit 0 and goes red, raise the limit, or set it to 0 to
    switch the throttle off, rather than lowering the assertion. The red is the finding: the 0 it
    replaces was printed for a run that had not looked. Exit codes are 0 clean, 1 failed, 2
    usage, 3 inconclusive; 3 is not new, it already covered a failed identity probe in 0.6.0.

  • find_device no longer calls a Data Browser trend an operator screen. A .plt opened 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 carries screens[].node_kind ("display"
    or "trend"), and the report carries display_count and trend_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 called screens, because renaming it would break every caller in order to say
    what its members now say themselves. The three cap notes follow: 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/list is unchanged.

  • epics-doctor no longer blames your services for a limit you set on it. With
    EPICS_MCP_READ_RATE_LIMIT set 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 (exit 1), telling you to check a host and port nothing had contacted; a refused
    IDENTITY beacon was identity_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 exit 0 under this
    tool's strongest confirmation. A plane this command never asked is now throttled (»), listed
    in throttled_planes and NOT in inconclusive_identity_planes, with a remedy naming the limit
    instead of a URL. A run denied any read reports reads_denied, closes verification_complete
    and exits 3, which is the only signal for the sub-probe case. Scripts reading the exit code
    alone see a 0 become a 3 on 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 with SAFETY_CONFIG_INVALID naming EPICS_MCP_AUDIT_LOG_FILE and withholding
    its value; the path stays in the exception's details, 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
    from set_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.md under SAFETY_CONFIG_INVALID,
    and docs/safety.md for 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".
    serverInfo on the handshake and the server field of
    epics-pv://health both said epics-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 reserves serverInfo for 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_device now says that every match comes back with the roles it uses the device in.
    The answer has carried screens[].roles all 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_pvs now names the field the engineering unit rides in. Its description referred to
    get_pv_info for 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. units and precision
    are now named where they sit, in display, per PV. Nothing changed on the wire beyond the
    sentence.

  • set_pv_value now says that an operator screen writes through its widget, and docs/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, and sandbox, 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 the initialize instructions untouched (a separate entry in
    this release does edit them).

  • The diagnose_pv prompt now establishes WHICH world it read from, and corroborates a negative
    before concluding.
    The chain started at get_pv_info and 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 the reach field of that first answer before interpreting anything else, and a
    new final step counter-checks with find_channels, get_pv_history and search_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-gated find_device, which on a core-only install would have been an
    instruction the client cannot follow. This is prompts/list, a surface a client shows without
    any tool call. setup_epics_mcp gains the throttle marker for the read rate limit as a fourth
    finding to read out of an epics-doctor report.

  • The server's initialize instructions 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 as lookup_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.note reads Naming lookup withheld: ... where it read Naming error: ....
    The older wording survives on one path and deliberately so: the gatherer keeps a wide except
    for the failures the shared query does not catch, and a note from there still reads
    Naming error: ....

  • epics-init names 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: an HTTP_PROXY
    turns six healthy REST planes into six unreachable, each naming a host the block never
    mentions, and an EPICS_PVA_BROADCAST_PORT decides who answers a PV search (at the time of that
    change the search 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. Setting EPICS_MCP_CA_BUNDLE in 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, and errors serves 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. The errors key therefore serves the section's own
    lead, not the signatures below it
    , exactly as tools has 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_audit announces 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 reported withheld rather than no), with cf_only the
    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_check now says HOW MANY non-channel references there are per protocol, not just
    that four protocols exist.
    The note used to read N 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 reads N 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 field pvs_non_channel_by_protocol, a
    (protocol, count) tuple whose counts sum to len(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 the epics-doctor
    section that teach how to READ its report (the remedy rule, the unverified rationale, the write
    gates block, the exit-code and --json scripting 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://guide and
    get_guide therefore 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_guide for a topic rather 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
    --json carries files_walked. The noun moved with the number: the walk enqueues every
    operator-facing file and a Data Browser .plt among 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 at 0ab0d73 while nine commits
    landed in opi_navigation, so every reachability and PV answer this server gave came out of a
    state the engine had already corrected. What arrives: open_display targets 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, 2619 open_display actions) 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 by find_screen and change_impact. ⚠️ It named
    find_device too, 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 the find_device entry 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_audit header needs.

  • An unreadable EPICS_MCP_CA_BUNDLE is now reported as ca_error naming that variable,
    instead of as unreachable on every https plane at once.
    requests refuses such a bundle with
    a bare OSError before 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 a v* tag,
    and the publish job waits for an approval on the pypi environment, whose deployment policy
    admits tag refs matching v* only. Nothing about the published artifact changes; what changes is
    the procedure in CONTRIBUTING.md, so anyone cutting a release from a checkout of this repository
    needs to know that a pushed tag now stops and waits. SECURITY.md states 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 under pull_request_target and workflow_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, and crossplane_check no longer promises it never did. A .plt trend opened by a button is an operator-facing top level, so a PV that appears on no .bob at all used to come back with has_display: yes and drop out of cf_only, the blind-spot count that tool exists to produce. has_display now answers about screens alone, the new on_trend answers beside it, and such a PV is named in the new trend_only (reported with or without a registry) and, when registered, in cf_only and the new cf_trend_only. ⚠️ cf_only therefore 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_uncovered grows with it only where the walk was NOT cut short by a cap; a capped walk withholds has_display rather than proving a gap, as it always did. display_only is unchanged and still measured against every operator-facing file. Each coverage row gains screens/trends beside displays, and crossplane_check gains screens_linked/trends_linked beside displays_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 that status stays "success" on a readback mismatch (it describes the PUT,
    which really did happen) and new_value is the value that was SENT, so the two together can read
    as "it landed" when it did not; the measured pair is readback with verified, and note
    carries the reason. Nothing about that behaviour changed, only its delivery. For an archived
    sample, secs is 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. ⚠️ tolerance is 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-fields topic 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 as cf_capped, config_msg,
    default_level, next_steps or archive_fields was 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 _write twins, the withheld ChannelFinder and .db verdicts, 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 reach field: which plane served it and how far that plane
    reaches.
    Values are loopback-only (provably confined to this machine), beyond-loopback (it
    can leave this machine) or not-configured (no request was made). probed: false rides 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 from epics-doctor, a console script an MCP client cannot run, or from the
    epics-pv://health resource, 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 note pattern 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. Covers find_channels, search_logbook, list_logbooks, list_tags,
    list_log_attachments, get_alarm_history, list_archived_pvs and get_pv_history.

  • get_guide: the operator guide is now a TOOL, not only the epics://guide resource. 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. topic serves 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: tools is 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_TOPIC for a bad key,
    GUIDE_DRIFT if 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_TOPIC and
    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,
    so EPICS_PVA_BROADCAST_PORT=70000 named :70000 where the client dialled :4464 and =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]x was 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. The Installation
    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-doctor reports 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
    at EPICS_MCP_CA_BUNDLE). An Installation block now names them, printed only when a pattern
    matches, and --json gains installation.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.md section 22.

  • epics-doctor's search paths: line named addresses but not ports, and a port decides who
    answers.
    The line was word for word identical with and without EPICS_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_PORT for the name servers). Three measured client
    behaviours are reported rather than hidden: a written :0 or a number past 65535 is not the port
    you wrote, an entry the client refuses is marked DROPPED instead 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 reported DROPPED:
    that refusal happens before any host is looked up, so it holds everywhere.

  • epics-doctor reported localhost-isolated for a EPICS_MCP_PROVIDER=ca server that was
    broadcasting PV searches into the local subnets.
    p4p offers PVAccess only; it accepts ca and
    builds a PVAccess context anyway, so EPICS_CA_AUTO_ADDR_LIST=NO switched 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-doctor printed 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. For https://svc:p@ss/w0rd@host/Olog urllib3
    reads the host as ss and 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 in EPICS_MCP_OLOG_AUTH rather 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 substituting scheme://***@ 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.md and docs/known-limits.md described 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, so epics://guide says what to do about it.
    The one real credential leak on this command was elsewhere and is listed above.