Repository navigation
Commit f2a04ba
authored
feat(opcua)!: read OPC UA for agents and MCP only beneath a seed's declared read roots (#2237)
* test(ci): publish the OPC UA live fixture on a Docker-picked port
The OPC UA read-only check published its fixture on 127.0.0.1:54840,
inside Linux's ephemeral range (32768-60999), where a client socket of
an earlier step, open or in TIME_WAIT, can hold the port and Docker
refuses the bind, as the live PostgreSQL fixtures did. The step now
publishes on 127.0.0.1::50000, reads the port back with docker port
and sets LIBREDB_OPCUA_LIVE_URL from it after the container runs, and
the compose service block publishes the same way. The pinned literal
was not load-bearing: the probes and both live check runs read the URL
from the environment, and the provider never dials the endpoint the
server advertises.
live-environment.test.ts pins the new form; the step run locally as
the workflow runs it passed on Bun and Node.
The backlog entry for the fixed port is removed.
* feat(seed): declare an OPC UA connection's read roots on its seed entry
An OPC UA seed connection may now carry agentReadRoots, the node ids
beneath which agent mode and MCP will read. Each root is a node id in
the console's form with its namespace written by URI, at most 32 roots
of at most 512 characters, and namespace 0 only from a fixed table of
standard nodes that describe the server's state and limits.
The grammar is checked when the seed file loads, through the engine's
validateSeed, with one refusal per element naming its index, and again
when the provider is built. The field is refused on every other engine
and in defaults, stripped from the managed connection list so the
browser never holds it, refused with 400 on a connection a caller
supplies inline, and framed into the provider cache key.
SEED_CONNECTIONS.md documents the field and its refusals, and
SECURITY.md records the control.
* feat(opcua): prove the set of nodes beneath an OPC UA connection's read roots
proveReadSet walks down from the read roots on a read's own session
and answers the set of nodes that read may answer. Each root is keyed
through the server's NamespaceArray and refused whole when its URI is
missing, listed twice, at index 0 or in the diagnostics namespace, or
when the server does not answer its facts Good. The walk follows only
Organizes, HasComponent, HasOrderedComponent and HasProperty forward,
keys every target by its table entry so namespace 0 and the
diagnostics namespace are never members, and prunes a node by its own
class and data type, a standard identity property by qualified name,
a withheld type definition and a credential name. Depth is a stop; the
node and request bounds refuse the read whole, as does a refused
continuation point. A namespace-0 root's members are its fixed rows.
The browse shaping keeps each reference's type definition, and Read
gains a second fixed attribute list, the four facts the walk reads.
The credential name check and the namespace-0 type definition and data
type tables come back as the prune's defence in depth.
* feat(opcua)!: read OPC UA agent requests only beneath the seed's read roots
Every OPC UA agent read now reads the server's NamespaceArray and
proves the set beneath the connection's read roots on its own session
before anything the request names is sent. An id outside that set,
namespace 0 and the server's configuration, roles and sessions among
them, answers exactly as an id the server does not hold, decided by a
set lookup with nothing sent for it: a value read answers its row
BadNodeIdUnknown with no value and no units, and a history read or a
browse of one is refused with the server's own sentence for that
status. A browse of a member answers the references the walk kept,
with no Browse sent and a count of those that lead outside; a browse
of the Objects folder lists the roots. Units come from the properties
the walk kept, with no Browse, and one outside the roots is answered
as not read. An in-set row is checked again on its own NodeClass and
DataType at answer time.
The withheld roots, the namespace-0 read list, the walk beneath them,
the placement by inverse Browse and the Withheld row status are
removed, and the agent's client no longer browses on the answer path.
A statement-sending agent profile on a connection without read roots
is refused at open with the new READ_ROOTS_REQUIRED deny code, and the
runs route refuses such a Start with read-roots-required before a run
exists, read from the new READ_ROOTS_REQUIRED record. Plan mode and
the operations workflow are unaffected. Deployed OPC UA seeds keep the
editor and lose agent runs until their operator adds agentReadRoots.
The provider doc, AGENT.md, the language pack's texts, the plan label
and the regenerated language surface golden follow.
* feat(mcp): read OPC UA through run_opcua_read, only beneath the seed's read roots
MCP_EXPOSABLE.opcua and MCP_STATEMENT_READ.opcua turn true, and the
opcuaread language registers its MCP facet, run_opcua_read, which runs
the agent's own request through queryReadOnly under the agent
read-only profile, so a call reads only beneath the seed's read roots
and answers every other node as one the server does not hold.
READ_ROOTS_REQUIRED holds MCP to the roots: the seed schema refuses
mcp: true without agentReadRoots at load, the MCP context refuses such
a connection at open with READ_ROOTS_REQUIRED, and a governed record,
which carries no roots, is never opted in. inspect_schema refuses an
OPC UA connection by name.
The tool description names the two facts an answer can reveal about
nodes outside the roots, a member's outward reference count and a
units property outside them. MCP.md, SEED_CONNECTIONS.md and the
provider doc's new MCP section document it, and the backlog entry that
kept MCP closed on OPC UA is removed.
* test(opcua): hold the read roots to a real server's identity, session and credential nodes
An in-process node-opcua server hangs, beneath a declared read root,
a DI device's Lock with its LockingClient and LockingUser, a
condition's ClientUserId, a session object of the standard type, a
node named for a credential and a reference out to the Server object,
and keeps a folder outside every root. Driven through the provider's
own queryReadOnly over the library, every one of them answers a value
read, a history read and a browse exactly as a missing id does, with
the same requests sent and no planted value in any answer, the folder
outside the roots is named by nothing a read sends, and a browse of
the root lists the process value and counts the rest without naming
them, sending nothing beyond the walk.
* docs(backlog): narrow the inline seed-only refusal and file the read roots follow-ups
D380 now says that an inline connection carrying OPC UA's read roots
is refused and the other seed-only fields still are not, and D414 says
how the read roots walk keys a target on another server until the id
is written in the Part 6 spelling.
B170 records that OPC UA proves its read roots on every read, with the
walk measured on the pinned fixture, until a fixture shows a cache is
worth its invalidation. B171 records that an operator cannot exclude
one node beneath a read root. Both are cited in AGENT.md.
* fix(opcua): split a server-written name into words in linear time
The credential-name check runs on every name a server answers during the read roots walk, and its split of a run of capitals backtracked, so one long name could hold the event loop for seconds. The split now looks ahead one capital and a lower-case letter, which splits the same names in linear time.
* refactor(agent): word the read roots refusal without naming an engine
The runs route answers the refusal for every engine READ_ROOTS_REQUIRED names, so it takes an engine-neutral sentence from the registry beside that record instead of importing the OPC UA provider's, and the timeline and the snapshot advice drop the engine name too.
* docs(mcp): name BrowseNext among the services run_opcua_read sends
* fix(opcua): send a history read only for a member the walk classed as a Variable
The walk checks the data type of a Variable alone, and a node can class itself as an Object while it keeps history, so a history read of any other member now answers as an id outside the read roots with nothing sent. The answer-time check also answers a Variable whose own Read returns no data type as outside, since the walk admitted it on one.
The tests replay a units property the walk pruned by its own facts, a refused NodeClass and DataType, a NodeClass attribute that disagrees with the browse, a data type that changes between the walk and the answer and a clean root on the in-process server, the walk's deadline on Browse and BrowseNext, every spelling of the Objects folder, and the walk's request bound at exactly its size and one past it.
* test(opcua): re-record the live agent and MCP captures beneath read roots
The agent capture script read the fixture with no read roots, which a statement profile now refuses at open, so it could no longer run, and its capture described the earlier withheld design. Both live scripts now read through seeds whose roots are the Telemetry and DataAccess folders and Server.ServerStatus, on containers whose ports Docker picks, as the anonymous, a non-privileged and an administrator identity.
The agent capture records the walk's request count and time, values with their units, history and browses, every id outside the roots beside a missing twin for each read kind with the requests each sent, a session object the administrator is shown, the walk over wider and narrower roots, and that a profile without roots opens no session. The MCP capture records the same through the tool's handler, the refused forms, the write control and the cancellation timings. Two unit tests hold the docs' claims to the captures, and opcua.md, the MCP cancellation row and the walk's backlog entry cite them.
* fix(opcua): prune a node whose BrowseName is not answered, or whose valueless class answers a DataType
The read roots walk matched the standard identity properties only when the server answered a node's
BrowseName, so a server that refused that one attribute had a locking object or a condition's
ClientUserId admitted with its value. A node whose BrowseName is not answered is now pruned with its
subtree, since the name is the only thing that tells an identity property apart.
A node whose own NodeClass attribute claims a class that bears no value was admitted without a data
type check, so its subtree was walked and answered even when the node carried a withheld data type.
None of those classes has a DataType, so a node of one that answers a DataType all the same is now
pruned with its subtree.
The in-process vendor server test holds both shapes beneath a declared root to the answer of a missing
id, and the scripted walk test holds a root with a refused BrowseName to the whole read's refusal.
* fix(opcua): hand a browse continuation point to the library as a Buffer
The library's browseNext takes a continuation point only as a Buffer and throws on the Uint8Array the
provider carries, so every continued or released browse failed before anything was sent. On a server
that pages a Browse, the Keys panel could not open the folder, and an agent read whose roots the
server paged was refused as a failed connection instead of being proved. The bridge now hands each
point back as a Buffer, as the history read already does with its own point.
The in-process vendor server lists one folder two references at a time: the Keys panel and the read
roots walk both follow every point to its end, and a point the server refuses stops the agent read
whole with the server's status.
* fix(opcua): stop naming a type among the reasons a read root is refused
A root's type definition is never read, since only a parent's browse names one and a root's parent is
never browsed, so the type definition prune never applies to a root. The refusal of a pruned root now
names only what can refuse it (its class, data type or name), and the provider doc states that the
operator vouches for what kind of node each root is, with the session object case as a residual.
* fix(opcua): bound a browsed level's continued pages and hand back the point a bound stops
Now that a continued browse reaches the server, a server that answers every BrowseNext with another
continuation point kept the Keys panel's level browse looping, gathering every reference. A level now
stops after OPCUA_BROWSE_LEVEL_MAX_CONTINUATIONS continued pages, or once it holds more than
OPCUA_BROWSE_LEVEL_MAX_REFERENCES references with a point still to follow; the point is released and
the level is refused rather than shown in part.
The read roots walk was already bounded by its request count and the read's time budget, but a
BrowseNext either bound stopped left the server's point held until the session closed. The walk now
releases it before refusing, uncounted.
The whole-tree walk module had no caller outside its own tests, so it is removed with them instead of
being bounded, and the provider doc says the editor walks nothing.
* refactor(opcua): name the Devices companion namespace once, written from the Foundation's own URI1 parent 8ae87a0 commit f2a04ba
98 files changed
Lines changed: 7858 additions & 3657 deletions
File tree
- .github/workflows
- docker/opcua
- docs
- providers
- scripts
- src
- app/api
- agent/runs
- connections/managed
- components/agent
- hooks
- lib
- agent
- languages
- opcuaread
- db
- connection-string
- providers/industrial/opcua
- governance
- mcp
- seed
- storage
- tests
- api
- agent
- seed
- fixtures
- agent/language-surface
- opcua
- 2.15.10
- agent-boundary
- mcp-boundary
- hooks
- integration
- db
- mcp
- isolated
- live
- unit
- components
- db
- connection-string
- opcua
- lib
- agent
- db
- governance
- storage
- mcp
- seed
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
618 | 618 | | |
619 | 619 | | |
620 | 620 | | |
621 | | - | |
622 | 621 | | |
623 | 622 | | |
624 | 623 | | |
625 | | - | |
626 | | - | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
627 | 628 | | |
| 629 | + | |
| 630 | + | |
628 | 631 | | |
629 | 632 | | |
630 | 633 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
32 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2440 | 2440 | | |
2441 | 2441 | | |
2442 | 2442 | | |
| 2443 | + | |
2443 | 2444 | | |
2444 | 2445 | | |
2445 | 2446 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| |||
3777 | 3777 | | |
3778 | 3778 | | |
3779 | 3779 | | |
| 3780 | + | |
3780 | 3781 | | |
3781 | 3782 | | |
3782 | 3783 | | |
| |||
4119 | 4120 | | |
4120 | 4121 | | |
4121 | 4122 | | |
4122 | | - | |
| 4123 | + | |
| 4124 | + | |
4123 | 4125 | | |
4124 | 4126 | | |
4125 | 4127 | | |
| |||
4297 | 4299 | | |
4298 | 4300 | | |
4299 | 4301 | | |
4300 | | - | |
4301 | | - | |
4302 | | - | |
4303 | | - | |
4304 | | - | |
4305 | | - | |
4306 | | - | |
4307 | | - | |
4308 | 4302 | | |
4309 | 4303 | | |
4310 | 4304 | | |
| |||
9312 | 9306 | | |
9313 | 9307 | | |
9314 | 9308 | | |
9315 | | - | |
9316 | | - | |
9317 | | - | |
9318 | | - | |
9319 | | - | |
9320 | | - | |
9321 | | - | |
9322 | | - | |
9323 | | - | |
9324 | | - | |
9325 | | - | |
9326 | | - | |
9327 | | - | |
9328 | | - | |
9329 | | - | |
9330 | | - | |
9331 | | - | |
9332 | | - | |
9333 | | - | |
9334 | | - | |
9335 | 9309 | | |
9336 | 9310 | | |
9337 | 9311 | | |
| |||
9370 | 9344 | | |
9371 | 9345 | | |
9372 | 9346 | | |
| 9347 | + | |
| 9348 | + | |
| 9349 | + | |
| 9350 | + | |
| 9351 | + | |
| 9352 | + | |
| 9353 | + | |
| 9354 | + | |
| 9355 | + | |
| 9356 | + | |
| 9357 | + | |
| 9358 | + | |
| 9359 | + | |
| 9360 | + | |
| 9361 | + | |
| 9362 | + | |
| 9363 | + | |
| 9364 | + | |
9373 | 9365 | | |
9374 | 9366 | | |
9375 | 9367 | | |
| |||
0 commit comments