Ovvi
Agent control

Protocol

The doorbell token, the mailbox files, request ids, answers, error and exit codes, versions and the security rules

The protocol of the ovvi command (Agent control) has two parts, both on the asking pane's machine:

  • The mailbox: the folder ~/.cache/ovvi/rpc/ (mode 0700). A request and its answer are small files there, named by the request id.
  • The doorbell: the herdr pane token ovvi-req, whose value is the request id. Each Ovvi that follows the session hears it on the event stream that it already has open (Talking to herdr).

Ovvi reaches the mailbox with run: a Process on This Mac, the relay on a remote machine (Running commands). Nothing listens on a port, and nothing new runs on the machine.

One request

StepWhoWhat
1. Writethe commandmakes an id, then writes ‹id›.req (to ‹id›.tmp, then mv). For copy it writes its standard input to ‹id›.in first
2. Ringthe commandherdr pane report-metadata ‹pane› --source ovvi-cli --token ovvi-req=‹id› --ttl-ms 3000 on the asking pane
3. Heareach Ovvi that follows the sessionthe pane.updated event carries the pane and its tokens; Ovvi acts once per id
4. Waitan Ovvi whose user is away500 ms, so that an Ovvi in use takes it first (Two Ovvis)
5. Takethe Ovvione run on the machine: mv of ‹id›.req to ‹id›.run, then base64 of the file. A failed mv means that another Ovvi took it, or the command gave up: Ovvi stops
6. Dothe Ovviparses the argv, checks it (Security), acts. copy reads ‹id›.in with one more run
7. Answerthe Ovvione run writes ‹id›.out, then ‹id›.exit (each to a temporary name, then mv), and removes ‹id›.run and ‹id›.in
8. Readthe commandprints ‹id›.out to standard output and the rest of ‹id›.exit to standard error, removes ‹id›.* and exits with the code

The command looks for the files every 50 ms:

  • Taken: ‹id›.req must be gone within 3 s (2 to 3 s, because the command counts whole seconds of the clock). Else the command renames it to ‹id›.dead itself. One mv wins, so an Ovvi that is late finds nothing. The command removes its files and exits 3.
  • Answered: ‹id›.exit must exist within the timeout (10 s by default, --timeout ‹seconds›). Else exit 4. An answer that comes later lands in the folder, and Ovvi removes it on a later connect.

The take, as Ovvi runs it (the limit is 64 KiB and one byte, so a request that is too large shows):

sh -c 'cd "$HOME/.cache/ovvi/rpc" && mv -- "$1.req" "$1.run" && head -c "$2" "$1.run" | base64' \
    sh 7f9c2b1e-4d0a-4c55-9a51-0b6c3e2d8f10 65537

Ids

  • A request id is a lowercase UUID version 4 that the command makes, from /proc/sys/kernel/random/uuid or uuidgen. With neither, the command exits 5.
  • Ovvi takes only a value that matches ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$, and builds every path from it. So a token can never name another file.
  • herdr sends the pane's tokens again on every change of the pane until the token expires. Ovvi remembers the last 256 ids per session and acts once per id.
  • One pane rings one request at a time: a second ring replaces the token's value. The command waits for the answer before it returns, so calls in a row from one pane never overlap. Parallel calls from one pane can lose the first ring, which then exits 3.
  • Ovvi reads the token off each pane.updated event and each snapshot, so a ring while its stream opens again (after a link drop, or when the set of agent panes changes) is taken from the next snapshot.

The request

‹id›.req holds NUL-separated fields, each ended by a NUL, so no field needs quoting:

ovvi-req ␀ 1 ␀ /Users/me/repo ␀ open ␀ git ␀ . ␀ uncommitted ␀
FieldValue
1ovvi-req
2the protocol version, 1
3the command's working folder ($PWD), for relative paths
4…the arguments, as typed after ovvi and its own options
  • At most 64 KiB and 64 fields. ‹id›.in holds at most 1 MiB.
  • The asking pane is the pane that carries the token. Ovvi knows its machine and session from the stream that heard it, so the request does not repeat them.
  • --pane, --timeout and status are the command's own and are not sent. A --json before the command is sent after it.
  • A file that is not a request, or one over the limits, is usage. A newer protocol than Ovvi's is protocol.

The answer

  • ‹id›.out: what the command prints, as text, or as JSON with --json.
  • ‹id›.exit: the exit code on the first line, then the text for standard error.
1
ovvi: pane w1:p9 is not in herdr session default

With --json, a failure also writes the error to ‹id›.out:

{"error":{"code":"pane_not_found","message":"pane w1:p9 is not in herdr session default"}}

A failed answer run is logged in Ovvi, and the command exits 4.

Exit codes

CodeMeaningWho
0doneOvvi
1the command failed; the error says whyOvvi
2usage: an unknown command, a bad option, a missing or extra argumentOvvi, or the script for its own options
3no Ovvi took the request within 3 s, or (ovvi status) no Ovvi follows the sessionthe script
4an Ovvi took it but did not answer within the timeoutthe script
5not in herdr: no $HERDR_PANE_ID and no --pane, herdr refused the ring (a stale or unknown pane: pass --pane), or the mailbox could not be writtenthe script

A stale $HERDR_PANE_ID never reaches Ovvi: herdr refuses the ring, so the script exits 5 and says to pass --pane.

Error codes

The errors, in {"error":{"code","message"}} with --json. The script answers no_ovvi, timeout, no_herdr, and usage for its own options; Ovvi answers the rest.

CodeExitWhen
usage2the script's --pane or --timeout without a whole value; an unknown option, a missing or extra argument, two places for open, --replace for agent; a file that is not a request, or over 64 KiB or 64 fields
unknown_command2a command this Ovvi does not have (ovvi help lists them)
protocol2the request's version is newer than this Ovvi speaks
pane_not_found1a pane argument: no such pane, or no such session running on the asking pane's machine
plugin_not_found1no browser or plugin with that id; with the reason when its manifest did not load
bad_argument1the argument does not fit: not a pane, a missing path, a kind that does not open it, a label over 1024 characters or with a control character, a source that is not one, standard input over 1 MiB, an unknown agent, a pane with no agent for chat, an agent with no chat view for view
other_machine1plugins new, reload or install from a pane on a remote machine
not_floating1unfloat of a pane that does not float
busy1chat while the agent works or needs you, or while a relaunch runs
self1chat on the asking pane
rate_limited1a second notify from the asking pane within 10 s
dirty1open --replace of a card with unsaved edits; plugins reload while a card of the plugin has them (the message names the pane)
declined1the user pressed Cancel in the confirmation of plugins install
build_failed1plugins reload: the build failed; the message ends with the build's last 20 lines
failed1anything else: a herdr call or a run failed, no window shows the session, the Plugins page's own refusal; the message is herdr's, the shell's or the page's
no_ovvi3no Ovvi took the request within 3 s
timeout4an Ovvi took it but did not answer within the timeout
no_herdr5no pane to ring, herdr has no such pane or refused the ring, or the mailbox could not be written

A pane argument is always read on the asking pane's machine, because its syntax cannot name another one. So no pane argument is other_machine.

Versions

  • The protocol version (1) is in each request, in ovvi-open and in the script's second line. It changes only when the files, the token or the exit codes change. The format of ‹id›.exit never changes, so any Ovvi can answer protocol to any command.
  • Commands change without a version. A new Ovvi adds commands, options and JSON fields. A field is never renamed or removed. A change that breaks a command gets a new command name. ovvi info --json lists commands, so a caller can look for one first.
  • The script's version is its second line, # ovvi-cli ‹protocol›.‹revision› (# ovvi-cli 1.2). Ovvi compares the machine's line with its own script's, as numbers, and writes the script only over a lower one. A fixed script gets a new revision and replaces the old one without a new protocol. Ovvi never writes an older script, so two Ovvis of different versions do not take turns (How the command gets there).

Two Ovvis

Two windows of one Ovvi share one store per session, so they hear one event (Open sessions). Two Ovvis (two Macs, or two builds on one Mac) both hear it.

  • The take is one mv on the asking machine: exactly one Ovvi wins.
  • An Ovvi waits 500 ms before it takes when its user is away: no key press or click in the app for 60 s, the screen is locked, or no visible window has the session's page in view. So the Ovvi in use answers first.
  • Each command runs in the Ovvi that took it. A float opens over that Ovvi's window, and the clipboard is that Mac's.

Presence

Each Ovvi writes ovvi-shown on each pane in view and ovvi-open on each space of a session it has open (Is my pane in Ovvi?, the calls: Agent requests). Both go in one burst every 10 s, the panes' ovvi-shown first: a tool reads it at each click, and the spaces' marks are one call each. At quit all go at once, because the quit waits only 1 s. A session with 8 spaces adds 8 small calls every 10 s.

Security

Who can ring: any process that reaches the session's herdr socket, which is the user who owns herdr on that machine. That process can already type into any pane of the session. So the command gives nothing new on that machine. The rules guard the Mac and the user's attention:

  1. Own machine only. A pane argument is a pane of the asking session, or ‹session›/‹pane› of a session that herdr runs on the same machine. Anything else is pane_not_found or bad_argument.
  2. Nothing read or started on the Mac. No Mac file, no Mac app (Open With is not in the command), no read of the clipboard. copy only writes it. browser ./file.html opens a file only for a pane on This Mac.
  3. Plugins change only from This Mac. plugins new, reload and install from a remote pane are other_machine. An install shows the confirmation, and Return does not press Install.
  4. Pages load by the browser's rule. ovvi open browser https://example.com makes the card. A site that is not local and not approved shows Open this page? until the user clicks (Browser).
  5. No focus, no view change. Ovvi never activates itself for a request, never switches a window's tab or page, never moves the keyboard. A float opens its window without activating the app. Only a click on an agent's notification brings Ovvi to the front, because the user asked.
  6. Notifications are limited to one per asking pane every 10 s, and follow the user's When an agent sends one switch.
  7. Paths built by Ovvi. The mailbox path comes from the id that matched the pattern. The request's size is checked before it is decoded.
  8. No secrets in tokens or labels. A token holds only the id. Every device that follows the session sees tokens and labels.

In Ovvi

  • HerdrKit. HerdrClient decodes ovvi-req off pane.updated and off each snapshot's panes, and yields Update.request(id:pane:) once per id. AgentRequest reads ‹id›.req, and its Exec runs are prepareAgent (on connect), take, input (for copy) and answer, each one run.
  • The store. HostStore hears .request, waits when its user is away, takes the request, runs AgentCommands.run and answers. markShown writes both presence tokens.
  • The commands. Each command is an AgentCommand (name, usage, summary, help, run) in a file under Ovvi/Agent/, listed in AgentCommands.all. run reads its arguments off an AgentCall, throws AgentError(code:message:) (any other error answers failed) and returns AgentOutput. help writes its text from AgentCommands.all.

On this page