Corbie

Reference

Definitions

Every type of machine shares these fields, expressions, jobs, events and approval.

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 ls does 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
  1. The client reads each file when it sends the definition. A file holds any fields, and has no type.
  2. 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.
  3. 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: or meta:, 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.

  1. It fills in each missing name that has a default.
  2. It checks the input against input:.
  3. 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 as null. A definition with no input: takes any input.
  • A default is filled in the input and in each object that properties and items lead to. A default elsewhere, such as under anyOf or behind a $ref, is refused.
  • A $ref resolves only within its own shape.
  • A shape may lead back to itself with $ref, and so describe a tree. A value checked against a $ref that 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, require or plugins, so it reads no file and makes no network connection.
  • Date gives the time of the step, and Math.random gives 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 input and 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: or ask_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 require names 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: always and 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 against limits: { 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.

  1. A client gives a key with corbie run submit --key, a job with key:, and a statechart's send with to:.
  2. Among one client's running runs, a key names at most one run. It is free again once its run ends.
  3. 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.
  4. 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 machine starts 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 that ask_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, with entry and tool too for a tool from a list:, and part, the part's name, for a tool of a part or for open. The list of an entry in a part has asked: { 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 nearest ask: 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 nearest ask: above it that was not made while running, and with none they fail with status denied.
  • 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 no ask_about: from above it.
  • corbie run submit refuses a definition that can hold a job and has no ask: 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