Login
OCX protocol v1, as tables (draft)
Login

A draft of the protocol written as data: the tables are the protocol, and the prose only explains them. It is not yet the standard; protocol v1 is. Once reviewed, it replaces that document, and the node's code becomes small engines that read these tables (KEEP.md, "Structure the data"). Section numbers here are this draft's. The words it adds are in section 13, for the maintainer to approve.

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 (protocol v1, 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.

Who sent a message is the user the server received it from (section 8 of protocol v1), never the name written in it. A message is read from the first version of its post or change: an edit is ignored.

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 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 password clones the server as the run's user into into, remembering the password clone
checkout of, into 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

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
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 last name: /root/.claude as $$workspace$$/.claude
files list of {"at", "text"} files made for it alone: at a path, text what the file holds
proc, dev, tmp paths where it sees its own /proc, its own /dev, and an empty directory for temporary files

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 $$release$$ and $$workspace$$ themselves may be named
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 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
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 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
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

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 ended ended, stopped
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, or a field has the wrong type or form the field
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) 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 and 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. Words for the maintainer to approve

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

14. An unconfined echo, and Claude in a sandbox

{"ocx": 1, "kind": "run", "node": "lab-1", "objective": "Say hello.",
 "process": {"user": "builder", "command": ["echo", "hello"], "env": {"PATH": "/usr/bin:/bin"},
             "keep": {"stdout_when": "always"}}}

The node writes started, runs echo hello in /, posts a reply holding hello, then ended, exited, exit 0.

{"ocx": 1, "kind": "run", "node": "lab-1", "objective": "Answer what is said here.",
 "workspace": [
   {"step": "directory", "at": "$$root$$/$$user$$/projects/$$project$$/$$run$$"},
   {"step": "password"},
   {"step": "clone", "into": "$$workspace$$/clone"},
   {"step": "checkout", "of": "trunk", "into": "$$workspace$$/tree"}],
 "sandbox": {"uid": 1000, "gid": 1000, "capabilities": [], "network": true, "own_session": true,
             "ends_with_node": true,
             "read_only": ["/usr", "/bin", "/lib", "/lib64", "/etc/resolv.conf", "/etc/ssl/certs", "$$release$$"],
             "read_write": ["$$workspace$$"],
             "homes": ["/root/.claude", "/root/.claude.json"],
             "files": [{"at": "/etc/passwd", "text": "$$user$$:x:1000:1000::$$workspace$$:/bin/sh\n"}],
             "proc": "/proc", "dev": "/dev", "tmp": "/tmp"},
 "limits": {"wall": "8h"},
 "process": {"user": "claude-1", "command": ["claude", "--remote-control"], "cwd": "$$tree$$",
             "session": true, "hears": ["*"], "terminal": true,
             "env": {"HOME": "$$workspace$$", "USER": "$$user$$", "PATH": "$$release$$:/usr/local/bin:/usr/bin:/bin",
                     "IS_SANDBOX": "1"}}}

A pattern, by convention the wiki page run/NAME, holds such fields once; create writes them into each run in full.

Open