Replaces the "Reusing a userId" subsection with a clearer
"userId is session-scoped" note that captures the actual
GRAccess/MxAccess contract:
- AuthenticateUser returns an int that is bound to the LMX session
hServer it was issued against, exactly like tag handles from
AddItem. When the session unregisters (i.e. the MxSession is
disposed, which happens when the process exits) the userId is
no longer valid.
- Each mxa write invocation creates a fresh MxSession, so an
authenticated write must include --username/--password (or a
userId resolved within the current process).
- --user-id <N> is a hand-off for in-process callers, not a
persistable credential.
This matches the user's observation that userIds behave like
tag/item handles in scope, and explains why batch flows benefit
from a future session-mode mxa daemon (modeled on graccesscli's
session) that holds one MxSession across many commands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WriteCommand now picks the LMXProxyServer advise variant based on
whether credentials were supplied:
--username given -> Advise (operator action; the write
is attributed to the
authenticated Galaxy user
in the alarm/event audit
trail)
no --username -> AdviseSupervisory (supervisory action; the
write is attributed to the
hosting client itself, no
Galaxy user claimed)
MxItem grows AdviseSupervisory() alongside Advise() and shares the
same UnAdvise / RemoveItem teardown.
Verified live with the trigger / ack-as-dohertj2 / clear sequence on
TestMachine_001.TestAlarm002. The Set (anonymous, supervisory) and
Clear (anonymous, supervisory) rows pair with the Acknowledged row
(authenticated, Advise) under one Alarm_ID. On this development
galaxy every action still maps to User_Name=DefaultUser regardless
of advise variant — that's a galaxy-security configuration trait,
not a CLI bug. The routing is in place and will differentiate
correctly on a strict galaxy with real user records.
docs/usage.md gains an "Advise variant" section explaining the rule
and the expected User_Name population on strict vs permissive
galaxies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps LMXProxyServer.AuthenticateUser at the session level
(MxSession.Authenticate) and surfaces three new options on the
write command:
-u, --username <name> galaxy / OS user
--domain <name> composed as <domain>\<username>; omit for
galaxy-authenticated logins or UPN forms
-p, --password <pwd> redacted to "***" in the JSON query echo
The CLI calls AuthenticateUser before AddItem so an auth failure
(userId == 0) bails out cleanly without leaving a half-set-up item.
On success the resolved userId flows into Write(hServer, hItem,
value, userId) and is reflected in:
- human output: "[OK ] write <tag> = <val> (as <verify-user>, userId=N)"
- LLM-JSON results[]: { "authenticated": true, "auth_user_id": N }
Verified live against TestChildObject.TestInt with credentials
DESKTOP-6JL3KKO\dohertj / Sonamu89:
read -> 99
write 7 with --username/--domain/--password -> ok, auth_user_id=1
read -> 7
write 99 with same auth -> ok
read -> 99 (restored)
Important behavior surfaced and documented in docs/usage.md
"Authentication" section: on a galaxy configured in permissive
(Free Access) mode, AuthenticateUser returns a non-zero userId
regardless of credentials — verified by intentionally passing a
wrong password and an unknown user, both of which still resolved
to userId=1 and the write went through. The CLI's auth path is
wired correctly; the galaxy just isn't strict. To exercise real
authentication, target a galaxy with galaxyAuthenticationMode and
attribute-level security above Free Access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WriteCommand now accepts multiple positional values when the tag
reference ends with '[]', bundling them into a strongly-typed array
(string[], int[], bool[], etc.) before passing to MxAccess.Write.
The CLR marshals the array to a COM SAFEARRAY of the matching
VARTYPE, which is the shape MxAccess expects for an array attribute.
Verified live on a 50-slot String[] (MESReceiver_001.MoveInPartNumbers):
write 50 distinct strings A1..A50 -> ok, MxCategoryOk
read [] -> ['A1','A2', ..., 'A50']
Plus a guardrail: passing multiple values without the '[]' suffix
exits 2 with a clear error so a typo can't accidentally write only
the first element of an indexed reference.
Critical finding documented in docs/usage.md: **a bulk write resizes
the array to the count provided.** Writing 25 values into a 50-slot
array leaves the array at 25 elements; the trailing 25 are
deallocated, not zero-filled. Verified by 50 -> 25 -> 50 round-trip
on the same attribute. Discover the runtime length via
'mxa read <attr>[]' or the configured length via grdb's
attributes.sql array_dimension column.
Type matrix in docs/usage.md updated:
- Bulk array via '[]' - read ✅ + write ✅
- Bare reference (no brackets) - read ❌ + write ❌
- Element via '[N]' - unchanged
ValueCoercion.cs: adds CoerceArray(IReadOnlyList<string>, typeHint)
that produces strongly-typed arrays. Default element type is inferred
from the first value when --type is unspecified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty square brackets — `<obj>.<arrayAttr>[]` — are the supported
MxAccess reference form for fetching a whole array as a single value.
Verified live:
mxa read 'MESReceiver_001.MoveInPartNumbers[]' --llm-json
-> ok=true, value=["", "11111", "", ..., "15", ...] (50 elements,
quality=192, MxCategoryOk)
The bare reference without brackets continues to return
MxCategoryCommunicationError, Detail=1003 — that's a real proxy-side
rejection, not a CLI bug. Documented separately so the matrix shows
both the working syntax and the failing one.
docs/usage.md:
- Type matrix gains a "bulk array read via []" row (✅) and a
"bare reference" row (❌, kept for reference).
- "Reading arrays" section rewritten around the two reference
shapes — `[]` for whole arrays, `[N]` for elements (1-based,
N <= NumElements).
- Adds a "Discovering array length" note pointing at grdb's
attributes.sql array_dimension column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Type support matrix' table to docs/usage.md capturing what was
verified against the live ZB galaxy on System Platform 2017 Express
with MxAccess 3.2.0.0:
Verified read + write (round-tripped):
- MxBoolean — DelmiaReceiver_001.TestAttribute (false→true→false)
- MxInteger — MESReceiver_001.MoveInBatchID (999→12345→0→999)
- MxString — MESReceiver_001.MoveOutErrorText (round-trip + restore)
- String array element via 1-based [N] indexing
— MESReceiver_001.MoveInPartNumbers[2] (11111→ARR-WRITE-OK→11111)
Verified read only (no writeable instance in this galaxy):
- MxFloat — DevPlatform.CPULoad{,Avg,Max,Min}, PageFaultsAvg
- MxTime — DevPlatform.SystemStartupTime → ISO-8601 string
- MxReferenceType — Container/Host/Area → target object's Tagname
Wired but no live instance found (matrix marked '❓'):
- MxDouble, MxElapsedTime, MxQualifiedEnum, MxInternationalizedString,
MxBigString. Adding a writeable UDA of any of these types should
exercise the existing code path without further changes.
Hard limitation surfaced and documented:
- MxAccess.AddItem('<obj>.<arrayAttr>') for bulk array read returns
MxCategoryCommunicationError, Detail=1003. Read array elements
individually via the 1-based '[N]' suffix; '[0]' is invalid.
Adds a 'Reading arrays' section pointing at grdb's attributes.sql
('array_dimension' column) for discovering element count.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites DiagCommand to match the structure of read/write/subscribe:
- Output goes through CliFx's IConsole instead of System.Console
(kills the 6 CliFx_SystemConsoleShouldBeAvoided build warnings).
- Single concise summary line at the top: `diag <tag> (<sec>s, <status>)`.
- Status now correctly distinguishes ok / error / no-events. A bogus
reference produces a DataChange event with MxCategoryConfigurationError;
that reports as "error", not the previous (incorrect) "ok".
- Adds --llm-json mode mirroring read/write: { query, ok, results: [...] }
with register/add_item/events/totals fields.
- Captures both DataChange and OperationComplete events with status
detail rendered consistently.
- Drops the duplicate "Done. Total events: N" / "diag result: events=N"
trailing lines.
Also fixes exit-code propagation from soft-failure commands. Environment
.ExitCode set inside ExecuteAsync was being overwritten by CliFx's own
return from RunAsync. Program.Main now folds the two:
return cliFxExit != 0 ? cliFxExit : Environment.ExitCode
This restores correct exit-on-failure behavior for both `diag` (error
status) and `write` (timeout / non-Ok ack) without resorting to
CommandException, which would print "ERROR\n<stack trace>" even for an
empty message.
Verified:
diag TestChildObject.TestInt -> EXIT 0, "ok"
diag NoSuchObject.NoSuchAttr -> EXIT 1, "error"
diag --llm-json -> structured envelope, ok flag honors statuses
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New tool wrapping ArchestrA.MxAccess.LMXProxyServerClass (the same COM
proxy aaObjectViewer / WindowViewer use) as a CliFx CLI for LLM-driven
debugging.
Commands:
- mxa info — loaded MxAccess assembly identity, supported value
types, MxStatusCategory enum.
- mxa read — fetch one or more tag values; subscribes briefly,
captures first OnDataChange per tag, tears down.
- mxa write — write a value with optional --type coercion; advises
first to resolve the attribute type, then waits for
OnWriteComplete with a per-call timeout.
- mxa subscribe — stream OnDataChange events for --seconds; JSON Lines
under --llm-json for piped agent consumption.
- mxa diag — minimal smoke test on a private STA thread; bypasses
the CliFx pipeline for diagnosing apartment / pump
issues.
Implementation notes documented in docs/api-notes.md (reverse-engineered
because AVEVA does not publish a single canonical MxAccess reference):
- Net48 / x86 / [STAThread] are non-negotiable. The CLI runs the entire
CliFx pipeline on a dedicated STA thread.
- COM events are dispatched as Win32 messages; AutoResetEvent.WaitOne
alone does not pump them on this configuration. MxSession.WaitForUpdate
loops Application.DoEvents() + drain + Sleep(20ms) instead.
- Write requires the target attribute's type to be resolved first.
WriteCommand advises and waits for the initial OnDataChange before
calling LMXProxyServerClass.Write to avoid ArgumentException
"Value does not fall within the expected range".
- Errors carry the full MXSTATUS_PROXY[] from MxAccess (Success,
Category, DetectedBy, Detail) so an agent can tell exactly which
layer rejected a request.
Verified against the live ZB galaxy with a writeable tag identified
via grdb (TestChildObject.TestInt, mx_attribute_category=10):
read: 99 (q=192, MxCategoryOk)
write 7: round-tripped — read returned 7 — written back to 99
write str: TestChildObject.TestString round-tripped a timestamp
subscribe: captured initial value plus subsequent change from a
separate process
The vendored ArchestrA.MxAccess.dll is gitignored — it is copied from
C:\Program Files (x86)\ArchestrA\Framework\Bin\ on any System Platform
install per the README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>