Skip to content

CLI

The cli tool (algol) is a stateless command-line client. It has no persistent configuration. Connection parameters are flags at invocation (--nats-url, default nats://localhost:4222; --key-file, see Bus identity). --unscoped lifts the CLI's own role-based filtering of what its view verbs show (RFC-035 D5; the configurer's intersection of the config listing stays) - see the app README.

It wraps the configurer, recorder, and flower request-reply endpoints. See Messaging for the subject map.

algol --help
algol config --help

Commands

Command Purpose
algol bundle ls / info / update / activate / delete Configuration bundles
algol manifest import / export Backup and restore
algol config ls / info / update / delete / patch / commit Running and startup configuration
algol file put / ls / info / get / rm Files on the device: content inline, a path, or a url to download
algol channel ls / info / subscribe Data channels
algol data-source ls / info Data sources
algol measurement ls / info Measurement windows
algol recording ls / info Recording catalog
algol trigger publish Start/extend/stop a measurement (talks to flower)
algol command <ref> [<payload>] Send a command to any endpoint, by attribute ref; a declared command's body is checked first
algol command ls / info The commands this build declares, and what one takes and answers
algol key init / show This user's key on the device bus
algol nats ls / enrol / revoke The nats app's identity registry (role keymaster)
algol secret set / get / ls / rm Secrets: declare and set, read back, list, delete
algol version Client version

Configuration bundles, manifests, and --bundle on config commit / config delete are described under Configurer. Config type shapes are in Configuration.

Bus identity

Every command except key and version connects with a user key (RFC-034 D2, D11). Which key:

  1. --key-file <path> when given;
  2. otherwise the effective user's own, ~/.algol/nats/identity.nkey (home looked up by uid, not $HOME);
  3. otherwise, for root only, the boot's root seed /run/algol/nats/root.nkey, which the nats app writes at start under --auth.

A non-root user without a key is told to run algol key init; there is no fallback to root's key and no anonymous connection. The connection is named <user>@<host> for the nats app's log. First-time setup of a device - creating and enrolling these keys, per user - is walked through in Device Setup.

algol key init                                  # ~/.algol/nats/identity.nkey, 0700/0600; refuses to replace one
algol key show                                  # the public key, for enrol
algol nats ls                                   # modules (expected, bound) and enrolled user keys
algol nats enrol --key UQRS... --label lukas-laptop --role superuser
algol nats enrol --module recorder --key UNEW... # rebind a module (e.g. after its volume was wiped)
algol nats revoke --key UQRS...
algol nats revoke --module recorder

The nats verbs are signed with the caller's key and need role keymaster: the boot's root, or a key enrolled as keymaster. A role is granted at most at the caller's own. A revoke, a rebind and a role change drop the affected key's live connections at once; the command prints how many. A revoked module name stays closed - shown as (revoked) in ls - until enrol --module --key binds it again. --nats-id addresses a nats app running under another --id.

The upstream nats CLI

The same keys work for the plain nats CLI. An enrolled user key - root's, or one enrolled as superuser or keymaster - may subscribe every reply inbox (RFC-034 D8 rule 1), so --nkey alone is enough for req, sub and anything JetStream, and nats sub '_INBOX.>' next to nats sub 'local.>' shows the replies as well as the requests. One request is the exception: a secret get is answered to a platform-shaped reply subject only (rule 4), so from the nats CLI it needs --inbox-prefix "_INBOX.userkey.<publicKey>"; algol secret get is the same request with the prefix already right. A key with a configured role (RFC-035 D7) is confined to its own inbox and needs the prefix for every request/reply: without it the subscribe is refused and every req times out waiting for a reply it was never allowed to hear, which looks like an unresponsive device rather than a permission error.

algol key show prints the public key the prefix needs, on stdout - the resolved path goes to stderr, so command substitution captures the key alone:

nats --nkey ~/.algol/nats/identity.nkey \
  --inbox-prefix "_INBOX.userkey.$(algol key show)" \
  req local.cfg.cmd.secret.wifi_site.get '{}'

That holds where algol is a host binary. On a device it is a docker exec wrapper, so algol key show resolves inside the container and reports the boot seed rather than the key the --nkey file holds - the two halves would then name different identities. The device setup runbook has the device form and the nats context form, which types both flags once. One trap when saving a context: a context persists the prefix, so an algol key show that fails leaves _INBOX.userkey. - ending in a dot - and every later command under that context fails with nats: error: nats: invalid custom prefix. Capture the key into a variable first so the failure is visible, and read back what a context holds with nats context info.

algol command

The generic verb, for the endpoints that have none of their own (algol cmd for short): one request to a component's command endpoint, addressed by attribute reference - <moduleId>.<componentName>.<componentId>.<command>, which is the subject local.<moduleId>.cmd.<componentName>.<componentId>.<command>.

algol command sys.led.status.set '{"green":"blink","red":"on"}'
algol command sys.systemdmount.usb0.release             # an endpoint that takes no body
algol command bc.bcapp.reg.wifi_on '1' --content-type text/plain

What a command takes is what the build declares (RFC-040), and the verb reads that declaration before anything leaves:

algol command ls                          # every declared endpoint, with its identity, body and reply kinds
algol command ls sys.teapot.*             # narrowed by a filter over the endpoints' refs
algol command info sys.teapot.pot0.brew   # the fields with their descriptions, the reply, an example

A declared JSON command's body has to be a JSON object whose declared fields are present when required and of the kind each field takes, or the send is refused with the reason and a pointer at info; its content type defaults to JSON, a text command's to the one it declares, and a given --content-type stands. A signed command is refused with the verb that signs it. --no-check sends the body as given. A command the build does not declare is sent as it is, unchecked - a service from another repository is what the generic verb exists for - and ls and info do not list it. Both read the registry compiled into the CLI, not the bus, so neither needs a key.

The body is sent as given and the reply is printed as it arrived, with a newline added when it does not end in one, so it pipes into jq. An endpoint that answers with an empty body prints nothing - the exit status is the answer - and a refusal (a micro error, no responder, no reply within --timeout, default 5s) is an error, never an empty success. --content-type is sent only when it is given, so the endpoint's own default applies.

The request is not signed. A signed body is what the configurer's write verbs need (RFC-035 D14); every other command is decided by the bus from the requester's role, so a command this key may not send is refused by the server. That is also what cmd means in the access line config info prints: a role that writes a component may send every command on it (D4 rule 2).

Secrets

A secret (RFC-001) is a secret declaration, cfg.secret.<id>, plus a value the configurer stores and serves on the secret's own command subject. The declaration travels the config API; the value never does. The verbs:

algol secret set wifi_site --consumer sys.wlan --from-file ./site.psk   # declares cfg.secret.wifi_site (string, for sys.wlan), then sets the value
algol secret set wifi_site --from-file ./new.psk                        # rotates: the declaration exists, only the value is sent
echo -n "$TOKEN" | algol secret set api_token -                         # a lone - reads stdin
algol secret set aws_export --kind bytes --consumer recorder.s3upload --from-file ./credentials
algol secret set webui_lukas --kind bcrypt --consumer webui.webuser --bcrypt -   # a password on stdin, hashed by the CLI; the hash is what is sent
algol secret set webui_lukas --from-file ./hash.txt                              # a ready-made bcrypt hash (htpasswd -B), stored as given
algol secret set aws_export --kind awsCredentials --consumer recorder.s3upload --from-file ./sts-session.json   # the JSON its issuer produced, checked for the key pair
algol secret get wifi_site                                              # the value, raw, nothing else
algol secret get cfg.secret.wifi_site                                   # the same secret, named by its whole ref
algol secret ls                                                         # config ls cfg.secret.*
algol secret rm wifi_site                                               # config delete cfg.secret.wifi_site

Every verb names the secret either way: the <id>, or the whole cfg.secret.<id> ref that config ls and secret ls print. The two cannot be confused, since an id may not contain a .. A ref under another module, or of another type, is refused rather than read for its last part.

set takes the value from exactly one of --value, --from-file and a lone - (stdin). A file or stdin loses one trailing newline (LF or CRLF) when the secret is a string or a bcrypt one, because echo and editors add one and a key with a newline never matches; --value is taken as given. The value is checked against the kind before anything is sent, so a bad value never leaves an empty declaration behind.

--bcrypt hashes the value with bcrypt (cost 10) before anything is sent, so the password never leaves the CLI; it is refused unless the secret is a bcrypt one (or is being declared as one with --kind bcrypt). It is a flag, never inferred from the kind: without it a bcrypt secret takes a hash as given, so one produced elsewhere can be piped in, and anything that is not a bcrypt hash of cost 10 or more is refused before sending. get on a bcrypt secret prints the hash, which is what the store holds.

On an id with no declaration, set first issues config update cfg.secret.<id> from --kind (string by default, bytes, bcrypt or awsCredentials), --consumer (required then, repeatable) and --description, reports the configurer's answer (WAIT no value for a fresh declaration), then sends the value. On a declared secret those three flags are refused: scoping a secret is a declaration edit, algol config update cfg.secret.<id>, not a value write. Like every config update, the declaration is uncommitted until config commit - --commit on set and rm does that in the same command; the value itself is stored regardless.

get prints the bytes and nothing else - no newline, no label - so they can be redirected as they are. Both set and get are made with the key the CLI connected with (Bus identity); that key is what the configurer's audit line names, and what the bus confines the reply to.

Files

A file on a device is a file config, <module>.file.<id>, and algol file is the way to write one without typing base64. The verbs:

algol file put bcapp   --file ./bcapp.sh --inline --cache high --adopt-digest --commit
echo 'ret-0017' | algol file put site_id --data -                      # literal content, or stdin
algol file put staged  --file /var/algol/public/fw.zip                 # a path on the DEVICE, not on this machine
algol file put rootfs  --url https://host/rootfs.squashfs --digest sha256:ab12...
algol file put tool    --url https://host/tool.sh --inline --adopt-digest   # fetched here, embedded
algol file ls                                                          # where each file came from and where it landed
algol file info sys.file.bcapp                                         # the whole ref names the same file
algol file get bcapp -o bcapp.sh                                       # inline content only
algol file rm fw_v381 --commit

Every verb names the file either way, as algol secret does: the <id>, which is under sys, or the whole <module>.file.<id> ref that file ls and config ls print. The ref is the only place another module is named - there is no flag for it - and file ls lists every module's files, which is why its rows carry the whole ref.

put takes exactly one source, which is the three url schemes the type has, spelled as what an operator has in hand. --data is content, or a lone - for stdin; --file is a path on the device, under the public or non-volatile file root, which this machine never opens; --url is downloaded by the device. --inline turns the second and third into content instead of a reference: the CLI reads the file or fetches the url and embeds what it got. It is an explicit flag, never inferred from the path, because "name this file on the device" and "send me this file" look identical on the command line.

Stdin is the form to use on a device. The algol wrapper runs the CLI inside a container that sees no host path, so --file ... --inline there reads a path inside the container - the same trap algol key init has (Device Setup). A pipe crosses that boundary with nothing staged: algol file put fw --data - < ./fw.zip. The bytes are taken as they stand, with no trailing newline stripped, because a text file keeps its final newline and a zip must arrive untouched.

A digest is at most one of --digest, the pin you write and the module verifies, and --adopt-digest, which pins the content this command read. The computed pin is opt-in on purpose: a later hand edit of the url would otherwise turn a silent back-fill into a mismatch nobody asked for. A url the device downloads has no content to read here, so its digest is mandatory - --adopt-digest then fetches the url from this machine to pin it, which is trust on first use.

Inline content is capped at 64 KiB decoded, checked here before anything is sent; past that a url the device downloads is the shape to use. Where an inline file lives on the device is --cache: ranked it is kept in the artifact cache and survives a wiped config directory, unranked it lives on a tmpfs the config rewrites at every boot. See RFC-032.

A put is not on disk until it is committed, and --commit does that in the same command. Without it the write lives in the config store's memory and the command says what the next restart costs: the ref goes, and with it whatever named the bytes - inline content at no rank sits on the tmpfs the config rewrites at every boot, an unranked download is swept once no config names it, and a ranked artifact stays, so putting the same content back under the same ref is a cache hit rather than a download. file ls marks an uncommitted config with *. rm is the same the other way round: an uncommitted delete leaves the config on disk, so the file is back at the next restart. A config the module refused is never committed - --commit fails instead, because a rejected config on disk is re-applied at every boot.

ls and info read the fileinfo mirror, which reports the source, the size on disk and the retention level, so a listing never carries a payload - for inline content the body is the file itself. info is the verb that reads the body, and it summarises a data: url rather than printing it. get answers from that body, so it is inline content only: a downloaded or referenced artifact lives on the device and reading it back is not something sys serves. rm is config delete on the ref, and names the artifact left behind when one was ranked.

config patch

Updates enabled and/or named properties on an existing config without replacing the whole body. The type of each property stays whatever the intent already has; adding, removing, or changing a type is a config update.

algol config patch cfg.props.shared -p device_label=RET-99
algol config patch cfg.props.shared -p device_label=RET-99 -p retries=3 --commit
algol config patch trigger.trigger.speed --disable

-p / --property takes name=value and may be repeated. It copies the existing entry and changes only the value, so description and a configRef's jsonPath / timeout / ignore stay. --enable / --disable toggle the enabled field. --commit persists after a successful patch. --ref-path, --ref-timeout, and --ref-ignore still adjust those fields on a configRef in the same command.

--string, --int, --float, --json, and --ref are deprecated: they restate the type and replace the whole entry, which drops those fields.