This defines the messages this tree reads and writes. Fossil, senders and acting clones are described in Fossil; the tools, verdicts, failures and examples complete the definition. The glossary defines the words.
Version 2's completion and republication are built. The watcher writes
ended before publishing; an explicit publish retries unfinished
publications without launching the process.
2.1 Messages, and how they are found
An OCX message is a JSON object in a fenced block of a forum post of any
mimetype, or in the text a ticket change adds to its ticket's comment (its
icomment field). The fence is paired as CommonMark pairs it. Its opening is three
backticks alone, or three backticks then json, as OCX writes it; up to three leading spaces, and
spaces around json, are accepted; any other opening holds no message. Other text may come
before or after it and is ignored; keep it short. A block holds an OCX
message when its JSON repeats no key, has "ocx": 2, and has exactly one
key besides the envelope's own (ocx, to, re), and that key's value is
an object: the key names the message's kind, and its value is the content.
A block with no key besides the envelope's, more than one, or one whose
value is not an object holds no message. Keys inside the content, whatever
their names, are content: a to or ocx there is not the envelope's. A post's or change's message is its first such block. Actors read no version 1 block: stop every
open version 1 run before switching. Historical verdict and question readers
retain their evidence only; see verdicts.
The envelope, layer 2:
| Key | Holds | In |
|---|---|---|
ocx |
2 |
every message |
to |
the login of the user asked to act | where its kind asks for it (2.3) |
re |
the full hash of the message it answers: the forum post or ticket change holding it | where its kind asks for it (2.3) |
| one other key | the kind, its value an object: the content, which layer 2 does not read | every message |
Layer 2 reads every message the same way, of whatever kind. OCX's own kinds are those of 2.3; a message of another kind is carried, addressed and answered by the same envelope, for whichever reader it is meant for (examples).
An artifact never changes. An edit of a post holding a message is a new
message, named by the edit's own hash; the one edited stands until it is
stopped. Several runs may share a thread or a ticket; each is answered on
its own, by re.
Notation: {"a", "b"} is a JSON object with the keys a and b. "The
server's order" is the order the server received artifacts in: by its
receipt's number, then by the artifact's row number.
2.2 Who sent it
Authority comes from the server's receipts, not an artifact's user card.
Each daemon and watcher asks the server's receipt tool. Until it answers,
they decide nothing from that artifact. See senders.
2.3 The kinds: content, layer 3
| Kind | Envelope | Sent by | Content | Answered by |
|---|---|---|---|---|
run |
to, required |
a user holding admin or setup; or a user holding R, with the isolation refusal 1 asks (2.14) |
clone, objective, of, workspace, sandbox, limits, process |
started, ended, then publications; or ended alone |
stop |
re: the run, required |
a user holding admin or setup; or the user holding R that sent the run |
{} |
the run's ended |
publish |
to: the clone login, and re: the run, required |
the run's sender or a user holding admin or setup | {} |
unfinished publications, or a reply saying why not |
started |
re: the run, required |
OCX on the run's clone, as the run's to |
values, limits |
none |
ended |
re: the run, required |
OCX on the run's clone, as the run's to |
outcome, exit, note, workspace, outputs |
none |
| Key | In | Holds | Required |
|---|---|---|---|
clone |
run |
the server-code of the repository that is to run it, all 40 lower-case hex digits (2.4) | yes |
objective |
run |
text: what the run is for, and what counts as passing | yes |
of |
run |
a check-in name resolved once; sets checkin without a workspace step |
no |
workspace |
run |
a list of steps (2.7); absent, the run gets no workspace | no |
sandbox |
run |
every required key of 2.8; absent, refused unless the host has ROOT/settings/unconfined (refusal 5) |
no |
limits |
run |
the keys of 2.9 it sets; absent, unlimited | no |
process |
run |
the keys of 2.10 | yes |
values |
started |
every value set (2.6), as set; passwords are absent | yes |
limits |
started |
the limits applied, as written | yes |
outcome, exit, note |
ended |
2.12 | as 2.12 says |
ended may also hold workspace: removed for an unlaunched run, or kept: and why, for a
run whose directory step asks removal once saved (publications pending after launch). Unknown report keys are
ignored; an unfamiliar text outcome still ends the run. A stop's content
and a publish must be empty: an unknown key makes either unreadable.
ended.outputs records each unversioned file's expanded as and SHA-256
hash, or null for a missing or forbidden file. The counted started
records the check-in in values.checkin. Ended describes the process alone.
Logs, files and verdict follow it; publication failure is a separate reply
and changes nothing in ended. A durable plan retains unfinished publications.
A publish addressed to the clone's login, naming the run in re, retries
that plan from the kept workspace. It never launches a process. Files must
still match the recorded hashes and stay within the workspace; files and
verdicts are refused for stopped, lost or refused outcomes. A publish while
the watcher holds the run lock does nothing and says so. Workspace removal
waits until every publication has reached the server.
2.4 Who acts and whose answers count
Both to and the full clone must match the acting repository. A missing,
malformed or different address is passed by, with no refusal posted. Reports
answer the run by re and count only on its thread or ticket, received from its to.
A counted started also requires a run sent by a different user holding
admin, setup or R; a refusal's ended needs no such sender.
See acting clones for the whole rule.
2.5 The two users
The clone's user writes OCX's reports. The process's user acts in the
process's separate clone. Neither holds admin or setup on the server;
the process's user must be no clone's user. create approves its password,
then the password step collects it; a run never resets one. See
users and passwords.
2.6 Values
A string names something OCX knows as $$name$$: $$, a name of letters,
digits or _ (or secret: and a secret's name), then $$. Everything else is itself, $ and {
included, so a shell's ${HOME} reaches the process as written. Values are
read in every string of workspace, sandbox and process, at any depth,
except sandbox.workspace_at, process.user, process.deliveries, process.hears and
process.checkins, and never in clone or in any object's keys. OCX checks
every value a run names when the run arrives, before any step: a run naming a
value that nothing sets before it is refused then (refusal 3), and nothing
is made. The default cwd is the workspace when set, otherwise /.
| Value | Set by | Holds |
|---|---|---|
root |
OCX | the directory under which OCX makes runs' workspaces |
repository_name |
OCX | the server's repository's name: the last part of the server's URL |
server_url |
OCX | the server's URL, from which the process's clone is made |
run |
OCX | the run's hash, in full |
clone_user |
OCX | the run's to, the clone's user |
process_user |
OCX | the run's process.user |
releases |
OCX | the release root; its staged/ directory may be bound read-write |
release |
OCX | the directory of the OCX release that acts, which a process may read and run |
secret:NAME |
OCX | the server's repository's secret NAME; filled in process.env alone when available, and redacted in kept logs |
workspace |
step directory |
the workspace's directory |
process_clone |
step clone |
the path of the process's clone |
tree |
step checkout or files |
the directory holding the check-in's files |
checkin |
run's of, step checkout or files |
the check-in's hash, in full, resolved once |
The password is held privately between steps: it is never a value,
never in started.values, and $$password$$ is refused as unset.
Secrets are available only to an actor that can read them; the daemon's
remote reader supplies none.
2.7 workspace: a list of steps
Each step is {"step": KIND, ...}. Steps run in order. A step that fails
ends the run refused, its note naming the step. The process runs as OCX's
host account, inside its sandbox, in the workspace the steps make.
| Step | Keys | Needs | Does | Sets |
|---|---|---|---|---|
directory |
at, within $$root$$ |
none | makes the directory, removing what was there, readable by OCX's host account alone | workspace |
password |
none | none | collects the process's user's approved password; changes no password; holds it privately for clone |
no value |
clone |
into, within $$workspace$$ |
password |
makes the process’s clone from $$server_url$$, signed in as the process’s user with the privately held password, remembering it at into |
process_clone |
checkout |
of, into within $$workspace$$ |
clone |
opens a checkout, from the process's clone, of the check-in of names (any name Fossil resolves), resolved once |
tree, checkin |
files |
of, into within $$workspace$$ |
none | writes the files of the check-in of names, resolved once in the run's clone, each executable as the check-in marks it, and nothing else: no repository, no password, so the process signs in as no one |
tree, checkin |
The clone step has no password key. The password step collects through
collect, signed in as the approved clone's user, and hands the password
privately to clone. It sets no value. Current server approvals take
precedence over the older local runs map; see passwords.
A failed collection fails the step (refusal 9). A run from R alone takes
no password step (refusal 1).
"Within" means below that directory, not the directory itself, once links and
.. are resolved. A directory whose at is not within $$root$$, or is or
holds another run's workspace in OCX's care, and a clone, checkout or
files whose into is not within $$workspace$$, are refused when the run
arrives, before any step (refusal 7).
Removal once saved requires a sandbox with ends_with_parent: true.
The directory step also takes remove_on: "saved" (remove is read as
its older name). Once the process and its children have ended, OCX removes
the workspace only if its clone has sent every artifact and its checkout
has no change, extra file or stash. Otherwise it keeps it and says why in
ended.workspace. Without removal requested, it keeps the workspace.
2.8 sandbox
Every key except workspace_at and devices is required, so the run says all of it. On Linux, OCX passes the
keys to bubblewrap (bwrap), applied in this table's order, so a later one
is seen over an earlier one at the same place.
| Key | Type | Meaning |
|---|---|---|
uid, gid |
whole numbers | the Linux account and group IDs the process runs as inside |
linux_capabilities |
list of names | the Linux capabilities it keeps; empty keeps none |
network |
true or false | whether it shares the host's network; it never sees the host's processes, accounts, host name or message queues |
isolate_terminal |
true or false | whether it is kept from every terminal outside the sandbox, in a Linux session of its own (bwrap --new-session) |
ends_with_parent |
true or false | whether it ends when its watcher ends (bwrap --die-with-parent) |
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; a path in both lists is read-write |
devices |
list of paths, optional | host devices it may open, each where it is on the host (bwrap --dev-bind-try), which a read_write path, mounted nodev, forbids; each lies within /dev, with no . or .. part and no / repeated or at its end; absent, none |
homes |
list of paths | host paths it sees read-write inside the workspace, by base name: /root/.claude as $$workspace$$/.claude |
workspace_at |
absolute normalized path other than /, optional |
where the process sees its workspace; absent, the host path; names no value |
created_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 (refusal 7) when, once links are
followed, it is, holds or lies within one of OCX's own files: the run's
clone, ROOT/state/, ROOT/settings/, the release root (which holds OCX's
programs and every workspace), or the passwords file. But $$workspace$$ may
be named in any of the three, $$release$$ in read_only, and $$releases$$/staged read-write.
2.9 limits
| Key | Type | Counted from | Enforced on Linux by | Past it |
|---|---|---|---|---|
wall |
a whole number and s, m, h or d |
the launch; with deliveries start, the first |
timeout, then OCX ending its children |
ended, budget |
cpu |
whole seconds | the launch, for each process | the kernel, SIGXCPU | ended, budget |
processes |
a whole number | any moment | the kernel, for the sandbox's own account | no new process starts |
file_size |
a size | each file a process writes | the kernel, SIGXFSZ | ended, budget |
memory |
a size | any moment | not yet provided, so refused (refusal 6) | ended, budget |
A size is a whole number of bytes, or a whole number and K, M or G for 1024, 1024² or 1024³ bytes.
A size above 9223372036854775807 bytes is refused as the run's fault.
A processes limit without a sandbox is refused.
2.10 process
| Key | Type | Default | Meaning |
|---|---|---|---|
user |
a login | required | the process's user (2.5) |
command |
list of text | required | the program, then its arguments; a program without / is found on env's PATH |
env |
object of text | empty | the process's whole environment; OCX adds nothing |
cwd |
text | $$workspace$$, or / with no workspace |
where it starts |
manual |
true or false | false | true, a manual run: it waits for a person with local setup to start it with OCX's run |
session |
true or false | false | false: it runs to completion; true: it is given what it subscribes to (2.11) |
hears |
list of logins, 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 | it runs on a terminal OCX holds, which a person may join with OCX's attach |
ends |
true or false | false | a session also receives counted ended reports where it is |
keep |
{"stderr", "stdout", "stdout_when"} |
64K, 64K, failure, each on its own |
how much of the end of STDERR and STDOUT OCX keeps; STDERR always, STDOUT on failure (an exit but 0, or a signal) or always |
verdict |
true or false | false | when its process has run and was not stopped, OCX writes a ci verdict (2.16) on $$checkin$$, as the clone's user, against the run: pass for exit 0, else fail and how it ended. Refused when neither of nor a step sets checkin (refusal 3), and in a session (refusal 2) |
unversioned_files |
list of {"at", "as"} |
none | when its process has run and was not stopped, OCX sends each file at, within $$workspace$$, as the unversioned file as, as the clone's user: only a regular file, never a link nor one leading out of the workspace; on a clone's host, only if the clone's user holds y on the server. Then a reply where the run is names each, with its hash, or why not. Never for a session whose deliveries is start |
Streams:
| Stream | Without a terminal | With a terminal |
|---|---|---|
| STDIN | the line {"run", "objective"}; then, for a session that feeds, each delivery, a line each; otherwise closed |
the terminal: the first line and each delivery typed in, and what a person joined types |
| STDOUT | a pipe OCX reads | the terminal, which a person joined sees |
| STDERR | a pipe OCX reads | a pipe of its own, which OCX copies to the terminal |
With deliveries start, each process reads the first line, then its
delivery's line, then its input closes; with a terminal, each has a terminal
of its own.
Kept logs and where things go:
| When | OCX writes | As |
|---|---|---|
| the process is over | ended, then logs, files and verdict (2.3) |
a reply in the run's thread, or a change to its ticket |
| a process exits, and a stream it keeps printed a byte | $$workspace$$/stderr.log, $$workspace$$/stdout.log, when the run has a workspace |
files, rewritten for each process |
| the same | a reply where the run is: each stream's end in a fenced block headed stderr or stdout, and the exit |
a plain post or ticket change, holding no OCX message, as the clone's user |
its process has run, was not stopped, and the run names unversioned_files |
the unversioned files, then a reply naming each | as the clone's user |
its process has run, was not stopped, and the run asks a verdict |
a ci tag on $$checkin$$ |
a raw tag, as the clone's user |
Reports count only where the run was written. Actor prose quotes any embedded
OCX message, including one in hears, a shared as name or a failure note.
2.11 Subscriptions
What a session is given. An artifact that arrives is given only if it passes every filter, at most once, in the server's order.
| # | Filter | Passes when |
|---|---|---|
| 1 | known | the server has said who sent it |
| 2 | after | the server received it after the run |
| 3 | no message | it holds no OCX message, or is a question or decision; or, with ends, a counted ended where the session is |
| 4 | not its own | its sender is not the process's user |
| 5 | where | a post in the run's thread, a change to its ticket, or a check-in on a branch in checkins |
| 6 | heard | a check-in or counted ended; or a post or change whose sender is in hears, or hears is ["*"] |
| Arrives | Line |
|---|---|
| a post | {"post": HASH, "from": LOGIN, "conversation": NAME, "text": TEXT}, NAME the raw tag console-name on the thread's first post as it is then, the field absent when it has none |
| a ticket change | {"change": HASH, "ticket": TICKET-ID, "from": LOGIN, "text": TEXT} |
| a check-in | {"checkin": HASH, "branch": BRANCH, "from": LOGIN, "comment": TEXT}, the branch as Fossil's timeline shows it |
A watcher reads the thread, ticket and named branches through each completed pull, in server receipt order. It remembers deliveries, so an arrival is given at most once. A failed receipt request gives nothing from that read; it tries again at the next completion. An artifact the server has not received is skipped while later received ones may be given; if received later it may be given then. It does not block those after it. What waits for a process is bounded by its pipe; a line that does not fit may be dropped.
2.12 stop, and the outcomes of ended
A stop names no clone: OCX on the run's clone acts on it, from the senders of 2.3 alone; others are ignored.
outcome |
When | exit |
note |
|---|---|---|---|
refused |
a refusal of 2.14 holds | absent | required: which, in plain text |
stopped |
a stop ended it, or came first | absent | absent |
exited |
its process ended, by itself or by a signal, with no stop or limit | its code, or 128 plus N when signal N ended it, as a shell writes it: an exit above 128 does not say which |
absent |
budget |
past a limit | absent | required: which limit |
lost |
OCX on the run's clone found its counted started but not the run in its care |
absent | required: why |
A reader that does not know an outcome takes the run as ended.
2.13 A run's life
The daemon pulls each clone whole, publishes its completion number, and
hands unsettled runs to detached watchers. On restart it reads all runs;
a waiting run is reconsidered at each completed pull. Chat wakes the daemon
but is never a record of work. Nothing acts on the server's repository.
At start, clone-listing sync is bounded to ten seconds; failure is logged as listing_failure in state/daemon.log and serving continues.
Each round's passed lists only newly read runs. Misplaced stops get a reply naming where to post them.
A watcher takes the run lock and decides again under round.lock. A run
for another address is ignored. Unknown receipts wait. A counted ended
settles it; a launched run no longer in care is lost. Before launch,
refusals 1 and 2 come before a stop; a stop comes before later refusals.
A manual run waits for run. Runs from R alone wait while another such
run is in care. There is no per-user lock: runs of one process's user may
run together, on one or several hosts.
Before posting started, the watcher writes durable taken state.
It launches only after the post is confirmed. It ends the process and what
it left for a stop, a limit or the process's own exit. It posts kept logs,
shares files and writes a verdict when allowed, handles removal once saved,
then holds the exact ended in ending state until confirmed and writes
done. A session with deliveries: start stays open between processes.
A free run lock with taken state means recovery ends the old processes
and posts lost, never launches again. ending retries the held report;
done does nothing. A missing post redirect leaves its outcome unknown:
at another completion, sender, text and reply target are checked before a
retry. A stop confirmed while started is unknown prevents launch.
A watcher failure after started ends the process, keeps a requested
removal unread, logs the failure and writes lost.
2.14 Refusals
Checked in order when a run arrives, and again when a person starts a manual
run; the first that holds ends the run refused, written as the clone's
user, so the refusal counts.
| # | Holds when | Note names |
|---|---|---|
| 1 | its sender holds none of admin, setup and R on the server; or the run's to sent it; or a user holding R alone sent it and it has no sandbox, ends_with_parent false, devices, a path with .., a directory whose path does not end in $$run$$, or an unversioned name without $$run$$ (as Values.named reads whole names) |
which |
| 2 | a required key is missing, a key has the wrong type or form, or a key version 2 does not define, including retired version 1 keys | the key |
| 3 | a step kind 2.7 does not have, a step whose needs are not met before it, or a value not set by then | which |
| 4 | process.user is the run's to, any login ever named as a run's to, any login holding all node letters goC4w, or holds admin or setup on the server |
which |
| 5 | no sandbox, and the host has no ROOT/settings/unconfined |
that |
| 6 | a sandbox key or limit OCX cannot provide | which |
| 7 | a path outside where 2.7 and 2.8 allow | the path |
| 8 | the clone's user lacks i for a requested verdict |
that |
| 9 | a workspace step fails, collect refusing included |
the step, and why |
A run written straight into a clone reaches the server as received from that clone's user, so refusal 1 holds: send runs to the server.
2.15 What OCX keeps
The record is in Fossil. Host state, settings and published clone addresses are listed in Fossil and clones. Workspaces remain unless removal once saved was requested. Per-run durable state prevents a second launch; a completion number bounds a whole pull. Completion and republication are built (2.3). Publication state retains the workspace until all publications have reached the server.
2.16 Verdicts
Verdicts defines writing and counting them.
2.17 Stability
| Change | Within version 2 |
|---|---|
| a new key, in any kind, whose absence means what version 2 says without it | allowed |
| a new message kind, step kind, value, outcome or refusal row | allowed |
| renaming a key, changing its meaning, type or default, or removing it | never: that is version 3 |
OCX meeting a key it does not know in a run |
refuses the run, naming the key (refusal 2) |
a reader meeting a key it does not know in a started or ended |
ignores the key |
| a reader meeting a kind it does not read | passes it by: it is for another reader (2.0) |
A stop with any content key is ignored. Actors read only version 2. Stop open version 1 runs before switching. Server tool answers tolerate added fields, including fields nested in their results, while checking the required fields and answers actually requested.
2.18 Words and examples
A node is a host where OCX's daemon acts on runs, named by its hostname, such as x86-1 or aarch64-1. It is never a login, key, value or directory. The glossary gives the other words; the examples show whole messages.