Drivers
Catalog, under Components: Drivers (/drivers, with driver:read, covered by every viewer’s *:read floor)
is the directory of drivers: the normalized menu that turns a device family’s API into
what to fetch, how to parse it, and what actuates,
on the same flat-registry pattern as Location Types and Tags.
Where a vendor names who a device comes from, a driver names how it is
talked to (for example Generic SNMP or Newtron NVP). Each row shows the name (the
operator-facing name, for example snmp-generic), the label, an optional
version, and its origin (official or custom). A driver also carries
an id, a uuid minted by the database, the internal address the handle resolves to
(ADR-0062); the handle is what you type and read.
A driver’s body is its spec (ADR-0135):
a versioned declarative document (data, not code) naming the transport it rides, the inputs
an attach must supply (host, port, credentials as secret references), and its three function
families (poll functions, listeners, command bindings). The detail blade renders the spec as
the menu it is: what each poll asks and what lands, what each listener waits for, what each
command binding actuates. On a line protocol a listener’s arm commands (the subscribe
conversation) are sent on every connection the node holds, its match rule claims the
component’s pushes, and each claimed line lands as a sample with no poll involved (a listener rides a
held line session or a udp socket, and is refused at write on any other transport, where nothing
would run it); a request may
declare its reply shape (request.reply, a prefix or regex) so its answer is picked out of
those pushes, plus an optional error shape (reply.error) for the device’s refusals, which then
answer the request as a failure carrying the device’s text instead of timing out. Every regex in
a spec is Go RE2 with substring semantics: INPUT \S+ also matches EVT INPUT hdmi2, so anchor
a pattern meant to match a whole line (^INPUT \S+$). unmatched: collection-failed makes a line
nobody claims a collection fault rather than a counted discard (routing).
An emit names a collected key. A key only the node writes (every native probe key: the
reachability verdicts and probe timings, tls-certificate, ssh-host-key, endpoint-rejection and
its endpoint-rejection-reason, a held session’s session-state and session-reason, and
collection-failed), health,
command-issued, or a key a calc rule outputs is refused at the spec’s write, naming the key, since
each has its own writer (who writes a key):
the node would otherwise take a driver’s value as its own observation, the pin or the gate. A spec
stored before that refusal is held to it where it acts: the worklist strips such an emit from the
tasks a node receives (the rest still run), a re-derivation never bakes one, and the server names the
spec and its tasks in a warning at boot so you can mend it.
A spec may declare a session block, the auth step the held session runs before anything rides
it (auth and rejection). session.login is a login
dialog: its steps each wait for a prompt (expect, a prefix or regex that may match a prompt
with no line ending, such as login: ) and send a line, which may name a secret input’s field
(${secret.login.password}, the input declared kind: secret), and its ready match says the
login passed. session.terminal lists the replies retrying cannot fix, each a match with a
class (auth, host-key, certificate, protocol): a line matching one, during the login or
answering any request, moves the session to rejected, where the node attempts nothing more until
the endpoint’s connection fingerprint changes (its secret is rotated, its address, port or transport
changes, the driver’s login declaration changes) or an operator retries it (Retry on the
rejected session’s notice in the console, or omniglass endpoint retry <component>/<endpoint>, with
endpoint:retry); a label edit changes nothing. session.reprobe
(at least 5m, default never) lets a rejected session try once more on its own. A wrong password the
component answers with a declared refusal then costs one attempt rather than a retry every 30 seconds;
a component that hangs up on a wrong password without saying so costs three (the login budget), and
then reads rejected as auth. Anchor a prompt’s regex (^login: ?$): a prompt is matched on a line
the component pauses on, and an unanchored one also matches a banner such as Last login: . A spec
whose session only commands would ride (no poll or listener to hold it) refuses at write. Over ssh
the session’s auth step is the transport’s own: session.credential names the secret input it
authenticates with (required: a basic-auth username and password, or an ssh-key username and
private key; a session.login beside it only answers keyboard-interactive questions, and a question
nothing declared answers, such as a one-time code, rejects the session), session.pty asks for a
terminal and session.exec runs a command as the channel instead of a shell. The host key is trusted
on first use and kept as the endpoint’s ssh-host-key; when it changes the session reads rejected
naming both keys, which the gate carries as data (endpoint-rejection ends with the pinned and the
offered key). The console’s Trust this key on the session’s notice trusts the offered one after
a confirm (held sessions), and so does
omniglass endpoint retry <component>/ssh --host-key <the offered key>. While the refusal holds, the key must be
the offered one it names (any other is refused, 409, and changes nothing; retry without a key first
to have the node read a key the component presents since); with no refusal held, --host-key
declares a pin ahead of time, for a replacement whose key you already know.
A driver on tls speaks the line protocol over a TLS connection, its handshake the transport rung
(tls). How the handshake judges the certificate is the driver’s
tls-mode input, a string input the engine reads by name, like host and port: verify (the
certificate chains to one of the node’s system roots and names the host), pin (the first public
key offered is trusted, any other refused) or skip (nothing is checked). The driver declares its
default, as the official newtron-nvp-tls declares pin for gear that signs its own certificate; a
driver that declares no tls-mode input verifies; and a spec that defaults it to skip refuses at
write, since skipping verification is only ever an operator’s choice. An operator edits the mode in
place on the endpoint like any input, which moves its connection fingerprint, so a session a
certificate refused tries again under the new mode with no retry. A refused certificate reads
rejected with class certificate and its reason; for a pinned key that changed, the reason names
both keys and omniglass endpoint retry <component>/tls --certificate <the offered key> trusts the new one.
transport: tlsinputs: - { name: host, kind: string, required: true } - { name: port, kind: number, default: "51326" } - { name: tls-mode, kind: string, default: pin }A spec may also declare its reachability ladder (ladder), bottom up: ladder.ping.enabled
(whether to ping at all; a ping is informational only and never gates), ladder.transport.rung (the
gate: connect on the transport’s port, the protocol’s handshake, or a named request answered,
given as ladder.transport.request, where an open port cannot be detected; it runs as a line over
tcp, a datagram over udp, or a get over snmp) and
ladder.session.enabled (whether a login rung exists). Every field defaults from the transport
registry, so most drivers declare nothing, and a rung the transport cannot run refuses at write. A
cloud API is never pinged, but its 443 is checked open before its session runs:
transport: httpladder: ping: { enabled: false } transport: { rung: connect }A refusal at a rung gates that rung and the ones above it for the endpoint; the rungs below keep reporting (the gate), except an snmp endpoint’s get, which carries the community, so beneath its auth gate it is not sent and reads down, rejected.
A udp driver speaks the line protocol as datagrams (udp). An open
udp port cannot be detected, so it must name its request rung, and it may declare a listen-port
input, the local port its endpoint binds, which its requests leave from and the component’s pushes
arrive on (unset, the node picks one, which suits a component that answers to a request’s source).
Every udp request declares its reply’s shape (reply), since a datagram’s answer is told from a
late reply to another ask only by it. A request may declare retries (udp only, 0 to 5), how many
times a lost datagram is re-sent within
the endpoint’s timeout: a poll and the request rung default to 2, and a command to none, so
declare them only on a command that is safe to send twice (one that names its value, never a
relative step):
transport: udpinputs: - { name: host, kind: string, required: true } - { name: port, kind: number, default: "51327" } - { name: listen-port, kind: number }ladder: transport: request: { line: "GET INPUT", reply: { prefix: "INPUT " } }commands: - command_type: set-input request: { line: "SET INPUT ${value}", reply: { prefix: "INPUT " }, retries: 2 }Over udp a session block may declare only terminal replies: nothing logs in or is held, and a
reply it names gates the endpoint at the transport rung.
An snmp driver’s rung is a get of a standard OID, sysUpTime.0 unless it names another under
ladder.transport.request.get (numeric OIDs only; a line is refused), sent with the endpoint’s
community secret. A v2c agent drops a wrong community silently, so it reads not answered and
retryable; an agent answering authorizationError reads rejected, class auth:
transport: snmpladder: transport: request: { get: ["1.3.6.1.2.1.1.2.0"] }An http or https driver sends real requests (http and https).
A request declares its method (GET unless it says otherwise), path, query, headers, a body
(a JSON value, encoded by its body_format: json, form, xml or text) and the answer’s
format (json, xml, html or text, declared rather than read off the Content-Type, which gear
mislabels). An emit locates its value with one selector the format admits: a jsonpath on a json
answer, an xpath on an xml answer (its prefixes declared on the request’s namespaces) or on an html
page, a regex on any. A selector must find exactly one value: none, or more than one node, is a
fault naming the emit, and the rest still land. A command’s request renders ${value} and
${arg.<key>} where you place them, escaped for where they land (a path segment, a query value, a
JSON or xml body), and a JSON body leaf that is exactly ${value} keeps the value’s type, so an
intended volume is sent as a number. A request never names a secret and never sets Authorization
or Cookie: the credential is the session block’s.
transport: httpsinputs: - { name: host, kind: string, required: true } - { name: port, kind: number, default: "443" } - { name: tls-mode, kind: string, default: pin } - { name: login, kind: secret, secret_type: basic-auth, required: true }session: credential: login scheme: basicpolls: - name: audio schedule: { every: 30s } request: { path: /api/audio } emits: - { name: volume, extract: { jsonpath: volume } } - name: firmware schedule: { every: 1h } request: { path: /api/status.xml, format: xml, namespaces: { d: "urn:polaris:dsp:1", sys: "urn:polaris:system:1" } } emits: - { name: firmware-version, extract: { xpath: "/d:status/sys:device/sys:firmware" } }commands: - command_type: set-volume request: { method: POST, path: /api/audio/volume, body: { volume: "${value}" } } settle: { from: response, success: { regex: '"ok":\s*true' } }session.scheme says how the credential session.credential names rides every request: basic (a
basic-auth secret), bearer (a bearer-token), api-key (an api-key, placed by
session.api_key: { in: header, name: X-Census-Key } or { in: query, name: apikey }), or digest
(a basic-auth secret answered as HTTP digest, MD5 or SHA-256: the node holds the component’s
challenge, answers a stale nonce afresh, and never sends the password). A 401 to a
credentialed request reads rejected, class auth, and nothing credentialed is sent again until the
endpoint’s connection fingerprint changes; a session.terminal rule may name a status (a gear’s
403 for a wrong credential) or a body match. Any other 4xx faults that request alone and waits its
interval; a 408, 429 or 5xx retries on the backoff, no sooner than a Retry-After; a redirect is
not followed. The reachability probe never sends the credential, so an http endpoint’s
endpoint-authenticated is proven by its polls; a driver whose commands carry a credential needs a
poll to carry it to the node. Over https the tls-mode input and the pinned key work as over tls
(above), and omniglass endpoint retry <component>/https --certificate <the offered key> trusts a changed key.
Gear whose web API signs in once declares session.login instead of a scheme
(logging in over http): the login dialog a telnet
driver declares, its steps requests. A step names only the secret fields it sends
(${secret.login.password}); the node keeps the session it opens (the component’s cookies, the
values capture locates in the last answer, the headers that send them) in its memory alone, per
endpoint, and logs in again only when a request draws a 401, a redirect to the declared login
path, or the declared expired text, then replays that request once. A form login:
session: login: path: /login steps: - request: method: POST path: /login body_format: form body: { username: "${secret.login.username}", password: "${secret.login.password}" }A token login captures the token and sends it as a bearer; expiry renews it before it lapses:
session: login: steps: - request: { method: POST, path: /api/login, body: { username: "${secret.login.username}", password: "${secret.login.password}" } } capture: [{ name: token, extract: { jsonpath: token } }] expiry: { jsonpath: expires_in } headers: { Authorization: "Bearer ${session.token}" } expired: { regex: "token expired" }A login the component refuses (a 401, a redirect back to the login page, another 4xx, or a last
answer without its ready text or a value it captures) is tried once: the endpoint reads
rejected, class auth, with the component’s reason, and logs in again only when the fingerprint
moves (fix the secret in place with omniglass secret update, or edit the endpoint’s inputs). A
login answered with a 5xx or a dropped connection is retried, but the third in a row reads
rejected too. A session refused right after a fresh login reads rejected, never a second login.
expiry is seconds, held between 10 seconds and 30 days; zero or a missing value means none. A
login declares no scheme beside it, its step paths open with a literal /, and a header it sends
names only a capture, never a secret, Cookie or a header a poll or command sets.
A component whose web page updates live from a server-sent events stream declares the stream in its
session block, and listeners claim its events (a server-sent events
stream). The stream is a GET (a path, a
query, headers other than Accept and Last-Event-ID, which the node sends), read as json
(the default) or text, and it rides the session’s scheme or login like every request. A listener
over http declares no arm (a stream has no upstream); its match may name the event (event: change), and carry one data rule: a prefix, a regex, or a JSON field ({ path: path, equals: status.audio.volume }, or a regex in place of equals):
session: credential: login scheme: basic stream: { path: /api/events }listeners: - name: volume-change match: { event: change, field: { path: path, equals: status.audio.volume } } emits: - { name: volume, extract: { jsonpath: value } }The node holds the stream as the endpoint’s session: it reads up, recovering after a drop (it
resumes from the last event id after the server’s retry, landing nothing twice), and rejected
when the component refuses its credential, which also gates the endpoint’s polls and commands. A
stream needs a listener (with none, nothing holds it), and a listener over http needs the stream.
A telnet driver:
session: login: steps: - { expect: { regex: "^login: ?$" }, send: "${secret.login.username}" } - { expect: { regex: "^Password: ?$" }, send: "${secret.login.password}" } ready: { prefix: "Newtron Panel 7 NVP ready" } terminal: - { class: auth, match: { prefix: "auth rejected" } } reprobe: 1hA poll’s schedule.every is how often the node asks the device, at the node’s heartbeat
granularity (a 60s poll on a 30s heartbeat runs every other tick). A poll whose fetch fails
(the device rebooting, say) retries sooner, on a backoff that starts at the heartbeat and doubles
up to the interval, so a daily poll is not lost for a day. A listener’s pushes land as they arrive
whatever the schedule. Changing a spec (or its version) reaches the endpoints already attached through it: their tasks
re-derive in the update’s own transaction, under its one audit row, and the next poll runs the
new spec; an official spec a new release ships reaches them at boot the same way.
A row without a spec is a stub and cannot be attached; specs are
authored over the API or CLI (omniglass driver create --spec ...) and validate at write,
so a spec that references an unregistered sample name, an unknown secret shape, or a broken
template refuses with the fault named (422). Attaching a driver to a component happens on the
component’s own detail (nodes and reachability): the spec
derives the endpoint and its tasks. Its inputs (the host, the port, an attached secret’s name) then
edit in place on the endpoint’s blade or with omniglass endpoint update <component>/<endpoint> --inputs ..., checked
against the spec’s declared inputs; no re-attach. Clearing an optional input restores its default;
a required one cannot be cleared.
A driver is consumed by the product catalog: a product references
its driver through an optional driver_id to say which driver reads it, chosen from a driver
picker on the product’s create and edit forms, and three of the shipped official products bind a
driver this way. It is a leaf catalog beside the vendor registry. See
core entities for where it sits in the fleet model.
- New driver (with
driver:create, an admin permission) opens a create drawer: give it a name (unique tenant-wide, e.g.snmp-generic), a label, and, optionally, a version. - Pick a row to open its detail blade. The footer Edit pencil (with
driver:update) edits the label and version; the name is fixed, since a catalog row carries no rename (:renameis a component, system, location, and principal group affordance). Delete (withdriver:delete) removes the row, behind a confirm. A verb you lack greys just that button, its hover reason naming the permission (Requires driver:update,Requires driver:delete); the pair never disappears. - An official row is always read-only: the blade keeps the Edit and Delete pair in place, greyed, with the reason on hover: “Official: ships with Omniglass and updates with it.” Omniglass ships a starter set of official drivers (Generic SNMP, Newtron NVP, and Newtron NVP over Telnet, its login a session block, over SSH, and over TLS, pinning the panel’s key, the Kestrel Device API over https with basic auth, HTTP JSON over http with a header key, Newtron Panel Web over https behind its form login, and Boreal Edge Web Admin over http with digest, all with real specs), upserted idempotently at boot so the shared set cannot drift install to install; add a custom driver for anything else.
- Delete refuses while an endpoint is attached through the driver (422): an attach
derives real collection work from the spec, so the driver cannot vanish out from under it.
The product link stays soft:
product.driver_idison delete set null, so deleting an unattached driver detaches it from those products (their driver clears) rather than blocking. Official rows stay refused outright (422).
Minting a driver is admin-gated; the picker that consumes it lives on the
product create and edit forms. The same operations are omniglass driver list/get/create/update/delete from the CLI (see the CLI reference).