Written against the hook and poll design, which the daemon replaced
(How a post reaches an agent); what it says
of today's code is not current.
The second part of the draft of the proposal, awaiting the maintainer; nothing of it is built.
2.8 sandbox
Every key 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 NEW TERM |
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 NEW TERM |
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 NEW TERM |
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 |
homes |
list of paths | host paths it sees read-write inside the workspace, by base name: /root/.claude as $$workspace$$/.claude |
created_files NEW TERM |
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/care/, 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, and $$release$$ in read_only alone.
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 |
OCX ending the process | 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 NEW TERM |
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.
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 until a user holding admin or setup starts it with OCX's start |
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 |
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 with no step setting checkin (refusal 3), and in a session (refusal 2) |
unversioned_files NEW TERM |
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 |
|---|---|---|
| a process exits, and the run is over | ended, first (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 |
Where a run is, a thread or a ticket, decides only where its reports and kept logs go.
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 | session | the run's process.session is true, and its started counts (2.4) |
| 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 neither the process's user nor any clone's user |
| 6 | where | a post in the run's thread, 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 post | {"post": HASH, "from": LOGIN, "text": TEXT} |
| 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 |
OCX gives an artifact only as it acts on its arrival: on the server's
repository when the hook is called, on a clone at the poll whose pull
brought it. One missed, as when the hook failed, is never given later; the
session may read its thread itself. On a clone's host, delivery stops at an
artifact whose sender the server has not said, so none after it is given
first. What waits for a process that does not read is held up to a bound;
past it, nothing more is given until it reads, and OCX's log names what it
could not give. An edit of a post is a post, and is given.
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 exited by itself | its code; 128 plus N for signal N | 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
A run's state is read from the repository (its started and ended that
count) and from whether OCX on the run's clone has it in its care.
| State | What arrives | Next | OCX writes or does |
|---|---|---|---|
| none | a run a refusal holds for | ended | ended, refused, and the note |
| none | a run, manual false |
started | takes it into its care, makes its workspace, writes started, launches the command |
| none | a manual run | waiting | nothing |
| waiting | a user holding admin or setup starts it with start |
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 refusal 1 or 2 | ended | ended, stopped, even where refusals 3 to 8 would hold |
| started | a delivery, deliveries feed |
started | the delivery to its STDIN |
| started | a delivery, deliveries start |
started | a process for it, once the one before exited |
| started | a stop | started | ends the process |
| started | its process exits, deliveries not start |
ended | ended: exited, budget or stopped; then kept logs and publications |
| started | a process exits, deliveries start |
started | kept logs |
| started | a stop, or past wall, deliveries start |
ended | ends its process, ended: stopped or budget; then kept logs |
| started | OCX on the run's clone finds it not in its care | ended | ended, lost |
| ended | a publish, from a sender 2.3 allows, the run's workspace kept |
ended | publishes again from the workspace what the run names, without launching anything |
| ended | anything else | ended | nothing |
OCX writes started before it launches, so it launches at most once; on a
clone's host, only once the server has received the started. Every run it
acts on gets one ended. When OCX starts on a repository, and whenever a
person with the host's command line asks (hook --login LOGIN on the
server's host, poll --all on a clone's), it reads every open run naming its
clone, with its stops and reports, and decides each by this table: one no
longer in its care is answered lost, its process ended first if it still
runs; one with no started is decided as if it had just arrived. Nothing
reads them in the background.
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 sent it, and ROOT/settings/capability-R.json allows no such run, or the run asks more than it allows: no sandbox, the network or a Linux capability not allowed, a path to read outside its own $$workspace$$ and the paths listed, a path to write outside its own $$workspace$$, a home, a secret, a password step, a path with .., a directory whose path does not end in $$run$$, or a limit the file caps left out or set above the cap |
which |
| 2 | a required key is missing, a key has the wrong type or form, or a key version 2 does not define, such as node |
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, or any clone's user (one named as the to of a run that a user holding admin or setup sent), 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 | 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 on a host
The repository holds every message. OCX keeps on the host what its runs
need, and nothing the record depends on: a run's state while it is in
OCX's care, and each run's workspace, kept after ended so what it made
can be read and published again (2.3).
| Kept | Where | For |
|---|---|---|
| the runs in its care | ROOT/care/RUN.lock, held by the run's watcher |
so it acts on each run once, and finds what its going down left |
| what each session has been given, and the deliveries waiting for it | ROOT/care/RUN.inbox |
each artifact at most once |
| the run's workspace path and watcher log | ROOT/care/RUN.workspace, ROOT/care/RUN.log |
for the watcher and status |
one call of the hook or poll deciding at a time |
ROOT/care/round.lock |
so two calls never decide one run |
| the passwords file, on a clone's host | a path the clone's owner gives poll, readable by OCX's host account alone |
the clone's user's password and the code it registered with; never a process's user's |
| each run's workspace | the path its directory step names |
kept after ended, so what the run made can be read and published again |
2.16 Verdicts
A verdict is a raw tag on one check-in, never propagating: audited for a
person's or an agent's judgement, ci for a program's. Its value is
pass: or fail:, the reason on one line, then (against X), X a run's
full hash or the path of a file stating the objective, read as it stands in
the check-in judged. Anyone may write one, with OCX's audit or
fossil tag add; it needs no run. Readers also read sym-audited and
sym-ci.
A verdict counts only if its sender neither sent the check-in nor is the user named in it, and:
audited: its sender holdsiwhen it is read, and is no clone's user (one named as thetoof a run that a user holding admin or setup sent);ci: its sender is theprocess.useror thetoof the run it names, and that run has astartedthat counts (2.4), and the check-in is the one itsstartednames invalues.checkin, or one on a branch in itscheckins.
For each sender, a reader counts its latest verdict for each tag and each
objective, by the server's order, taken from Fossil's event table (type
g); a reader on a clone asks the server with receipt. How to combine
them is the reader's choice. Cancelling a tag writes no verdict.
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 stop, 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 post may carry a block for each version; a reader takes the first block of the newest version it knows.
2.18 Version 1 parts dropped
- The key
nodein every kind, and with it "any node of the login may act" and its rules: clones sharing a login only when every run names its clone, a machine joining a login only after runs naming no clone end, any node of a login answeringlost, an older release refusingclone. cloneas an optional prefix of 10 to 40 digits: now the run'sclone, the server-code in full; the user who acts is the envelope'sto.- The key
kind: the content's own key names it. The keyrunin a stop or report: nowre. A report'sclone: who wrote it is its receipt's. endedafter publication: nowendedcomes first, publication follows in the watcher's care, and apublishmakes it again; the process never reruns.- One run at a time for each run user, and the waiting it caused: several runs of one process's user may run at once.
stop'snode.- The values
$$node$$,$$user$$,$$project$$,$$server$$,$$clone$$, and$$checkout$$as a hash. - The keys
capabilities,own_session,ends_with_nodeandfilesof the sandbox;filesof the limits;sharesof the process. - A run user working for a login, and a fresh password at each run. A password is set once, when
createmakes the user, and no run changes it, so no user is bound to a clone and no run waits on another host's. - Counting a
startedwhose run came from a user without admin, setup orR, or from the run's own clone's user. - The word "layer".
- Reading
"ocx": 1blocks at all.
2.19 Two uses, one envelope
Words in capitals stand for values: a login, a server-code (40 lower-case hex digits), a hash (40 or 64), a path or a program. Everything else is as it would be sent.
A build, from a pattern holding its content; create adds to and clone:
{
"ocx": 2,
"to": "LAB-USER",
"run": {
"clone": "SERVER-CODE-OF-THE-LAB-CLONE",
"objective": "build trunk for the lab board and send the image",
"workspace": [
{"step": "directory", "at": "$$root$$/$$process_user$$/projects/$$repository_name$$/$$run$$"},
{"step": "files", "of": "trunk", "into": "$$workspace$$/tree"}
],
"sandbox": {
"uid": 1000, "gid": 1000, "linux_capabilities": [], "network": false,
"isolate_terminal": true, "ends_with_parent": true,
"proc": "/proc", "dev": "/dev", "tmp": "/tmp",
"read_only": ["/usr", "/etc/alternatives"], "read_write": ["$$workspace$$"],
"homes": [], "created_files": []
},
"limits": {"wall": "1h", "cpu": 3000, "processes": 256, "file_size": "2G"},
"process": {
"user": "BUILD-USER",
"command": ["make", "-C", "$$tree$$", "image"],
"env": {"PATH": "/usr/bin:/bin", "HOME": "$$workspace$$"},
"verdict": true,
"unversioned_files": [{"at": "$$workspace$$/tree/out/board.img", "as": "builds/board.img"}]
}
}
}
A session of an agent that answers each post, from another pattern: the envelope is the same; only the content differs, and layer 2 reads none of it.
{
"ocx": 2,
"to": "SERVER-HOOK-USER",
"run": {
"clone": "SERVER-CODE-OF-THE-SERVER-REPOSITORY",
"objective": "answer each post in this thread about the parser",
"workspace": [
{"step": "directory", "at": "$$root$$/$$process_user$$/projects/$$repository_name$$/$$run$$"},
{"step": "password"},
{"step": "clone", "into": "$$workspace$$/clone", "password": "$$password$$"},
{"step": "checkout", "of": "trunk", "into": "$$workspace$$/tree"}
],
"sandbox": {
"uid": 1000, "gid": 1000, "linux_capabilities": [], "network": true,
"isolate_terminal": true, "ends_with_parent": true,
"proc": "/proc", "dev": "/dev", "tmp": "/tmp",
"read_only": ["/usr", "/bin", "/lib", "/etc/resolv.conf", "/etc/ssl/certs", "$$release$$"],
"read_write": ["$$workspace$$"],
"homes": ["AGENT-SIGN-IN-DIRECTORY"], "created_files": []
},
"limits": {"wall": "8h"},
"process": {
"user": "AGENT-USER",
"command": ["AGENT-PROGRAM", "AGENT-ARGUMENTS"],
"cwd": "$$tree$$",
"session": true, "hears": ["PERSON-USER"], "deliveries": "start"
}
}
}
Their answers, each a reply where the run is:
{"ocx": 2, "re": "RUN-HASH", "started": {"values": {"workspace": "...", "password": "$$password$$"}, "limits": {"wall": "8h"}}}
{"ocx": 2, "re": "RUN-HASH", "stop": {}}
{"ocx": 2, "re": "RUN-HASH", "ended": {"outcome": "stopped"}}
OCX on the repository whose server-code the run's clone gives, acting as
the run's to, takes each; every other repository does nothing with it.
Layer 2 carries, addresses and answers both without reading run; layer 3
enforces each one's sandbox, limits and steps.
A message of a kind OCX does not read rides the same envelope. Here, for illustration only, a person asks a reviewing agent for a verdict, and it answers; no command, workspace or sandbox is involved, and OCX's readers pass both by:
{"ocx": 2, "to": "REVIEWER-USER", "review": {"checkin": "CHECK-IN-HASH", "objective": "PATH-OF-THE-OBJECTIVE"}}
{"ocx": 2, "re": "REVIEW-REQUEST-HASH", "reviewed": {"verdict": "pass", "note": "ONE LINE"}}
Layer 2 finds each, says who sent it, whom the request asks and what the answer answers, and hands each content to a reader of its kind, here the reviewer's own session; it reads neither content.