A definition is a YAML file. Its first line is the machine's type and name: flow: digest, statechart: review or agent: librarian. A name is letters, digits, _ and -, and starts with a letter.
Fields every type takes
input: { urls: list } the shape of a run's input, as below
keep: 7d how long a run is kept after it ends; default 7d
limits: { timeout: 1h } how much a run may use, as below
allow: [net:example.com] permissions of every script job in the definition
deny: [exec] what no script job in the definition may do
ask_about: [disk] permissions that hold a job for approval
ask: { script: ./ask.js } the job that approves held jobs
title: "${input.topic}" a run's title, filled in when it starts, as below
meta: { owner: alice } data Corbie keeps and never reads; no expression in it is filled in
title: names a run to a person, and corbie run show prints it. Its expressions read input and run.
- A title is one line of at most 200 characters. Each run of white space, line breaks among it, becomes one space, and a longer title is cut.
- Only the client that started the run sees its title.
corbie run lsdoes not print it. - A title that cannot be filled in fails the run, as an input that does not fit does.
limits: takes these.
timeout: 1hhow long a run may take; default no limit
depth: 8levels of child runs below a run that a client started; default 8, 0 for none
jobs: 100how many jobs a run may start; default no limit
turns: 20an agent's turns, as in Agent
A length of time is written 30s, 5m, 48h or 7d.
Include
Any mapping of fields, or of names such as tools: or stages:, can take fields from other files.
agent: family
include: ./base.yaml one file
tools: { include: [./a.yaml, ./b.yaml], own: { … } } or a list
- The client reads each file when it sends the definition. A file holds any fields, and has no type.
- A field the mapping writes stands before a file's, a file's before those of the files it includes, and an earlier file's before a later one's.
- The stored definition holds the fields written out.
- A path in a file is read from the file's directory.
- What is a value, such as
input:ormeta:, stays as it is written. - A problem in a file names the file.
- More than 1,000 files included by one definition are refused.
Input
input: is the shape of a run's input: each name, with the type of its value.
string, number, booleanthat JSON type
list, mapan array, an object
bloba blob, such as a file given with --input-file
anyany value
? after the typethe name may be left out; inside { } YAML needs it in quotes
The controller checks the input when the run starts.
- It fills in each missing name that has a
default. - It checks the input against
input:. - A run whose input does not fit fails, and the error names each place.
A shape written with type: object is JSON Schema, which can give a default.
# look.yaml
flow: look
input:
type: object
properties:
url: { type: string }
depth: { type: number, default: 2 }
required: [url]
stages:
it:
job: { script: ./echo.js, input: "${input}" }
output: ${stages.it.output}// echo.js
return input
corbie test look.yaml --input '{"url": "https://example.com"}' output: {"depth":2,"url":"https://example.com"}
- A name that
input:does not give is taken as it is, and a name the input does not hold reads asnull. A definition with noinput:takes any input. - A default is filled in the input and in each object that
propertiesanditemslead to. Adefaultelsewhere, such as underanyOfor behind a$ref, is refused. - A
$refresolves only within its own shape. - A shape may lead back to itself with
$ref, and so describe a tree. A value checked against a$refthat leads back with no part of the value between fails.
Values and expressions
An expression is one JavaScript expression between ${ and }. Any value may contain one.
count: "${input.n}"exactly one expression: its value, of any type
text: "Look into ${input.topic}."text with each value filled in
n: 3anything else is literal
when: ${input.n > 1}a condition: exactly one expression
An expression reads these names.
inputthe run's input, in every type
runthe run's id and key, in every type
stages, itemin a flow
states, event, ticketsin a statechart
argsin an agent's tool
It may use JavaScript's operators, ? :, arrow functions, methods such as map and filter, and objects such as Math, JSON, Date, Object, Array, String, Number, RegExp, Map and Set.
- A name that is not one of these is refused when the definition is checked.
- An expression cannot change what it reads. It has no
eval,Function,Promise,requireor plugins, so it reads no file and makes no network connection. Dategives the time of the step, andMath.randomgives the same numbers each time the step is taken.- An expression may run for one second. That limit stops its own code, and not a call to something built in that is under way.
- In a permission's scope an expression reads
inputand nothing else. - Put a value that contains
${...}in quotes when it is inside{ }or[ ], or when the expression has a colon followed by a space. - These take no expression: a name, a type, a permission's kind, a secret's name, a
deny:orask_about:entry, a tag and a ticket's name.
Jobs
A machine contains jobs. A job is a script, which a runner executes, or a machine, which the controller starts as a child run.
script: ./fetch.jsa script file
machine: ./child.yamla definition file, or a definition written in place
key: "chat:${input.id}"machine only: the key the child run is started with
input: { url: "${item}" }the input of the script or of the child run
allow: [net:example.com]for a script, what the job may do, with the definition's allow:; for a machine, a limit on everything the child run does
deny: [fs.write:/etc/**]what the job may not do
timeout: 5mfor a script, how long it may run, default 5m; for a machine, the child run's timeout
restart: { limit: 2 }what happens when the job ends, as below; with none it runs once
approve: truethe job waits until the definition's ask approves it
files: [./lib.js]script only: files the script loads with require("./lib.js")
memory: 256MBscript only: the most memory it may use, in B, KB, MB or GB
tags: [gpu]script only: the job goes to a runner that has every tag
job: ./fetch.jsshort for job: { script: ./fetch.js }
job: { machine: "${stages.make.output.definition}" }exactly one expression in place of a path
- The context is the directory you submit from, or a file's own directory for a file outside it. Every file is in the context, or below it.
- A path is read from the context, and a path starting
./from the directory of the file it is written in: a definition, a machine kept in its own file, or an included file.../is refused. - A script that loads another with
requirenames it from its own directory, with./or../. - A machine file or an included file that names a file already being read is refused: no file leads back to itself.
- A machine as it is stored is at most 4 MiB.
- A YAML alias cannot contain itself. A file whose aliases make more than about 100 times what it writes, past 1000 values, is refused.
- A child run whose job has no
allow:has what its own definition allows. - An expression in place of a path evaluates to a blob that contains a script, or to a definition that a script made with
machines.define. - Each step of a machine made while running, and of everything below it, runs in a process of its own and may use 10 seconds and 1 GiB of memory. Past either, that machine fails with an error and nothing else does.
Restart
A job with restart: runs again when it ends. With none it runs once.
job:
script: ./fetch.js
restart: { when: always, limit: 5 in 1h, delay: 1s to 5m }
when: failurerestarts after an error or a timeout; the default
when: alwaysrestarts after the job ends well too
limit: 3at most three restarts; no limit by default
limit: 5 in 1hat most five in any hour
delay: 1s to 5mdoubles from the first to the second; the default
delay: 30sthe same each time
- The job restarts with the input it was first given, and a machine as a new child run, from its start.
- A denial and a cancel are never restarted, so a job cancelled by hand stays stopped.
- A delay that doubles starts again once an attempt has stayed up for twice its longest wait.
- The machine sees only the final outcome: the last attempt's, past the limit. The run's record shows each attempt.
- Every value an attempt emits is delivered, so a job that repeats itself delivers its values again.
- With
when: alwaysand no limit, the machine sees no outcome, so a stage waiting on the job never ends. - A restart after a failure is the same job, under a yes it was already given.
- A restart after a job ended well is a new job: it is held again if it needs approval, has a new
job.request, and counts againstlimits: { jobs: }. - An ask that ends well is final, whatever its policy says.
- A job's key is free between attempts. An event sent to it then is undelivered, and an attempt whose key has been taken fails.
Keys
A key names a run. Give a run a key, then send to that key. Send with machine too and a run is started if none has the key.
- A client gives a key with
corbie run submit --key, a job withkey:, and a statechart's send withto:. - Among one client's running runs, a key names at most one run. It is free again once its run ends.
to: { key }delivers the event to the run with the key.to: { machine, key }delivers it, or starts the machine with that key when no run has it.- A run reads its key as
${run.key}, its ID as${run.id}, and the absolute path of the context it was submitted from as${run.context}.
send: { to: { key: "chat:${event.data.id}" }, event: said }a statechart's send
send: { to: { machine: ./chat.yaml, key: "chat:1" }, event: said }the same, starting a run
job: { machine: ./ask-person.yaml, key: "question:${run.id}" }a job in any type of machine
started by when the key is in use the run is a client the start is refused in its receipt a root a job the job fails, with a reason a child of the job a send the event is delivered to that run a root; ending the sender does not cancel it
- A key is any text of 1 to 256 characters. A client's keys name only that client's runs.
- Anyone who can list runs can read a run's key. Put no secret in a key.
- Text from outside can name any of your runs, so write the start of a key yourself, as in
chat:${event.data.id}. - A run a send starts gets the event's data as its input, and then the event. It has the permissions and the ask a child of the sender would have.
- A send that cannot be made fails nothing, and the sender's record says why.
- Events from one run to one key arrive once each, in the order sent. An event sent with
machinestarts a new run when its run ends first. - A machine made while running fails if it sends to a key or gives a job a key, so it talks only to its parent and its children. A job above it may start it with a key.
A statechart a send starts takes the event in its first state, so the first event goes the same way as every later one. Do not act on input as well.
initial: waiting
states:
waiting: { on: { said: answering } }
answering: { job: { script: ./answer.js, input: "${event.data}" }, on: { done: waiting } }
Machines together has an example, and Statechart has the fields of a send.
Events
The controller sends events to a machine. An event has a name, a source and data.
startthe run's input
donea job ended well; data is its output
faileda job failed; data is { status, error, output }
<name>a timer that is due, a ticket's answer, an event from the parent run, a child run, the client (corbie run send) or a send to the run's key, or a value a job emitted with emit(name, value)
No client, machine or script can send an event named start, done, failed or timer.
A failed job's status is error, timeout, denied or cancelled, and denied adds refused, layer and fix to the data. Each type's page lists the events it acts on.
Approval
Approval has the mechanism and an example. The controller holds a job in two cases.
- The job has
approve: true. - The job's
allow:list declares a permission thatask_about:lists, in its definition or in one above it. Two entries match when they have the same kind and either scope covers the other, or when one is a group of that kind.
The ask is a job, and the controller adds two fields to its input.
ask: ./ask.jsa script
ask: { machine: ./ask.yaml }a machine, which can wait for a person
input.asking{ machine, instance }: the machine whose jobs are held, and its run's ID
input.jobs[{ name, input, allow, script or machine, asked }]: each held job
- A held job has
asked: { does, args, said }only for an agent's tool call, withentryandtooltoo for a tool from alist:, andpart, the part's name, for a tool of a part or foropen. The list of an entry in a part hasasked: { part }.
The ask returns { approve: [names of jobs], note: "..." }.
a job the ask namesstarts, with no permission it did not have
a job the ask does not namefails with status denied; its error has the note
An ask that fails, whose values cannot be filled in, or that returns any other value, approves nothing.
- A machine with no
ask:uses the nearestask:above it. - A machine made while running has no ask of its own, and one that writes an
ask:fails as it starts, saying so. Its held jobs go to the nearestask:above it that was not made while running, and with none they fail with statusdenied. - The jobs of an agent's set, and the machines below them, use the
ask:of the set, or of the nearest set it is inside that has one, before the agent's. The controller starts each ask once for the jobs of one step that use it. - A machine run as an ask uses no
ask:and noask_about:from above it. corbie run submitrefuses a definition that can hold a job and has noask:in it or above it.
A script ask runs alone under corbie test. This one approves a job only if its allow: list has no fs.write entry.
// ask.js
return {
approve: input.jobs.filter(r => !r.allow.some(a => a.startsWith("fs.write"))).map(r => r.name),
note: "writes are not approved here",
}corbie test ask.js --input '{"jobs": [{"name": "look", "allow": ["fs.read:/srv/**"]}, {"name": "tidy", "allow": ["fs.write:/srv/**"]}]}' output: {"approve":["look"],"note":"writes are not approved here"}
Reference
- Flow the fields of a flow
- Statechart the fields of a statechart
- Agent the fields of an agent
- Scripts what a script reads, calls and returns
- Permissions how an entry is written, and the layers that limit a job