MainworthyDocs Open Mainworthy

API and agent tools

Every operation Mainworthy has, with its address, the capability it needs and its input. The web UI, the command line and an agent's tools all call these same operations. The command line and the HTTP API reach all nineteen. An agent's tools are ten of them.

Call an operation #

An operation is one POST with a JSON body. An operation on a project is at /api/p/<project>/<operation>. An operation on the instance (admin, session, credential and invitation) is at /api/<operation>, and only there: under a project's address it is answered not_found, with the address to use.

$ curl -s -X POST http://127.0.0.1:7357/api/p/scratch/find \
    -H 'content-type: application/json' \
    -d '{"in":"cases","limit":2}'
{"ok":true,"result":{"count":0,"items":[],"cursor":null},"seq":1}

The OpenAPI document at /api/openapi.json describes each operation's input and successful reply, including the fields inside result. The registry supplies both schemas. seq is required and is null for an instance operation. A field declared nullable may hold null; an optional field may be absent. Replies selected by an action or a ref are described as alternatives.

A reply that ran has ok: true, the result, and seq, the project's sequence number at that point. Pass seq back to since to ask what changed after it.

A reply that did not run has ok: false and an error with a code and a message:

{"ok":false,"error":{"code":"not_found","message":"no operation named nope"}}
CodeHTTP statusMeaning
invalid400The input is wrong. details says which field.
unauthenticated401No token, or one the server does not know. A name, a password, a passkey's answer or an invitation code that was looked at and refused, whatever was wrong with it.
forbidden403The caller holds a grant on the project and lacks the capability, or the policy says no.
not_found404No such operation, project or ref. A project the caller holds no grant on is answered the same way.
conflict409The edit was made on an older version, or the command id was used for something else.
limited429Too many failed attempts: wrong tokens from this address or for a token that starts as this one does, wrong passwords from this address or for this name, or refused passkeys or invitation codes from this address. The Retry-After header says how many seconds to wait.
internal500The server failed. Nothing was written. The message is internal error, and the detail is in the server's log.

Authenticate #

Send a token as Authorization: Bearer <token>. A server in local mode trusts requests from the same machine and needs none. People, agents and pipelines explains how to make a token and what it may do.

Sign in as a person #

A browser signs in with these requests and then holds a session cookie. They are not operations: nobody is signed in yet, and a password never passes through an operation. Each POST refuses a request that a browser makes from another site's page and accepts one that names no site (a script, curl); GET /api/login/methods answers anybody. Each takes JSON and answers with the same envelope as an operation, without seq. People, agents and pipelines says what they are for.

RequestBodyWhat it does
GET /api/login/methodsnone{"password": true, "passkey": <boolean>}: whether this instance has passkeys.
POST /api/login{"token"}Signs in with a token.
POST /api/login{"name", "password"}Signs in with a name and a password.
POST /api/login/challenge{}The options for navigator.credentials.get, with a challenge that is good once, for five minutes. The reply also sets a cookie (mainworthy_ceremony, HttpOnly) that ties the challenge to the browser that asked: the answer is taken only with it. A browser sends it by itself; a script has to keep it.
POST /api/login{"passkey"}Signs in with what the browser returned for those options: id, clientDataJSON, authenticatorData, signature, userHandle, each base64url.
POST /api/login/challenge{"invitation"}Who the invitation is for, what it does (join or recover) and when it runs out, with the options for navigator.credentials.create, or null for them on an instance without passkeys. It writes no row. On an instance with passkeys, each lookup that finds the invitation makes a challenge the server keeps in memory for five minutes. A code that accepts nothing is answered not_found.
POST /api/login{"invitation", "password"?, "passkey"?, "label"?, "replace"?}Accepts an invitation with a password, a passkey or both, and signs in. passkey is what the browser returned for the options above: id, clientDataJSON, authenticatorData, publicKey, publicKeyAlgorithm, transports. In a browser that is signed in as somebody else it is refused with conflict, with that person's name in details.signedInAs, until replace is that name ("replace": "robin"): the page asks about them first, and their session in this browser ends when the invitation is accepted. A name that is not the person the browser is signed in as now is refused the same way, with the name it would have had to be. replace that is not a name (true) is refused with invalid.
POST /api/account/password{"current"?, "next"}Sets or changes the password of the person whose browser session sends it. current is required when they have one.
POST /api/logoutnoneEnds the browser's session and clears its cookie.

A body holds one of these shapes and nothing beside it. GET /api/session says who is signed in, which session the request came by (session), whether the caller owns the instance (owner) and, for a person, what they sign in with (credentials: whether they have a password, and how many passkeys).

A signed-in person registers a further passkey with the credential operation: passkey.begin answers the options for navigator.credentials.create, and passkey.add takes what the browser returned and a label. Both are for a browser that is signed in, and the session that begins a registration is the one that finishes it. A token is refused with forbidden, as it is when it tries to set a password.

Send a write twice safely #

A write takes cmd, an id you choose for that one request. Sent again unchanged, the server returns the first reply and writes nothing. Sent again with other content, it is refused with conflict. If you lost the reply, ask the command operation what became of the id.

The machine-readable description #

GET /api/openapi.json returns an OpenAPI 3.1 document of every operation. The server builds it from the same list as this page on each call. It needs no token.

Follow changes #

GET /api/p/<project>/events is a stream of server-sent events, one for each write to the project. It needs the read capability. A client that misses an event loses nothing: it reads again, or asks since.

Operations #

propose #

Create or change cases, folders, links or the policy. Saving and proposing are the same call; policy decides what happens next.

HTTPPOST /api/p/<project>/propose
Commandmainworthy propose
Needsproduce
Effectwrites
Agent toolyes
Input of propose
PropertyTypeRequiredDescription
editsarray<object>yes
edits[][create].op"create"yes
edits[][create].bodyobjectno
edits[][create].body.titlestringno
edits[][create].body.prestringno
edits[][create].body.stepsarray<object>no
edits[][create].body.steps[].idstringyes
edits[][create].body.steps[].actionstringyes
edits[][create].body.steps[].expectedstring | nullyes
edits[][create].body.methodsarray<"human" | "auto" | "agent">no
edits[][create].body.automate"none" | "wanted" | "not-applicable"no
edits[][create].body.fieldsrecord<string>no
edits[][create].mdstringno
edits[][create].outlinestringno
edits[][create].folderinteger | stringno
edits[][create].tagsarray<string>no
edits[][create].coversarray<string>no
edits[][create].aliasesarray<string>no
edits[][create].afterstringno
edits[][edit].op"edit"yes
edits[][edit].casestringyesThe case, for example C-311.
edits[][edit].baseintegeryesThe version this edit was made on. A stale base is merged or reported, never overwritten.
edits[][edit].bodyobjectno
edits[][edit].body.titlestringno
edits[][edit].body.prestringno
edits[][edit].body.stepsarray<object>no
edits[][edit].body.steps[].idstringyes
edits[][edit].body.steps[].actionstringyes
edits[][edit].body.steps[].expectedstring | nullyes
edits[][edit].body.methodsarray<"human" | "auto" | "agent">no
edits[][edit].body.automate"none" | "wanted" | "not-applicable"no
edits[][edit].body.fieldsrecord<string>no
edits[][edit].mdstringno
edits[][place].op"place"yes
edits[][place].casesarray<string>yes
edits[][place].folderinteger | stringno
edits[][place].afterstring | nullno
edits[][place].tagsobjectno
edits[][place].tags.setarray<string>no
edits[][place].tags.addarray<string>no
edits[][place].tags.removearray<string>no
edits[][state].op"state"yes
edits[][state].casesarray<string>yes
edits[][state].to"active" | "archived"yes
edits[][restore].op"restore"yes
edits[][restore].casestringyesThe case.
edits[][restore].tointegeryesThe version to restore.
edits[][folder].op"folder"yes
edits[][folder].folderinteger | stringnoThe folder to change. Absent when creating one.
edits[][folder].namestringno
edits[][folder].parentinteger | string | nullno
edits[][folder].archivebooleanno
edits[][cover].op"cover"yes
edits[][cover].casestringyesThe case.
edits[][cover].requirementstringyes
edits[][cover].onbooleanyes
edits[][cover].titlestringno
edits[][policy].op"policy"yes
edits[][policy].presetstringno
edits[][policy].clausesarray<object>no
edits[][policy].clauses[].idstringyes
edits[][policy].clauses[].kind"adopt" | "map" | "failure" | "finding" | "signoff"yes
edits[][policy].clauses[].whenarray<"always" | "author:human" | "author:agent" | "author:ci" | "conflict" | "policy" | "scope:change" | "scope:trunk" | "scope:run" | "hint:disputed" | "category:defect" | "verdict:passed" | "unlinked:track" | "unlinked:create" | string>yes
edits[][policy].clauses[].askobjectyes
edits[][policy].clauses[].ask.who"rule" | "member" | "admin" | "author"yes
edits[][policy].clauses[].ask.agents"answer" | "advise" | "none"yes
edits[][policy].clauses[].ask.notProducerbooleanyes
edits[][policy].clauses[].ask.reason"optional" | "required"yes
edits[][policy].clauses[].answerunknownno
edits[][policy].clauses[].blocks"nothing" | "change" | "release" | "all"yes
edits[][policy].settingsobjectno
edits[][policy].settings.unlinked"ask" | "track" | "create"yes
edits[][policy].settings.basis"exact" | "exact-or-assumed"yes
edits[][policy].settings.holdsobjectyes
edits[][policy].settings.holds.flakyDaysintegeryes
edits[][policy].settings.holds.infraThisTargetOnlybooleanyes
edits[][policy].unlinked"ask" | "track" | "create"no
reasonstringnoWhy. Shown in the case history.

report #

Record results: a parsed CI report, one manual result, or an agent's batch. The producer is the token's actor.

HTTPPOST /api/p/<project>/report
Commandmainworthy report
Needsrecord
Effectwrites
Agent toolyes
Input of report
PropertyTypeRequiredDescription
targetobjectnoWhat was tested. Required unless run is given.
target[1].commitstringyes
target[1].repostringno
target[1].branchstringno
target[1].envstringno
target[1].printegerno
target[1].basestringnoThe commit this change is based on.
target[1].titlestringno
target[1].urlstringno
target[1].filesarray<string>noPaths the change touches, for impact.
target[2].sessionstringyes
runstringnoA planned run these results are for, for example R-4.
jobstringno
attemptintegerno
buildUrlstringno
resultsarray<object>yes
results[].testobjectyes
results[].test[1].keystringyes
results[].test[1].namestringno
results[].test[1].filestringno
results[].test[2].casestringyesThe case this manual or agent result is for.
results[].outcome"pass" | "fail" | "skip" | "blocked" | "error"yes
results[].durationMsintegerno
results[].failureobjectno
results[].failure.messagestringyes
results[].failure.detailstringno
results[].failure.atstringno
results[].stepsarray<object>no
results[].steps[].stepstringyes
results[].steps[].outcome"pass" | "fail" | "skip" | "blocked" | "error"yes
results[].notestringno
results[].annotationobjectno
results[].annotation.casestringyes
results[].annotation.versionintegerno
results[].correctsintegernoThe id of an earlier result of yours that this one replaces.

claim #

Record a review and what it found. A review with no findings is still recorded.

HTTPPOST /api/p/<project>/claim
Commandmainworthy claim
Needsproduce
Effectwrites
Agent toolyes
Input of claim
PropertyTypeRequiredDescription
lensstringyesThe kind of review, for example adversarial-review.
aboutstringnoThe change reviewed, for example pr:482.
patchstringnoExact commit from impact's change.head; required for review reuse.
reviewedarray<string>noCases read, each as C-311@. Recorded even with no findings.
findingsarray<object>noDefault: [].
findings[].onstringyesWhere: a case (C-311), a step (C-311#b7), a requirement (req:WEB-290) or a change (pr:482).
findings[].category"cannot-fail" | "ambiguous" | "missing-expected" | "uncovered-path" | "duplicate" | "outdated" | "defect" | "other"yes
findings[].notestringyes
findings[].reprostringno
findings[].suggestobjectno
findings[].suggest[1].caseobjectyes
findings[].suggest[1].case.titlestringyes
findings[].suggest[1].case.folderstringno
findings[].suggest[1].case.stepsarray<string>yes
findings[].suggest[2].stepstringno
findings[].suggest[2].actionstringno
findings[].suggest[2].expectedstringno

change #

Say that a change ended (merged, or closed without merging) or is open again. An ended change is not asked about.

HTTPPOST /api/p/<project>/change
Commandmainworthy change
Needsrecord
Effectwrites
Agent toolno
Input of change
PropertyTypeRequiredDescription
changestringyesThe change, for example pr:482.
state"merged" | "closed" | "open"yesHow the change ended: merged, or closed without merging. open: it is open again.

plan #

Start a run: choose cases, say which results count and what completes it. Evidence already recorded fills it at once.

HTTPPOST /api/p/<project>/plan
Commandmainworthy plan
Needsplan
Effectwrites
Agent toolno
Input of plan
PropertyTypeRequiredDescription
namestringyes
selectobjectyes
select.casesarray<string>no
select.foldersarray<integer | string>no
select.tagsarray<string>no
select.textstringno
acceptsobjectnoWhich results count. Default: only results recorded in this run.
accepts[1].commitstringyes
accepts[1].repostringno
accepts[1].envstringno
accepts[2].selftrueyes
requirearray<object>no
require[][1].idstringyes
require[][1].kind"exercise"yes
require[][1].methodsarray<"human" | "auto" | "agent">yes
require[][1].minActors1 | 2yes
require[][1].basis"exact" | "exact-or-assumed" | "same-claim"yes
require[][2].idstringyes
require[][2].kind"jobs"yes
require[][2].jobsarray<string>yes
assignarray<string>noActor names to split the slots between.

decide #

Answer a question, or withdraw a decision. Policy says whether your answer settles it, is advice, or is refused.

HTTPPOST /api/p/<project>/decide
Commandmainworthy decide
Needsproduce
Effectwrites
Agent toolyes
Input of decide
PropertyTypeRequiredDescription
[1].qstringyesThe question, for example Q-12.
[1].onEvidencestringyesThe evidence hash you saw. If the evidence changed since, the reply is a conflict with the new evidence.
[1].answerrecord<unknown>yesThe answer, for example {"is":"defect"}. The allowed answers depend on the question kind.
[1].reasonstringno
[2].allarray<object>yes
[2].all[].qstringyesThe question, for example Q-12.
[2].all[].onEvidencestringyesThe evidence hash you saw. If the evidence changed since, the reply is a conflict with the new evidence.
[2].all[].answerrecord<unknown>yesThe answer, for example {"is":"defect"}. The allowed answers depend on the question kind.
[2].all[].reasonstringno
[2].reasonstringno
[3].undostringyesThe question whose decision to withdraw.
[3].ifDecisionintegerno
[3].reasonstringno

inbox #

The queue: every question that needs an answer, most blocking first. With state=decided: the ledger.

HTTPPOST /api/p/<project>/inbox
Commandmainworthy inbox
Needsread
Effectreads
Agent toolyes
Input of inbox
PropertyTypeRequiredDescription
state"open" | "decided"noDefault: "open".
kind"adopt" | "map" | "failure" | "finding" | "signoff"no
onstringnoOnly questions about this case, run, test or change.
limitintegerno
cursorstringnoThe cursor from the previous page.

get #

Read cases, questions, runs, tests or proposals by ref.

HTTPPOST /api/p/<project>/get
Commandmainworthy get
Needsread
Effectreads
Agent toolyes
Input of get
PropertyTypeRequiredDescription
refsarray<string>yesRefs of cases (C-), questions (Q-), runs (R-), tests (T-) or proposals (P-).
fieldsarray<string>noFor cases: any of title, pre, steps, methods, tags, covers, place, proof. Default: all.
as"data" | "md"nomd returns a case as canonical Markdown: fewer tokens than JSON. Default: "data".
unlessrecord<string>noRef to the hash you already hold. An unchanged case comes back as {ref, unchanged: true}.
versionintegernoFor one case: this version instead of the current one.

find #

Search and list: cases (by text, folder, tag), tests, runs, slots of a run, folders, requirements, changes.

HTTPPOST /api/p/<project>/find
Commandmainworthy find
Needsread
Effectreads
Agent toolyes
Input of find
PropertyTypeRequiredDescription
in"cases" | "tests" | "runs" | "folders" | "tags" | "requirements" | "slots" | "changes"noDefault: "cases".
textstringno
folderinteger | stringno
deepbooleannoWith folder: include its subfolders.
tagsarray<string>no
statestringno
runstringnoFor slots: the run.
unlinkedbooleannoFor tests: only those with no case.
limitintegerno
cursorstringnoThe cursor from the previous page.

impact #

The cases a change touches and why, with what was already reviewed or decided. Where to look, instead of the whole suite.

HTTPPOST /api/p/<project>/impact
Commandmainworthy impact
Needsread
Effectreads
Agent toolyes
Input of impact
PropertyTypeRequiredDescription
changestringnoA change key, for example pr:482.
filesarray<string>noChanged paths, when the change is not known to Mainworthy.
sincestringnoA cursor from an earlier call: only what moved since.
ledgerbooleannoFalse for fresh eyes: do not hide what was already reviewed. Default: true.

since #

What changed since a cursor: cases with what changed in each, and questions that were settled.

HTTPPOST /api/p/<project>/since
Commandmainworthy since
Needsread
Effectreads
Agent toolyes
Input of since
PropertyTypeRequiredDescription
cursorstringyesA cursor from any earlier read.
limitintegerno

gate #

The verdict for a commit, a change or a run: passed, failed (needs a fix), blocked (needs a decision), pending (needs evidence).

HTTPPOST /api/p/<project>/gate
Commandmainworthy gate
Needsread or record
Effectreads
Agent toolyes
Input of gate
PropertyTypeRequiredDescription
commitstringno
runstringno
changestringnoA change key, for example pr:482: the gate of its latest commit.
repostringno

history #

The versions of a case, or the step-level diff between two of them.

HTTPPOST /api/p/<project>/history
Commandmainworthy history
Needsread
Effectreads
Agent toolno
Input of history
PropertyTypeRequiredDescription
refstringyesThe case.
fromintegerno
tointegernoWith from: the step-level diff between the two versions.

timeline #

One timeline for a case: versions, moves, results from every producer, findings, reviews and its questions.

HTTPPOST /api/p/<project>/timeline
Commandmainworthy timeline
Needsread
Effectreads
Agent toolno
Input of timeline
PropertyTypeRequiredDescription
refstringyesThe case.
limitintegerno

check #

Check the suite for structural problems: cases with no steps or no expected result, duplicate titles, dangling links.

HTTPPOST /api/p/<project>/check
Commandmainworthy check
Needsread
Effectreads
Agent toolno
Input of check
PropertyTypeRequiredDescription
limitintegerno

command #

What became of a command you sent under an idempotency key: not recorded, begun, or done with its reply.

HTTPPOST /api/p/<project>/command
Commandmainworthy command
Needsread
Effectreads
Agent toolno

admin #

Projects, actors, grants and tokens. There are no seats: an actor is free, and a token carries what its actor may do.

HTTPPOST /api/admin
Commandmainworthy admin
Needsadmin
Effectwrites
Agent toolno
Input of admin
PropertyTypeRequiredDescription
[1].do"project.create"yes
[1].keystringyes
[1].namestringno
[1].presetstringno
[1].unlinked"ask" | "track" | "create"noWhat happens to a test with no case: ask, track or create. Absent: what the preset starts with.
[2].do"project.list"yes
[3].do"token.create"yes
[3].actorstringyes
[3].kind"human" | "agent" | "ci"yes
[3].canarray<"read" | "record" | "produce" | "answer" | "plan" | "admin">noWhat a new actor may do. Absent: a person everything, an agent read and produce, a pipeline record. Not for an actor that exists: use grant.set. Default: [].
[3].projectstringnoLimit the grant to one project. Absent: every project.
[3].labelstringno
[3].expiresstringnoHow long the token works from now: 90d, 12h, 2w. After that it is refused as a revoked one is. Absent: it has no end.
[4].do"token.list"yes
[5].do"token.revoke"yes
[5].idintegeryes
[6].do"actor.list"yes
[7].do"grant.set"yes
[7].actorstringyes
[7].projectstringnoThe project the grant is for. Absent: every project.
[7].canarray<"read" | "record" | "produce" | "answer" | "plan" | "admin">yesWhat the actor may do there from now on, in place of what it could.
[8].do"grant.remove"yes
[8].actorstringyes
[8].projectstringnoThe project the grant is for. Absent: the grant on every project.
[9].do"actor.forget"yes
[9].namestringyesRemoves who this actor was: a pseudonym replaces the name and its tokens stop working. History keeps what was done.
[10].do"policy.show"yes
[10].projectstringyes

session #

Your browser sessions: list them, and end one. An admin who is a person lists and ends anyone's.

HTTPPOST /api/session
Commandmainworthy session
Needsno capability: any person or agent that is signed in, for what is their own
Effectwrites
Agent toolno
Input of session
PropertyTypeRequiredDescription
[1].do"list"yes
[1].actorstringnoWhose sessions. Absent: your own. Somebody else's is for an admin who is a person.
[2].do"end"yes
[2].idintegeryesThe session, from the list. Your own, or anyone's for an admin who is a person.

credential #

How a person signs in: list your password and passkeys, register a passkey, remove one. An agent is refused: it signs in with a token only. A password is set in the web UI, never here.

HTTPPOST /api/credential
Commandmainworthy credential
Needsno capability: any person or agent that is signed in, for what is their own
Effectwrites
Agent toolno
Input of credential
PropertyTypeRequiredDescription
[1].do"list"yes
[1].actorstringnoWhose. Absent: your own. Somebody else's is for an admin who is a person.
[2].do"passkey.begin"yes
[3].do"passkey.add"yes
[3].passkeyrecord<unknown>yesWhat the browser returned for the options of passkey.begin: id, clientDataJSON, authenticatorData, publicKey, publicKeyAlgorithm, transports.
[3].labelstringyesWhat you call this passkey: the device it is on.
[4].do"remove"yes
[4].idintegeryesThe credential, from the list. The last one a person has is not removed.

invitation #

Invite a person with a link, since there is no mail: make one, list them, revoke one. The link is shown once.

HTTPPOST /api/invitation
Commandmainworthy invitation
Needsadmin
Effectwrites
Agent toolno
Input of invitation
PropertyTypeRequiredDescription
[1].do"create"yes
[1].namestringyesThe person. A new name: an invitation to join. A person who exists: a recovery, which replaces every way they sign in once it is accepted.
[1].canarray<"read" | "record" | "produce" | "answer" | "plan" | "admin">noWhat a new person may do. Absent: everything. Not for a person who exists: use grant.set. Default: [].
[1].projectstringnoLimit the grant to one project. Absent: every project.
[1].expiresstringnoHow long the link works: 12h, 3d, 2w. At most 30d. Default: "3d".
[2].do"list"yes
[3].do"revoke"yes
[3].idintegeryes

Agent tools #

An agent that connects over MCP gets these operations as tools, with the same inputs. Each tool has a compact output schema for { result, seq }, the result's top-level type and its required fields. Nested fields and alternative replies are described in full by the OpenAPI document. Most input-field descriptions are in the shipped skill reference; tool schemas retain guidance for commit reuse, question answers and review filtering. Let an agent in explains how to connect one.

ToolWhat the agent is told it is for
proposePropose cases at their base version. Status: adopted=current, open=needs acceptance, conflict=resolve, rejected.
reportRecord test results in one batch. Give target (the commit tested) or run (a planned run). Each result names a test by key or a case by ref.
claimReview cases (reviewed, C-311@hash) at change about and commit patch. Record findings; known returns prior findings and answers.
decideAnswer inbox using evidenceHash as onEvidence. Status: decided, advised=person decides, forbidden, invalid, already, reopened, conflict=evidence or decision changed. Choose from answers. Withdraw with undo and ifDecision=reply.decision.
inboxList open questions with evidence and evidenceHash, counts first. on selects a case, run or change.
getRead refs (C-, Q-, R-, T-, P-). For cases, as: "md" and fields read less; unless: {ref: hash} skips unchanged cases.
findSearch cases by text, folder or tag; list tests (unlinked: true), runs or slots. Page with cursor.
impactStart a review with a change (pr:482) or changed files. Returns affected cases, hashes, prior reviews, questions and uncovered files. Reuse cursor as since.
sinceWhat changed since an earlier cursor: changed cases (one-line diff, no bodies) and settled questions. If more, call again with the new cursor.
gateIs this commit, change or run tested? Returns passed, failed, blocked or pending with the reason and the open questions.
Mainworthy 0.1.0 · Licence · this page is docs/guide/api.md in the repository