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
| Step | Who | What |
|---|---|---|
| 1. Write | the command | makes an id, then writes ‹id›.req (to ‹id›.tmp, then mv). For copy it writes its standard input to ‹id›.in first |
| 2. Ring | the command | herdr pane report-metadata ‹pane› --source ovvi-cli --token ovvi-req=‹id› --ttl-ms 3000 on the asking pane |
| 3. Hear | each Ovvi that follows the session | the pane.updated event carries the pane and its tokens; Ovvi acts once per id |
| 4. Wait | an Ovvi whose user is away | 500 ms, so that an Ovvi in use takes it first (Two Ovvis) |
| 5. Take | the Ovvi | one 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. Do | the Ovvi | parses the argv, checks it (Security), acts. copy reads ‹id›.in with one more run |
| 7. Answer | the Ovvi | one run writes ‹id›.out, then ‹id›.exit (each to a temporary name, then mv), and removes ‹id›.run and ‹id›.in |
| 8. Read | the command | prints ‹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›.reqmust 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›.deaditself. Onemvwins, so an Ovvi that is late finds nothing. The command removes its files and exits 3. - Answered:
‹id›.exitmust 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 65537Ids
- A request id is a lowercase UUID version 4 that the command makes, from
/proc/sys/kernel/random/uuidoruuidgen. 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.updatedevent 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 ␀| Field | Value |
|---|---|
| 1 | ovvi-req |
| 2 | the protocol version, 1 |
| 3 | the 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›.inholds 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,--timeoutandstatusare the command's own and are not sent. A--jsonbefore 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 isprotocol.
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 defaultWith --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
| Code | Meaning | Who |
|---|---|---|
| 0 | done | Ovvi |
| 1 | the command failed; the error says why | Ovvi |
| 2 | usage: an unknown command, a bad option, a missing or extra argument | Ovvi, or the script for its own options |
| 3 | no Ovvi took the request within 3 s, or (ovvi status) no Ovvi follows the session | the script |
| 4 | an Ovvi took it but did not answer within the timeout | the script |
| 5 | not 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 written | the 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.
| Code | Exit | When |
|---|---|---|
usage | 2 | the 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_command | 2 | a command this Ovvi does not have (ovvi help lists them) |
protocol | 2 | the request's version is newer than this Ovvi speaks |
pane_not_found | 1 | a pane argument: no such pane, or no such session running on the asking pane's machine |
plugin_not_found | 1 | no browser or plugin with that id; with the reason when its manifest did not load |
bad_argument | 1 | the 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_machine | 1 | plugins new, reload or install from a pane on a remote machine |
not_floating | 1 | unfloat of a pane that does not float |
busy | 1 | chat while the agent works or needs you, or while a relaunch runs |
self | 1 | chat on the asking pane |
rate_limited | 1 | a second notify from the asking pane within 10 s |
dirty | 1 | open --replace of a card with unsaved edits; plugins reload while a card of the plugin has them (the message names the pane) |
declined | 1 | the user pressed Cancel in the confirmation of plugins install |
build_failed | 1 | plugins reload: the build failed; the message ends with the build's last 20 lines |
failed | 1 | anything 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_ovvi | 3 | no Ovvi took the request within 3 s |
timeout | 4 | an Ovvi took it but did not answer within the timeout |
no_herdr | 5 | no 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, inovvi-openand in the script's second line. It changes only when the files, the token or the exit codes change. The format of‹id›.exitnever changes, so any Ovvi can answerprotocolto 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 --jsonlistscommands, 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
mvon 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:
- 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 ispane_not_foundorbad_argument. - 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.
copyonly writes it.browser ./file.htmlopens a file only for a pane on This Mac. - Plugins change only from This Mac.
plugins new,reloadandinstallfrom a remote pane areother_machine. An install shows the confirmation, and Return does not press Install. - Pages load by the browser's rule.
ovvi open browser https://example.commakes the card. A site that is not local and not approved shows Open this page? until the user clicks (Browser). - 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.
- Notifications are limited to one per asking pane every 10 s, and follow the user's When an agent sends one switch.
- 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.
- 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.
HerdrClientdecodesovvi-reqoffpane.updatedand off each snapshot's panes, and yieldsUpdate.request(id:pane:)once per id.AgentRequestreads‹id›.req, and itsExecruns areprepareAgent(on connect),take,input(forcopy) andanswer, each one run. - The store.
HostStorehears.request, waits when its user is away, takes the request, runsAgentCommands.runand answers.markShownwrites both presence tokens. - The commands. Each command is an
AgentCommand(name,usage,summary,help,run) in a file underOvvi/Agent/, listed inAgentCommands.all.runreads its arguments off anAgentCall, throwsAgentError(code:message:)(any other error answersfailed) and returnsAgentOutput.helpwrites its text fromAgentCommands.all.