The CLI
The omniglass binary is both the server and the client. Its data commands are
generated from the OpenAPI (make gen, via cmd/cligen), so the CLI cannot drift
from the API: a new route is a new command on the next regeneration. A small set of
commands (the run modes and the trusted bootstrap) are hand-written and compose with the
generated tree on the same root.
This page is the mental model and the setup. For the exhaustive, generated list of every command, its flags, and an example, see the CLI reference.
Running the full stack locally
Section titled “Running the full stack locally”make dev brings up the whole stack for a browser session: a dev Postgres (docker
compose, matching the default DSN), the migrations and boot seed, a bootstrapped dev
owner whose token is printed once, a dev example fleet (a few campuses with rooms, a few
sign-in-able users, and their grants, so the console is not empty), and the server with
the operator console at http://localhost:8080/web. Ctrl-C stops the server; make down stops Postgres (the
named volume persists data between runs; docker compose down -v wipes it and re-mints a
token next run). make up / make down manage just the database. Tests never touch this
stack: they spin their own ephemeral Postgres via testcontainers.
The example fleet deliberately types almost none of its own names: its devices and its
systems all ask the platform to name them, so what you sign in to is the name generator’s
own output (display-1 in three different rooms, boardroom and boardroom-2 for the
two halves of one) rather than a list somebody wrote out. The rows that do carry a typed
name are the ones no rule could produce: every LOCATION, since no shipped location type
carries a name rule (a campus, a building, a floor and a room each have a name you hold
and the platform does not).
A server on ports it picks
Section titled “A server on ports it picks”omniglass server serves the API on OMNIGLASS_ADDR (:8080 by default) and hosts the
embedded bus nodes connect to on OMNIGLASS_NATS_ADDR (127.0.0.1:4222). Either address
may name port 0, which binds whatever free port the kernel hands out, so a second server on
the same host (a test run, another worktree’s stack) never collides with the first. To learn
the ports it got, set OMNIGLASS_READY_FILE to a path: once both listeners are bound, the
server writes the addresses there as one JSON object, written whole or not at all. A file
already at that path (one a killed server left) is cleared first thing at boot, before
anything that can fail, and the server removes its own at a graceful shutdown (SIGINT or
SIGTERM), so a file you find there names a live server’s ports.
{"addr":"127.0.0.1:41234","nats_addr":"127.0.0.1:41236","nats_url":"nats://127.0.0.1:41236"}nats_url is the bus URL the node-claim exchange hands each node: OMNIGLASS_NATS_URL when
that is set, otherwise the address the bus bound (read as loopback when it bound a wildcard
host such as 0.0.0.0), so a bus on port 0 tells nodes the port it got. Read the ports from
the file rather than finding a free port and passing it in: a port that is free when you look
can be taken by another process before the server binds it. The end-to-end tests start every
server this way (test-driven).
Connecting
Section titled “Connecting”Every generated command is a client of a running server and takes two shared flags, each with an environment default:
| Flag | Env | Default |
|---|---|---|
--server | OMNIGLASS_SERVER | http://localhost:8080 |
--token | OMNIGLASS_TOKEN | (none) |
The token is a bearer credential (mint the first one with omniglass bootstrap; see
Authentication below). The server enforces the same capability and scope
for the CLI as for any caller: the CLI is just another client, with no privileged path.
export OMNIGLASS_SERVER=https://omniglass.example.comexport OMNIGLASS_TOKEN=ogp_...omniglass location listomniglass location create --name hq --location-type campusomniglass location get hqomniglass location rename hq --name hq-west # needs location:rename, not location:updateOutput is JSON. A non-2xx response prints the server’s error body and exits non-zero, so the CLI is safe in scripts.
A flag carries the schema’s own type
Section titled “A flag carries the schema’s own type”The commands are generated from the API, so a request field’s type is a fact the
spec already states and the flag carries it
(#711). An integer field is an integer flag
and --settle-window-seconds soon is refused by the shell, before a request is issued, rather
than by the server’s 422 after a round trip. A boolean field is a bool flag, which is the one shape
with a spelling to learn: write --propagates=false, since a bare --propagates means true and
--propagates false reads false as a positional argument.
A field with no scalar type of its own (an object, an array, or a free-shape value) takes one
string flag parsed as JSON, which is a deliberate choice rather than a leftover. A nested value has
no shell-native flag type, and the alternatives (repeated key=value pairs, or a flag per leaf) can
express neither an array member nor null, which is the value that clears a field named in an
update_mask (ADR-0106):
omniglass location-type create --name wing --label Wing \ --name-rule '{"stem":"wing","bare_first":true}' # an object, as JSONomniglass location-type update wing --update-mask name_rule --name-rule null # clears itomniglass tag create --name rack-position --applies-to '["location"]' # an array, as JSONA bare word that is not valid JSON falls back to itself, so the common case needs no quoting
(--value HDMI1); a string that would parse as JSON is quoted to force a string (--value '"30"').
A name is renameable, and a rename is its own command rather than a flag on update: component,
system, location, and principal-group each carry one, gated by <resource>:rename, which is
granted separately because moving a name breaks the references stored outside Omniglass (bookmarks,
runbooks, integration config). Inside it nothing breaks, since every reference holds the entity’s
id, which every write returns (ADR-0076). A taken name is a 409, an
illegal or uuid-shaped one a 422.
Every command takes a name, never only a uuid
Section titled “Every command takes a name, never only a uuid”Every resource you type the name of answers to it, so a script or a runbook never has to look a uuid up first (ADR-1133). The uuid each command prints keeps working as well, and it is what to store when you need a handle that survives a rename:
omniglass component get 'boi.17c.415a.$comp.display-1' # a dotted address (quote the $comp)omniglass endpoint retry 'boi.17c.415a.$comp.display-1/https' # <component>/<endpoint name>omniglass secret update demo-panel-login --fields '{"password":"rotated"}' # its nameomniglass secret reveal '$platform/demo-panel-login' # <owner>/<name> where the name is sharedomniglass file download codec-firmware-2.1.4.txt # a filenameomniglass principal-group member create night-shift --principal-id aliceomniglass principal grant create alice --role viewer --scope-kind location --scope-id boi.17cA name only you can see resolves for you even where someone else’s out-of-scope row shares it, and a name more than one row you can see answers to is a 404 listing each one in the spelling that resolves, so the error tells you what to type next:
secret "demo-panel-login" not found: 2 you can see answer to it; address one as $platform/demo-panel-login or boi.17c/demo-panel-loginA name is a lookup, so it lands on whatever the one row you can see by that name is now: a write
answers with the row it reached (a secret’s owner_kind and owner_name), and a script that must
never drift, a credential rotation above all, addresses <owner>/<name> or stores the uuid.
Authentication
Section titled “Authentication”There are two ways to authenticate, both accepted on every request. A bearer token in
the Authorization header (the --token flag above) is the path for services and the CLI.
A username and password is the path for a human at the web console: the server verifies
it and sets an httpOnly session cookie. The CLI itself always uses a bearer token.
The first owner is created directly against the database with bootstrap (the trusted lane,
no running server needed):
# Mints a bearer credential (the token is printed once) and, with --password, a password# credential (argon2id) so the owner can sign in to the console. This is a trusted direct-DB# lane, so it is exempt from the password policy (unlike the console/API paths). The bootstrap# token expires after --ttl (default 90 days, hard maximum 365 days).
# Mint a fresh bearer token for an existing user (direct-DB, owner lane). A --description# (required) names what the token is for. Every credential is time-bounded: the token expires# after --ttl (default 90 days, hard maximum 365 days; a --ttl above the cap is an error). A# web-login session cookie has its own, shorter fixed lifetime.omniglass token ops --description 'ci pipeline'omniglass token ops --description 'nightly backup' --ttl 720h # a 30-day token
# A signed-in user can mint its own token over the API (the console Create token action), which# returns the secret once: omniglass auth create-token --description 'my laptop cli'.
# Set or rotate a user's password (direct-DB, owner lane; also policy-exempt as the recovery path).# A break-glass reset also revokes the user's live SESSIONS, so a stolen login stops at once; API# tokens are kept unless --revoke-tokens is given (a full lockout of a compromised account).omniglass set-password ops 'set-a-strong-one'omniglass set-password ops 'set-a-strong-one' --revoke-tokens # full lockout: sessions and tokensOnce a server is running, a signed-in principal manages its own account through the generated self-scoped commands (each edits only the caller’s own profile):
omniglass auth me # your principal, permissions, and grantsomniglass auth update-profile --label "Ops Lead"omniglass auth change-password --current-password 'orange-boat-42x' --new-password 'purple-canyon-7'omniglass auth set-avatar --image-base64 "$(base64 -w0 me.jpg)" # set your profile pictureomniglass auth remove-avatar # clear it, falling back to initialsomniglass auth avatar # read your picture back as { image_base64 }--image-base64 takes a plain base64 string, not a file path (base64-encode the image
yourself, as the $(base64 …) above does); the server accepts JPEG, PNG, or WebP and
normalizes it to a 256x256 JPEG. An administrator manages any principal’s picture with
omniglass principal setAvatar alice --image-base64 … and omniglass principal removeAvatar alice
(gated by principal:set-avatar), reading one back with omniglass principal avatar list alice (gated by
principal:read:admin). A principal with no picture is a 404.
Collection commands
Section titled “Collection commands”The collection surface regenerates into three top-level command
groups, node, endpoint, and task; the composed reachability read hangs off the component
that owns it (component reachability). They follow the same derivation as every
other resource (POST /endpoints is endpoint create, GET /endpoints/{endpoint} is endpoint get <endpoint>), so nothing here is special-cased. All examples require the matching permission on
the running server.
Register and enroll an edge node (the day-one handshake):
omniglass node listomniglass node create --name edge-hq --label "HQ Edge Node" --location hq-west --description "HQ network closet" # needs node:create (all-scope)omniglass node get edge-hqomniglass node update edge-hq --label "HQ Edge" --location hq-west # needs node:update; the name is immutableomniglass node delete edge-hq # needs node:delete; decommissions the node (cascades its endpoints, tasks, and enrollment)Every command above is new in practice, not only in the docs: a hand-written node run
mode occupied the same name as the generated node group, so cobra resolved omniglass node list to the daemon and it failed asking for --token. The run mode is now
omniglass node run, a verb beside the others:
omniglass node run --name edge-hq --token ogp_... # the edge daemon: claim, pull the worklist, heartbeatThe simulator runs beside a node with that node’s identity, serving the devices placed on it, and the node’s simulation view shows what it serves and what reached it:
omniglass sim run --server https://omniglass.example --name edge-hq --token ogp_... --port-offset 10000 # serve edge-hq's simulated devicesomniglass node simulation edge-hq # connected?, capabilities, and hits per simulated deviceomniglass component impairment set panel-1 --unplugged # break it: dark on every protocolomniglass component impairment set panel-1 --latency-ms 300 # or slow itomniglass component impairment set panel-1 # repair itomniglass simulation get # every simulator, its devices and their hitsomniglass simulation profiles # the profile catalog and the products each simulatesomniglass component simulation get panel-1 # its API surface: protocols, routes, commands, OIDsomniglass component simulation fire panel-1 --surface nvp --command "SET INPUT hdmi2" # the device answersomniglass simulation add-component edge-hq --name panel-9 --product newtron-panel-7 # served on the next pullomniglass simulation plan-system meeting-room # each role and the product it would getomniglass simulation add-system edge-hq --standard meeting-room --system-type meeting --location boardroom-a # the whole systemA set replaces the whole impairment, so name every fault you want at once: --latency-ms 300 on an
unplugged device plugs it back in.
Impairments apply only on a simulator started with --impairments and NET_ADMIN, in a namespace
of its own, since it rewrites that namespace’s firewall.
Locally, make dev-sim (beside make dev) enrolls the dev fleet’s node once, keeps its token in
bin/edge-hq.token, and runs the simulator and the node side by side, so the seeded fleet is
collected for real; its devices’ ports below 1024 are lifted by SIM_PORT_OFFSET (default 10000, where
the dev seed aims its endpoints).
The node-facing claim exchange is public (the enrollment token is the authentication), so
a node presents its name and token to receive its NATS credential:
omniglass node claim --name edge-hq --token ogp_...omniglass node enroll <name> mints (or re-mints) a node’s enrollment token and prints it
once (gated by node:enroll), the same action as the console’s Enroll / Re-enroll.
Author a reachability check by creating an endpoint (its poll task is derived automatically):
# An endpoint owned by a component, placed on a node, with its probe target in params.# It is named by its transport: --transport picks from the code registry, there is no --name flag.omniglass endpoint create \ --transport tcp --component disp-1 --node edge-hq \ --params '{"target":"10.0.0.1:22"}' # needs endpoint:create
# Or ATTACH a driver instead of picking a transport: the driver's spec derives the# transport, the target (from the host and port inputs), and the endpoint's tasks# (a poll task per poll function, a standing listen task per listener). Secret# inputs are references: the NAME of a secret of the declared shape, never a value.omniglass endpoint create \ --driver snmp-generic --component amp-1 --node edge-hq \ --inputs '{"host":"10.0.0.40","community":"lab-community"}' # needs endpoint:create
omniglass endpoint list# An endpoint is <component>/<endpoint name>: its name is its transport, unique on its component,# and the component is any spelling the component commands take (a name, a dotted address, a uuid).omniglass endpoint get disp-1/tcpomniglass endpoint update disp-1/tcp --node edge-hq --params '{"target":"10.0.0.2:22"}'# A driver-attached endpoint's address and secret edit in place, through its inputs# (validated against the driver; its tasks re-derive in the same transaction):omniglass endpoint update amp-1/snmp --inputs '{"port":"2323","community":"lab-community-2"}'omniglass endpoint delete disp-1/tcp # refused (409) while its task references it
# An endpoint a refusal gated (endpoint-rejection names the rung, endpoint-rejection-reason says why,# held session or not) is not attempted again until what it connects with changes, or an operator# retries it:omniglass endpoint retry disp-1/ssh # needs endpoint:retry; auditedomniglass endpoint retry disp-1/ssh --host-key SHA256:... # ssh: trust the offered key the refusal names (409 for any other)omniglass endpoint retry disp-1/https --certificate SHA256:... # tls or https: the same for a certificate's public key (--host-key works too)# A tls or https endpoint's verification mode is one of its driver's inputs, edited in place:omniglass endpoint update disp-1/https --inputs '{"tls-mode":"verify"}' # verify, pin or skip
# The poll task is derived from the endpoint, so the task surface is read-only.omniglass task listomniglass task get <id>The transports are a code registry the binary ships (omniglass transport list reads it):
icmp and tcp probe layers 3 and 4, telnet reads its first bytes, tls runs the handshake and reports the
certificate’s expiry, ssh, http and https (the handshake, then a response) climb to layer 7
(the probe draws a real response, so reached-but-not-responsive and responded-but-not-authenticated
are visible states; an http probe sends no credential, so its driver’s polls prove the login), udp proves its rung with its driver’s request (a bare udp endpoint has nothing to send),
and snmp proves its rung with a standard get (sysUpTime.0) carrying its driver’s community.
An endpoint is named by its transport (the --transport, or the attached driver’s spec),
unique within its component. An endpoint update changes only its node placement and
params. A task is derived when its endpoint is created, so there is no
task create, update, or delete; its placement follows the endpoint’s. A node purge
cascades its endpoints and their derived tasks.
Read a component’s composed reachability (the verdict, the probe-layer signals, and the recent transitions the availability strip draws):
omniglass component reachability list disp-1 # needs component:readRead a component’s property reconciliation, the want/told/is pivot (the current declared value with the contract default coalesced in, the intended value a command set, and the latest observed value, all series reads) with drift computed on read:
omniglass component reconciliation list disp-1 # needs component:readSecrets
Section titled “Secrets”The secret commands are generated like every other resource. secret
covers the encrypted values, secret-type lists the shape registry, and component effective-secret reads the masked cascade onto one component. Output is masked JSON, the same as the console; plaintext lives
behind reveal, which the server audits and which only admin and owner may call.
omniglass secret-type list # the shape registry (snmp-community, basic-auth)omniglass secret list # the all-scope admin directory (masked fields)omniglass secret create --name core-snmp --secret-type snmp-community \ --owner-kind location --owner hq --fields '{"community":"public"}'omniglass secret update core-snmp --fields '{"community":"s3cret"}' # an omitted field keeps its valueomniglass secret reveal hq/core-snmp # audited plaintext decrypt (secret:reveal), by <owner>/<name>omniglass secret delete core-snmp
omniglass component effective-secret list codec-1 # the masked cascade resolved onto a componentThe effective read answers which secret applies to a device and where it comes from, never
what it contains: fields are masked exactly as the directory masks them, and plaintext is only
ever secret reveal. It rides secret:read, which the viewer floor does not carry.
--owner-kind is one of platform | location | system | component; --owner names the owning entity
and is omitted for a platform secret (the install-wide tier, which needs an all-scope grant plus
platform:create). Field maps pass as a JSON object to --fields, validated against the type’s shape.
A secret’s name is unique within its owner, and the cascade makes one name at several owners the
ordinary case (a room’s core-snmp overriding the platform’s), so the bare name resolves only where
exactly one secret you can see carries it. <owner>/<name> always resolves: the owner by name,
dotted address or uuid, or $platform for the install-wide tier, which has no name of its own
(omniglass secret reveal '$platform/core-snmp').
Variables
Section titled “Variables”The variable commands are generated the same way. variable covers the
plaintext values and component effective-variable reads the cascade onto one component. There is
no reveal: the value is shown in the clear.
omniglass variable list # the all-scope admin directoryomniglass variable create --name poll-interval --value-type int \ --owner-kind system --owner east-auditorium-av --value 30omniglass variable create --name retry --value-type json --owner-kind platform \ --value '{"retries":3,"backoff":"1s"}'omniglass variable update east-auditorium-av/poll-interval --value 60 # validated against the fixed value_typeomniglass variable delete retry
omniglass component effective-variable list codec-1 # the cascade resolved onto a componentBoth effective reads return the winner and the candidates it beat, with the tier each came
from, so the answer explains itself rather than just asserting a value. The variable cascade
resolves the system band from the component’s primary membership; resolving against a named
system is offered only by component effective-tag list today.
--value-type is one of string | int | float | bool | json. --value is parsed as JSON, so a
bare 30, true, or {"k":"v"} sends the number, the boolean, or the object; a bare word like HDMI1
falls back to a string, so the common case needs no quoting. A string value that would otherwise parse
as JSON (30, true) is quoted to force a string: --value '"30"'. (secret create --fields parses
the same way.)
A signed-in principal manages its own account through the generated self-scoped commands
(omniglass auth me, auth update-profile, auth change-password, auth set-avatar /
auth remove-avatar); an administrator manages other principals, roles, groups, secrets,
and variables through the resource commands, all covered in the admin guide
and listed in full in the CLI reference.
The tag commands split along the governance line. The tag resource covers the
key vocabulary (minting, editing, and deleting keys, plus the install-wide platform binding), while binding a
value onto an entity is a custom method on the entity (component setTag and friends), so it needs
only the write the operator already holds on that entity. effective-tag reads the resolved cascade onto
one component.
omniglass tag list # the governed key vocabularyomniglass tag create --name environment # mint a key (tag:create, admin)omniglass tag create --name rack-position --applies-to '["location"]' --propagates=falseomniglass tag update environment --applies-to '["component","system"]'omniglass tag setPlatform environment --value prod # an install-wide default (tag:update + platform:update)omniglass tag clearPlatform environmentomniglass tag delete environment # cascades its bindings
omniglass component setTag codec-1 --key environment --value dev # component:updateomniglass component listTags codec-1 # the bindings set directly on the componentomniglass component removeTag codec-1 --key environmentomniglass system setTag east-auditorium-av --key environment --value prodomniglass location setTag hq --key environment --value staging
omniglass component effective-tag list codec-1 # the cascade resolved onto a componentBinding is a custom method on the entity (component setTag), like the principal lifecycle verbs, so it
stays clear of the top-level tag commands. A tag’s --name is an ordinary entity name (lowercase
letters, digits, and hyphens, no dots and no underscores, never a uuid), and minting one that breaks
the rule is a 422; that name is then the key a binding carries (--key). --applies-to is an
entity-kind allow-list passed as a JSON array ('["component","system"]'; empty means universal),
checked when a value is bound. --propagates
defaults true (the value cascades to descendants); --propagates=false binds a flat per-entity value
that resolves only on its own entity. Resolving onto a component unions keys and overrides values
most-specific-wins down the cascade.
Component classification catalogs
Section titled “Component classification catalogs”The component-classification catalog
commands cover the vendor and driver registries: flat, official-vs-custom catalogs on
the same pattern as the type registries. Each resource’s :read sits on the viewer floor; the three
writes (:create, :update, :delete) are admin-gated.
A vendor names an organization, carrying a --kind of manufacturer, integrator, or developer
(default manufacturer):
omniglass vendor list # the vendor registryomniglass vendor create --name barco --label Barco --kind manufacturer \ --icon monitor --support-phone "+1-555-0100" --website https://www.barco.comomniglass vendor get barcoomniglass vendor update barco --support-phone "+1-555-0199"omniglass vendor delete barco # refused (422) if officialA driver names the implementation that gets, emits, or sets a product’s signals, with an optional
--version:
omniglass driver list # the driver registryomniglass driver get snmp-generic # includes the spec body, when authoredomniglass driver create --name barco-snmp --label "Barco SNMP" --version 1.0.0 \ --spec '{"version":1,"transport":"snmp", ...}' # validated at write: a broken spec is a 422 naming the faultomniglass driver update barco-snmp --version 1.1.0omniglass driver delete barco-snmp # refused (422) if official, or while an endpoint is attached through itAn official row, for example the crestron vendor, is read-only: update and delete both
422. A vendor’s website is validated to an http/https scheme on write; any other scheme (for
example javascript:) is a 422.
Products
Section titled “Products”The product commands cover the product
registry: the concrete SKU that ties the vendor and driver catalogs together, classified under a
component_type, and the thing a component points at. product:read sits on the viewer floor; the
three writes (product:create, product:update, product:delete) are admin-gated.
A product names its kind (device, app, service, or vm, default device), the
component_type it is classified under, and optionally its vendor, driver, and a
parent product it is a variant of:
omniglass product list # the product registryomniglass product create --name barco-ub12 --label "Barco UB12" --kind device \ --component-type projector --vendor-id barco --driver-id barco-snmpomniglass product get barco-ub12omniglass product update barco-ub12 --label "Barco UB12 Pro"omniglass product delete barco-ub12 # 422 if official, 409 if a component points at itAn official product, for example cisco-room-bar, is read-only: update and delete
both 422. A product still referenced by a component (component.product_id) cannot be deleted (409);
an unknown vendor, driver, parent, or component_type id is a 422.
Standards
Section titled “Standards”The standard commands cover the
system-side counterpart of a product: the blueprint a system conforms to. standard:read sits on the
viewer floor; standard:create, standard:update, and standard:delete are admin-gated.
omniglass standard list # the standard catalogomniglass standard create --name lecture-hall --label "Lecture Hall" \ --parent-standard-id classroom # a variant of an existing standardomniglass standard get lecture-hallomniglass standard delete lecture-hall # 409 if a system still conforms to itUnlike a seeded product, the shipped standards are official: false: they are forked from an in-code
template once, with no inheritance, so they are yours to rename, re-parent, or delete, and the boot seed
installs one only if absent rather than reasserting over your edit. The same holds for the shipped
location types. See the seed model.
Event types
Section titled “Event types”The event type commands cover the occurrence keyspace,
the twin of the property catalog: the discrete happenings an event is typed by (call-started,
cable-unplugged). event_type:read sits on the viewer floor; event_type:create, event_type:update, and
event_type:delete are admin-gated. Official event types are read-only.
omniglass event-type list # the event type catalogomniglass event-type create --name cable-unplugged --label "Cable unplugged"omniglass event-type get call-startedomniglass event-type delete cable-unplugged # 409 on an official typeEvery metric, property and event type reads back its writer_class (node-probe, platform,
command-lifecycle, calc, collected): who may write the key (who writes a key).
omniglass event-type get collection-failed reads node-probe, the node’s own account, and
command-issued reads command-lifecycle, a command’s alone; a create under a reserved name is a 422
on every catalog.
Command types
Section titled “Command types”The command type commands cover the “do”
catalog: what a component can be told, with a target property and a settle window. command_type:read
sits on the viewer floor; create/update/delete are admin-gated, official types read-only. Issuing a
command (command:issue) records the invocation, writes a caused event, and returns the computed
settlement verdict; the invocation then rides the per-node command queue to the placed node,
which actuates it through the attached driver’s command binding, and the settle loop closes
against the observed value the driver’s polls report back
(commands).
omniglass command-type list # the command type catalogomniglass command-type create --name set-input --target-property-type video-input --settle-window-seconds 15omniglass command-type get rebootomniglass component command issue disp-1 --command-type set-input --value '"hdmi2"' # needs command:issueA target must be a key a command may write: a native probe key, health or a calc rule’s output is
refused (422) naming the key.
Property contracts and values
Section titled “Property contracts and values”A classifier declares which properties its instances carry; an instance sets a value. Both sides are the same three verbs, and the contract commands hang off the classifier that owns them:
omniglass product property list cisco-room-bar # a product's contractomniglass standard property list huddle-room # a standard's contractomniglass location-type property list room # a location type's contractomniglass standard property update huddle-room room-capacity --default-value 6 --required=trueomniglass standard property delete huddle-room room-capacity # systems keep any value they set
omniglass component property list dsp-boardroom-3 # the effective readomniglass system property list boardroom # same shape, system sideomniglass location property list east-campus # same shape, location sideomniglass system property update boardroom room-capacity --value 12 # idempotentomniglass system property delete boardroom room-capacity # falls back to the contract defaultThe read resolves the classifier’s contract against the instance’s own values, so a one-off system
(one conforming to no standard) and a productless component still resolve, to their off-contract
values alone. The value commands are scope-injected: an instance outside your read scope is a
non-disclosing 404 on the read and on the write. The registry and its contract share one noun:
omniglass location-type list is the registry, omniglass location-type property list <id> its contract.
Generated versus hand-written
Section titled “Generated versus hand-written”- Generated (
internal/cli/api_gen.go, do not edit): one command per API operation. The resource and verb come from the AIP-style path (POST /locationsislocation create,GET /locations/{name}islocation get <name>, a:verbcustom method is<resource> <verb> <id>, so the principal lifecycle isprincipal disable <id>,principal archive <id>,principal restore <id>, andprincipal purge <id>); path parameters are positional args, the request body is--flags, and OpenAPI query parameters become optional--flags(a set flag is appended to the request query string, an unset one keeps the server default), soprincipal list --include-archived --kind servicefilters the listing. A principal<id>argument accepts either the uuid or a human’s username (omniglass principal archive alice), resolved by the server, so you rarely need to look a uuid up first.--helpplus the example come from the operation’s summary and description. - Hand-written (
internal/cli/api_hooks.goand the run-mode files): the client runtime the generated tree calls, plus commands that are not API operations, theserver,node run, andmigraterun modes, the trusted direct-DB owner lane (bootstrap,token,set-password), and the idempotent dev-fleet seeder (seed-dev, the same trusted lane, never for production).
To add a hand-written command, write a newXxxCmd() returning a *cobra.Command and add
it in newRoot, exactly as bootstrap does. Regenerating the API commands never touches
it.