External Systems New in 1.0.0
A Named Endpoint lets the assistant look something up in a system QUBIQ does not own — an ERP, a CMMS, a maintenance API — so "is there stock for that seal?" can be answered.
The whole design turns on three properties. They are what make this safe enough to exist:
- The model never names a destination. It selects an operation by name. The host comes from an endpoint you authored, and the path from a template you wrote.
- The model never composes the payload. Parameters are declared, typed and pattern-constrained by you. Anything undeclared is refused rather than passed through.
- The reachable set is your artifact, not a model's choice.
Because of those three, this is not treated as the assistant "having internet access". It is a fixed, admin-authored menu.
Configuring one
Settings → AI Assistant → External Access.
The card carries one master switch — Let the assistant call external systems — separate from per-endpoint access, so an incident can be stopped in one click without losing how anything was configured. It is off by default, and while it is off no outbound call is made whatever is configured below it.
The system
| Field | Notes |
|---|---|
| Name | How it is referred to. |
| Base URL | The host and root path. A private address is fine — see below. |
| What this system is | For your team. Never sent to the model. |
| Authentication | none, bearer, basic, apiKey (in a header or the query string), or a custom header. |
| Let the assistant use this | Off while you set it up. Test it first — you can, without opening it. |
The credential is write-only from the interface's point of view: the form shows the scheme and whether a secret is set, and there is no route anywhere that returns the secret itself. Leaving the field blank on edit keeps what is stored.
Authorization and Host may not be set through the custom-header scheme. The first because there
is already a scheme for it and two competing values would be resolved by whichever ran last; the
second for a sharper reason — the transport sets Host from the resolved address to defeat DNS
rebinding.
The operations
An endpoint is useless until it has operations. Each one is a single named call.
| Field | Notes |
|---|---|
| Name | The only identifier that crosses to the model. Lower-case letters, digits and underscores, 3–64 characters. |
| Title | Your label. Never sent to the model. |
| Description | Is sent to the model, and is the entire basis on which it picks this operation over another. |
| Method | GET. Nothing else. |
| Path | A template hung off the base URL, with {param} placeholders. |
| Params | What the model may fill. |
Write the description as if it were the only documentation — because to the model, it is. "ERP integration" tells it nothing; say what comes back and when to use it.
PUT/PATCH/DELETE are obviously writes. POST is the one worth explaining: a POST to an ERP can
create a purchase order, so "read-only" is a claim about the remote system and not just about
QUBIQ. And the useful kind of POST carries a query body — which the model would have to compose,
defeating property 2 above.
Parameters
| Field | Notes |
|---|---|
| Name | The {placeholder} it fills, or the query-string key. |
| In | path or query. A path parameter must be required — an absent value would leave a hole in the URL. |
| Pattern | An anchored regular expression the value must match. |
| Description | Shown to the model, so it knows what to put here. |
| Means | Links the value to something real — a plant-vocabulary kind (equipment, area, …) or a temporal role. |
Write a real pattern. It is your only actual control over what a model-supplied string can be.
Containment (below) refuses the characters that would let a value escape its slot; a pattern like
^[A-Z0-9-]{3,20}$ also refuses the ten thousand strings that are merely wrong. Leaving it empty is
legal and lazy.
Means is advisory, unlike the pattern: it says what the value means, not what it may contain. Setting it lets the assistant resolve what the operator called something into what the system calls it, exactly as a saved query's parameter does.
The editor refuses a definition that cannot be coherent: a {slot} with no declared parameter, a
path parameter with no slot, a query string typed into the path, a duplicate name, an invalid
pattern, a means that is not a real vocabulary kind.
Containment — the rule that keeps a value in its slot
A template like /inventory/{sku}/stock looks scoped. It is not, unless the value substituted
into {sku} can never introduce structure of its own. These characters are refused outright in a
parameter value:
| Because | |
|---|---|
/ | A new path segment — /inventory/../../admin/users/stock |
? | A query string you never authored |
# | A fragment, which truncates everything after it |
@ | A userinfo section, which can relocate the host entirely: https://erp.internal/x/{sku} with sku=@evil.example is a request to evil.example |
\ | A separator some parsers fold to / |
% | Percent-encoding, which can re-introduce any of the above after a decode |
.. | Traversal |
The last two are the ones that get missed. The value is also percent-encoded on substitution — both, because encoding alone has been defeated by double-decoding proxies more than once.
Reaching something on the plant network
The usual advice for outbound HTTP is "refuse private ranges". That is exactly wrong here: the whole
point is to reach an ERP at 192.168.10.40, or erp.internal on a plant LAN. The legitimate
targets are the private ranges.
So the defence is built from what remains:
- Resolve, then pin. The host is resolved once, the decision is made on the resolved IP, and that IP is dialled. Resolving twice — once to check, once to connect — is a DNS-rebinding hole.
- Redirects are never followed. A
30xelsewhere is the cheapest bypass there is. - An always-deny set no configuration can widen: loopback,
0.0.0.0/8, link-local169.254.0.0/16(which is where cloud metadata hands out instance credentials), multicast, and the IPv6 equivalents. An endpoint pointed at QUBIQ's own loopback interface would be a confused-deputy path around every authorization check in the system. - Caps on the response: 256 KiB, hard-truncated with a note rather than silently; 10 seconds by
default and 60 at most; and only
application/json,text/plain,text/csv,text/html,text/xmlandapplication/xmlare accepted.
Testing
Test runs the operation with values you supply and shows what came back — including the resolved IP, which nothing the model sees ever includes. You can test an endpoint before opening it to the assistant, which is the intended order.
Using one
| From | How |
|---|---|
| The assistant | Plug the External Systems skill, and put the endpoint in the instance's endpoint fence. |
| A playbook | A Call external system step, naming the operation. → Playbooks |
| A playbook check | An External system input, so a figure is computed from the response. |
| An AI node | The endpoints fence, one endpoint at a time. |
The endpoint fence is fail-closed and has no * on a pipeline node: a node's prompt is a vendor
webhook or an email body nobody read, so text inside it can steer which system gets called. Give a
node the one endpoint it needs.
Gotchas
- The master switch and the per-endpoint switch are different things. An endpoint marked Let the assistant use this still makes no call while the capability is off.
- An operation the model cannot tell apart from another will be picked wrongly. Two operations described as "look up a part" is a coin flip; describe what each returns.
- A response over 256 KiB is truncated and says so. If an operation routinely returns more, it is the wrong operation — add a parameter that narrows it.
- The description reaches the model; the title and the system description do not. Putting the useful sentence in the wrong field is the most common reason an operation is never selected.