Login
OCX protocol v1
Login

OCX extends Fossil, so people and programs can ask a node to run a process, talk with it, learn when it ends, and end it. This is the protocol, written as data: the tables are the protocol, and the prose only explains them, so a node's code is small engines that read them (KEEP.md, "Structure the data"). What it rests on, Fossil as the medium, who sent an artifact, nodes and versions, is Fossil, senders and nodes. The maintainer promoted it on 2026-10-13 (HISTORY.md decisions 44 and 46), and it is stable: section 15 says what may be added and what never changes.

What it is for

Each row is a problem we have, what version 1 does about it, and the virtue of README.md it serves.

Problem What version 1 does Virtue
Work must run on machines no one can reach from outside, such as a build machine or a lab board behind its own network a node pulls runs from the server and pushes its reports; nothing listens for a connection, and no port is opened Loose Coupling
Afterwards, no one can say what ran, who asked for it, what it was given and how it ended every run, stop, report, delivery and kept log is an artifact in the repository, its sender the user the server received it from Demonstrability
An agent or program must not hold the keys that could change what it is allowed only a user holding admin or setup can send a run; a run's user is never one, and a node's user holds no capability on the server Clarity
What a process may see and use must be known before it starts, not learned by watching it the run states its sandbox and limits in full; the node adds nothing, records the values and limits it applied in started, and refuses what it cannot provide Clarity, Parsimony
Asking for work must not depend on one program or on being online a message is a JSON block in a forum post or ticket change; Fossil's web pages, any program, or a clone offline (a ticket change, or OCX's post) can write one, and OCX's tools are a convenience, but for starting a manual run at its node Loose Coupling
Talking with a running agent must not need a connection to it a session is given each post, ticket change or check-in it subscribes to, a line of JSON on its standard input; it answers by posting Loose Coupling, Parsimony
A run must never be run twice, nor end unanswered a node writes started before it launches, so it launches at most once; every run it acts on gets one ended, and one its going down left is answered lost when it starts again Demonstrability
The node must be replaceable, and readable by people and machines the protocol is these tables; a node is small engines that read them, and another can be written from these pages Loose Coupling, Clarity
Each moving part is another way to fail no daemon: a node acts when artifacts arrive, from the server's hook or a pull, a watcher lives only as long as its run, and the node keeps on its host only what its runs need (section 4) Parsimony

An OCX message is a JSON object in a fenced block of a forum post or a ticket change. A run's message is built in layers, like a network packet, and a node processes them outer to inner, in the order of table 1.

Notation: {"a", "b"} stands for a JSON object with the keys a and b, whose values the table beside it gives. "The server's order" is the order the server received artifacts in: by its receipt's number, then by the artifact's row number (Fossil, senders and nodes, section 1).

1. Layers

# Layer Key Answers Section
1 Envelope ocx, kind, node, and run in every kind but run which version, what kind, for which node, about which run 3
2 Workspace workspace the files the process works in, made step by step 5
3 Sandbox sandbox what the process sees and may do 6
4 Limits limits how much the process may use 7
5 Process process what to run, as whom, when it starts, what it is given 8

A run without workspace gets none, one without sandbox runs unconfined, and one without limits runs unlimited. A node never runs a run with less than it asks: what it cannot provide it refuses (section 11).

2. Values

Not a layer: how a string names something the node knows, written $$name$$. The node sets the values of this table before it processes the workspace, from the run itself and from the node; a workspace step sets more (section 5), for the steps, layers and process after it. started lists every value set, as it was.

Name Value
root the directory under which the node makes its runs' workspaces
project the server's repository's name: the last part of the server's URL, or its file's name without extension
user the run's process.user
run the run's hash, in full
node the node user
server the server's URL, from which a run's clone is made
release the directory of the OCX release the node runs, whose programs a process may use

How a string names a value:

Written Read as
$$, a name of letters, digits or _, then $$ the value of that name; a run naming a value not set by then is refused (section 11, row 3)
everything else itself, $ and { included, so a shell's ${HOME} or $$ reach the process as written

Values are read in every string of workspace, sandbox and process, at any depth, but process.user, process.deliveries, process.hears and process.checkins, and never in an object's keys. The node checks every value a run names when the run arrives, before any step, knowing which value each step sets: a run naming a value that nothing sets before it is refused then, and nothing is made. A default (section 8) is read the same way, but a default that names a value not set is left out instead of refused.

3. Message kinds

Kind Sent by Written where Its fields Answered by
run a user holding admin or setup anywhere: a new thread, a reply, a ticket change ocx, kind, node, objective, and the layers of table 1 started then ended, or ended alone
stop a user holding admin or setup where its run is ocx, kind, node, run its run's ended
started the node, as its node user where its run is ocx, kind, node, run, values, limits none
ended the node, as its node user where its run is ocx, kind, node, run, outcome, exit, note (table 12) none

objective (text, required) says what the run is for, and what counts as passing. run, in every kind but run, is the run's hash in full: the hash of the forum post or ticket change artifact that holds the run.

Who sent a message is the user the server received it from (Fossil, senders and nodes, section 3), never the name written in it. A post's edit is an artifact of its own: an edit holding a message is a new message, asking again, its hash the edit's.

A message that Is
is a run sent by anyone the table does not name answered ended, refused
is a stop, started or ended sent by anyone the table does not name, or written anywhere else ignored
has a kind the reader does not know ignored
is a second started or ended for one run ignored: the first, in the server's order, counts

4. What the node keeps

The repository holds every message; the node keeps, on its own host, only what a running process needs, and nothing a later reader depends on.

Kept For
which runs it has in its care: from just before the workspace is made until ended is written so it acts on each run once, and finds what its going down left (section 10)
what each session in its care has been given so it gives each artifact at most once
the deliveries waiting for a session whose deliveries is start so each process gets one
its file of passwords, on a node that is not the server's the run users it may run as (section 5, password)

5. The workspace layer

The workspace is a list of steps, run in order. A step is an object naming its kind in step, with that kind's arguments, which may name values (section 2). A step that fails ends the run refused, its note naming the step. The process runs as the node's own account, in the workspace the steps make.

Step Arguments Needs Does Sets
directory at, within $$root$$ none makes the directory, removing what was there, readable by the node's account alone workspace
password none none takes the run user's password: the server's node sets a fresh one (it runs one run at a time for each user); a node on another host reads it from its file of passwords none (a password is never a value)
clone into, within $$workspace$$ password clones the server as the run's user into into, remembering the password clone
checkout of; into, within $$workspace$$ clone opens a checkout of the check-in of names (any name Fossil resolves), resolved once, into into tree, and checkout: the check-in in full

"Within" means below that directory, not the directory itself, once links are followed. A directory whose at is not within $$root$$, or is or holds another run's workspace while that run is in the node's care, and a clone or checkout whose into is not within $$workspace$$, are refused when the run arrives, before any step (section 11, row 9): a step never removes or writes anything outside the run's own place.

The steps most runs take, which a pattern holds once:

"workspace": [
  {"step": "directory", "at": "$$root$$/$$user$$/projects/$$project$$/$$run$$"},
  {"step": "password"},
  {"step": "clone", "into": "$$workspace$$/clone"},
  {"step": "checkout", "of": "trunk", "into": "$$workspace$$/tree"}
]

6. The sandbox layer

What the process sees and may do. Every field is required, so the run says all of it. The node translates the fields into its confining program's arguments; on Linux, bwrap's. Every path is absolute once its values are in place, and the fields are applied in this table's order, so a later one is seen over an earlier one at the same place.

Field Type Meaning
uid, gid whole numbers the user and group the process runs as inside
capabilities list of names the Linux capabilities (not Fossil's) it keeps; empty keeps none
network true or false whether it shares the host's network; it never sees the host's processes, users, host name or message queues
own_session true or false whether it runs in a session of its own, so it cannot reach a terminal outside
ends_with_node true or false whether it ends when the node's process that watches the run ends
proc, dev, tmp paths where it sees its own /proc, its own /dev, and an empty directory for temporary files
read_only list of paths host paths it sees read-only, each where it is on the host
read_write list of paths host paths it sees read-write, each where it is on the host; a path in both lists is read-write
homes list of paths host paths it sees read-write inside the workspace, each by its base name: /root/.claude as $$workspace$$/.claude
files list of {"at", "text"} files made for it alone: at a path, text what the file holds

Nothing else of the host is visible to it. A path in read_only, read_write or homes is refused (section 11, row 9) when, once links are followed, it is or holds or lies within one of the node's own files:

The node's own files But
its repository
the directory it keeps its runs' state in
its release root, which holds its programs and every run's workspace $$workspace$$ may be named in any of the three, and $$release$$ in read_only alone, so no process changes the programs that confine it
its file of passwords

7. The limits layer

Field Type Counted from Enforced, on Linux, by Past it
wall a whole number and s, m, h or d the launch; a session's first, when each delivery starts a process the node ending the process ended, budget
cpu whole seconds the launch, for each process the kernel, signal SIGXCPU ended, budget
processes a whole number at any moment the kernel, for the sandbox's own user a new process is not started
files a size (section 8, keep) each file a process writes the kernel, signal SIGXFSZ ended, budget
memory a size (section 8, keep) at any moment a cgroup: not yet, so a node refuses it ended, budget

started lists, in limits, the limits applied, as written.

8. The process layer

Field Type Default Meaning
user text required the Fossil user the process acts as; never a node's, or one holding admin or setup
command list of text required the program, then its arguments; a program named without / is found on env's PATH
env object of text empty the process's whole environment; the node adds nothing
cwd text $$workspace$$, or / with no workspace where it starts
manual true or false false false: the node starts it when the run reaches the node; true, a manual run: it waits until a user holding admin or setup starts it, with OCX's run tool
session true or false false false: it runs to completion; true: it is given what it subscribes to (section 9)
hears list of users, or ["*"] for every user empty whose posts and ticket changes a session is given
checkins list of branch names empty the branches whose check-ins a session is given
deliveries feed or start feed feed: each delivery goes to the running process; start: each starts a process of its own, the same command, one at a time
terminal true or false false the process runs on a terminal the node holds, which a person may join with OCX's attach
keep {"stderr", "stdout", "stdout_when"} 64K, 64K, failure, each key on its own how much of the end of STDERR and STDOUT the node keeps; STDERR always, STDOUT on failure (an exit but 0, or a signal) or always

A size is a whole number of bytes, or text: a whole number and K, M or G, for 1024, 1024² or 1024³ bytes.

8.1 Streams

Stream Without a terminal With a terminal
STDIN the line {"run", "objective"}, run the run's hash and objective its text; then each delivery, one line each, for a session that feeds; otherwise closed the terminal: the first line and each delivery typed in, and what a person joined types
STDOUT a pipe the node reads the terminal, which a person joined sees
STDERR a pipe the node reads a pipe of its own, which the node copies to the terminal

A process a delivery starts reads the line {"run", "objective"}, then the delivery's line, then its input closes; with a terminal, each such process has a terminal of its own.

8.2 Kept logs and where reports go

When The node writes As
a process exits, and a stream it keeps printed at least one byte $$workspace$$/stderr.log and $$workspace$$/stdout.log, when the run has a workspace files, rewritten for each process
a process exits, and a stream it keeps printed at least one byte a reply where the run is: STDERR's and STDOUT's ends, each in a fenced block headed stderr or stdout, and its exit a plain post or ticket change, holding no OCX message
the run ends ended a reply in the run's thread, or a change to its ticket

Where a run is, a forum thread or a ticket, decides only where its reports and kept logs are written and what they point to.

9. Subscriptions

What a session is given. An artifact that arrives is checked against these filters in order, and given only if it passes every one, at most once, in the server's order.

# Filter Passes when
1 session the run's process.session is true, and its started counts (section 3)
2 after the server received it after the run's started
3 known the server has said who sent it
4 no message it holds no OCX message
5 not its own its sender is not the run's process.user, nor a node's user
6 where a post in the run's thread, or a change to its ticket, or a check-in on a branch in checkins
7 heard a check-in; or a post or change whose sender is in hears, or hears is ["*"]
Arrives Line, a JSON object
a post post: its hash; from: its sender; text: its text
a ticket change change: its hash; ticket: the ticket; from; text: the text it adds
a check-in checkin: its hash; branch: its branch as Fossil's timeline shows it; from; comment: its comment
The node guarantees That is
whole, once, in order each delivery reaches the session whole, at most once, one at a time, in the server's order
only what arrives a session is given what its node acts on as it arrives; one the node missed, as when its hook failed, is not given later, and the session may read its thread itself
known senders first on a node on another host, delivery stops at an artifact whose sender the server has not said, so none after it is given before it
bounded what waits for a process that does not read is held up to a bound the node sets; past it, nothing more is given until the process reads, and the node's log names what it could not give

An edit of a post is a post, and is given. To speak to one session alone, give it a thread or a ticket of its own.

10. A run's life

The run's state is read from the repository, started and ended that count, and from whether the node has the run in its care (section 4).

State What arrives Next state The node writes or does
none a run that section 11 refuses ended ended, refused, and the note
none a run, manual false started takes it into its care, runs layers 2 to 4, writes started, then launches the command
none a manual run waiting nothing
waiting a user holding admin or setup starts it started as for a run that is not manual; a refusal then ends it refused
none or waiting a stop, the run not refused by rows 1 or 2 of section 11 ended ended, stopped, even where rows 3 to 10 would refuse it
none a run whose user has another run in the node's care waiting nothing, until that run ends
started a delivery (section 9), deliveries feed started the delivery to its STDIN
started a delivery, deliveries start started a process for it, once the one before has exited; deliveries wait in the server's order
started a stop started ends the process
started its process exits; not a session whose deliveries is start ended kept logs, then ended: exited, budget or stopped (table 12)
started a process exits, in a session whose deliveries is start started kept logs
started a stop, or past wall, in a session whose deliveries is start ended ends its process, kept logs, then ended, stopped or budget
started the node finds it not in its care ended ended, lost
ended anything ended nothing

The node finds what is not in its care when it starts, and whenever its administrator runs that check again. It writes started before it launches, so a process is launched at most once for a run and others see it began; a node on another host sends started to the server and launches only once the server has it.

11. Refusals

A run is checked against these in order, when it arrives and again when a person starts it; the first that holds ends it refused, with its note.

# Holds when Note names
1 no user holding admin or setup sent it that
2 a required field is missing, a field has the wrong type or form, or it has a key version 1 does not define (section 15) the field or key
3 it names a step kind section 5 does not have, a step whose needs are not met before it, or a value not set by then which
4 its user is the node's own, a node's (one named as the node of a run an admin or setup sent), or holds admin or setup which
5 its user works for another node: the node of the first run, in the server's order, that an admin or setup sent with that process.user, whatever became of it which node
6 it has a password step, and the node, on another host, holds no password for its user the user
7 it has no sandbox, and the node runs nothing unconfined that
8 it asks a sandbox setting or a limit the node cannot provide which
9 a sandbox path reaches the node's own files (section 6), or a workspace step's path is not where section 5 allows the path
10 a workspace step fails the step, and why

12. ended

outcome When exit note
refused section 11 absent required: plain text, as for every note
stopped a stop ended it, or came first absent absent
exited its process exited by itself its code; 128 plus N for signal N absent
budget past a limit absent required: which limit
lost the node found it not in its care absent required

13. Its words

Word Meaning here Instead of
layer one level of a run's message, table 1
envelope the fields every message carries, so any reader knows what it is
value something the node knows, named $$name$$ in a string macro
step one action of the workspace layer tool, which GLOSSARY.md gives OCX's commands
process a program running for a run the glossary's program, for what runs
in its care a run the node is preparing or running
works for a run user bound to one node by its first run
kept logs the ends of STDERR and STDOUT the node keeps and posts
manual run a run whose process waits until a person starts it when, hand
session a run whose process.session is true, given deliveries (section 9)
node user, run user the Fossil user a node writes as; the Fossil user a run's process acts as login, agent

14. Examples

Whole messages, each with what a node writes back, are protocol v1's examples.

15. Stability

Every message carries "ocx": 1. Version 1 grows only by addition, and a reader can tell what it does not know.

Change Within version 1
a new key, in any layer or kind, whose absence means what version 1 says without it allowed
a new message kind, a step kind (section 5), a value name (section 2), an outcome of ended, a refusal row allowed
renaming a key, changing its meaning, its type or its default, or removing it never: that is version 2
a node that does not know a key in a run refuses the run, naming the key (section 11, row 2), so nothing asked is silently dropped
a reader that does not know a key in a stop, started or ended ignores the key
a reader that does not know a kind ignores the message, so a new kind asks nothing of an older node
a reader that does not know an outcome of ended takes the run as ended

A post may carry a block for each version; a reader takes the block of the newest version it knows (Fossil, senders and nodes, section 2). These rules bind version 1 from its promotion on 2026-10-13: no version 1 message was written on the server before it.

Open: possible additions, none changing version 1