openapi/dashboard.yaml
Rama main · réplica de lectura del repositorio original.
openapi: 3.1.0
info:
title: Ferryman Dashboard API
description: >
The loopback-only web dashboard for observing a channel and (when not
read-only) reviewing work. Served by `ferry dashboard` on a loopback
address (default http://127.0.0.1:8788). It is a distinct surface from the
bridge server API (`openapi.yaml`, port 8787): it authenticates a *human
operator* by password and returns a short-lived session token, rather than
the server's project/actor bearer tokens.
version: 0.3.1
servers:
- url: http://127.0.0.1:8788
security:
- DashboardSession: []
components:
securitySchemes:
DashboardSession:
type: apiKey
in: header
name: x-ferryman-dashboard-token
description: >
Random bearer token returned by `POST /api/auth/login`; idle-expiring
and revoked on sign-out.
schemas:
Credentials:
type: object
required: [name, password]
properties:
name: {type: string}
password: {type: string, writeOnly: true, minLength: 8}
CreatedOperator:
type: object
properties:
name: {type: string}
public_key: {type: string}
LoginResponse:
type: object
properties:
token: {type: string, description: Bearer token for the x-ferryman-dashboard-token header}
name: {type: string}
public_key: {type: string}
Review:
type: object
required: [accept]
properties:
accept: {type: boolean}
notes: {type: [string, 'null']}
Error:
type: object
required: [message]
properties:
message: {type: string}
paths:
/api/connect:
get:
summary: What the Connect an AI app panel needs to write a config (this ferry binary and a machine label; no secrets)
responses:
'200': {description: the ferry path and machine label}
'401': {description: no session}
/api/auth/create:
post:
summary: Mint a password-sealed operator identity and publish its public key to the roster
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/Credentials'}}
responses:
'200': {description: identity created, public key published, content: {application/json: {schema: {$ref: '#/components/schemas/CreatedOperator'}}}}
'403': {description: dashboard is read-only}
'409': {description: name already exists or password too short}
'429': {description: too many creations in the current window}
/api/auth/login:
post:
summary: Unlock an operator identity and start an idle-expiring session
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/Credentials'}}
responses:
'200': {description: session started, content: {application/json: {schema: {$ref: '#/components/schemas/LoginResponse'}}}}
'401': {description: name or password incorrect}
'403': {description: dashboard is read-only}
'429': {description: too many sign-in attempts for this name; wait a minute}
/api/auth/logout:
post:
summary: End the current session immediately
responses:
'204': {description: session revoked}
/api/auth/whoami:
get:
summary: Report the session's operator, or 401 when there is no live session
responses:
'200': {description: operator name, content: {application/json: {schema: {type: object, properties: {name: {type: string}}}}}}
'401': {description: no active session}
/api/auth/recover:
post:
summary: Restore an operator from its 24-word recovery phrase
description: >
Recovering a name the channel already publishes needs no setup token: the phrase
must derive that published key, which only its owner can do. Creating a NEW name
this way still needs the one-time token printed on the console.
responses:
'200': {description: session token and fingerprint}
'400': {description: the phrase is not a valid 24-word BIP-39 phrase}
'401': {description: an unpublished name, and no setup token or session}
'409': {description: this machine already holds a different seed}
/api/auth/status:
get:
summary: Whether this machine holds an operator, and which published ones it is missing
description: >
`orphaned_operators` names operators the CHANNEL publishes whose sealed key is not
on this machine. It distinguishes "never set up" from "identity lost", which look
identical otherwise and call for opposite actions.
responses:
'200': {description: any_operators, seed_present, read_only, orphaned_operators}
/api/identity:
get:
summary: The operator's fingerprint, and whether it derives from the machine seed
responses:
'200': {description: fingerprint and derivation}
'401': {description: no active session}
/api/team/invite:
post:
summary: Make an invitation code for a teammate who has not joined yet
description: >
Master only. Writes a signed invitation and returns the one-use code and the prompt
to send. A name may be reserved; without one the person picks any free name when
they create their identity, and the acceptance carries it. It cannot create their
identity - that happens on their machine, under their own password.
Two lengths are separate. `expires_days` is how long the CODE works for joining
(default 7). The ACCESS it leads to is chosen on every invitation, with no default:
send exactly one of `access_days` (1 to 3650) or `access_forever: true`. Timed access
ends that many days after the master's grant is signed unless the master renews it.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: {type: string}
agent: {type: string}
roles: {type: array, items: {type: string}}
expires_days: {type: integer, minimum: 1}
access_days: {type: integer, minimum: 1, maximum: 3650}
access_forever: {type: boolean}
responses:
'200': {description: 'the reserved name, the code, and `access_days` (null for no expiry)'}
'400': {description: not a path-safe identifier, or the length of the access was not chosen}
'409': {description: already in the channel}
/api/questions/{id}/answer:
post:
summary: The master renews, or lets end, access that is about to run out
description: >
Master only. Answers a question of kind `access` (the ones listed as `pending_access`
by `GET /api/team`) with exactly one of its options, `Renew` or `Let it end`, as the
master's own signed answer. Nothing else is answered here; every other kind has its
own page. A renewal extends the access by one more term from the date it would have
ended; no answer, or `Let it end`, leaves the end where it was.
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [answer]
properties:
answer: {type: string}
responses:
'200': {description: the recorded answer}
'400': {description: not an access question, or not one of its options}
'401': {description: no active session}
'403': {description: dashboard is read-only, or the signed-in person is not the master}
'404': {description: no such question}
'409': {description: already answered}
/api/master/init:
post:
summary: The signed-in operator becomes this project's master
description: >
The person, not the machine. Signed with the session's unlocked operator key, so no
password is asked for again. Flips `grants` to required. Refuses when a master
already exists; the master transfers the role, nobody seizes it.
responses:
'200': {description: the master's name and whether grants are now required}
'403': {description: dashboard is read-only}
'409': {description: a master already exists}
/api/head/revoke:
post:
summary: The master clears the head agent of this project
description: >
A head agent is whoever the master last named in their own signed words and who
claimed it (`ferry channel head claim`). Naming someone else replaces it; this
clears it. Master only.
responses:
'200': {description: how many appointments were removed}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/swarm:
get:
summary: Which projects the swarm may work
description: >
For each project on this machine (the ferry root's, or just the one on screen),
whether the swarm may work it: the master's signed `swarm = on|off`, who signed
it and when, the project's master, whether the person signed in may change it
(`may_set`, true only for that master), and whether it is archived. A project is
in unless its master signed off.
responses:
'200': {description: 'projects: project, on, set_by, set_at, master, may_set, archived'}
'401': {description: no active session}
post:
summary: The master puts a project in or out of the swarm
description: >
Writes the master-signed SWARM setting into the project's channel, where every
machine that syncs it reads it. A project that is out is not cloned by
`ferry repos sync`, not served by an all-projects worker and not enrolled by
`team approve --all`. A setting anyone but the master signed is ignored. Master only.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [project, "on"]
properties:
project: {type: string}
"on": {type: boolean}
responses:
'200': {description: 'project, the new state, and whether it changed'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
'404': {description: not a project on this machine}
/api/improve:
get:
summary: Self-improve for this project, its last run, and every worker's engines
description: >
Whether the project's master has switched the weekly self-improvement loop
(`ferry improve`) on, who signed that and when, the latest week the loop wrote
anything for, and each worker's signed engine inventory (tier, how it is paid,
up, down or out of credit until when; never a credential). Each engine also
carries its `capabilities` (modalities, strengths, context_k, cost, local) when
its worker publishes a v2 inventory. `may_set` is true only when the person
signed in is the master.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'enabled, set_by, set_at, master, may_set, last_run and engines'}
'401': {description: no active session}
post:
summary: The master switches self-improve on or off for this project
description: >
Writes the master-signed SELF_IMPROVE setting into the channel, where every
machine that syncs it reads it. Off by default; a setting anyone but the master
signed is ignored. Master only.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled: {type: boolean}
responses:
'200': {description: 'the new state, and whether it changed'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/improve/all:
post:
summary: Self-improve on (or off) for every project the signed-in person masters
description: >
"On for all my repos". Writes the master-signed SELF_IMPROVE setting into every
project in this machine's ferry root whose master is the person signed in (only
this project on a machine without a root). Projects mastered by someone else are
reported and left alone. The CLI equivalent is `ferry improve on --all`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled: {type: boolean}
responses:
'200': {description: 'how many were switched, and per project: switched, already, mastered by, no master or error'}
'401': {description: no active session}
'403': {description: dashboard is read-only}
/api/engine-policy:
get:
summary: The engine policy background work runs under, and what auto recommends
description: >
Which engines do this project's background work (self-improve planning,
building and review), in what order, which never, and on which machines: the
master-signed ENGINE_POLICY, or auto when none is signed (local and free engines
first, subscriptions never). `effective` is how it falls on the engines the
fleet published - each role's engines in order, and every blocked engine with
the reason. `recommended` is what auto would sign, with one reason per choice.
`may_set` is true only for the signed-in master. `adversary_mode` is what the adversary's findings do here: `off`, `advisory` (the
default) or `blocking`; `effective.roles.adversary` is who challenges, in order.
Each role in `effective.roles` carries its `effort`, `width` (orders at once, null
for no cap) and whether subscriptions are allowed for it; each engine its size
`class`. `team` is the team preset as it would be signed now, with reasons, and
`warnings` lists any subscription the policy opens that has no weekly cap.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'project, auto, set_by, set_at, policy, describe, effective, recommended, team, warnings, efforts, self_improve, adversary_mode, the smart router''s routing, thresholds, bias and default_threshold, the kinds of work, and may_set'}
'401': {description: no active session}
post:
summary: The master signs an engine policy, or goes back to auto
description: >
Signs `policy` with the session's key into the channel, where every machine
reads it; `null` goes back to auto. `all` does it in every project in the ferry
root the person is master of. The CLI equivalent is `ferry engines policy set`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
policy:
type: object
nullable: true
properties:
prefer: {type: object, additionalProperties: {type: array, items: {type: string}}}
never: {type: array, items: {type: string}}
where: {type: array, items: {type: string}}
caps_usd: {type: object, additionalProperties: {type: number}}
protect_subscriptions: {type: boolean}
never_applies_to: {type: string, enum: [background, all]}
all: {type: boolean}
responses:
'200': {description: 'how many changed, and per project: set, already, skipped or error'}
'400': {description: a role, selector or cap that means nothing}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/engine-policy/accept:
post:
summary: The master signs what auto recommends
description: >
Signs the recommended engine policy, computed from each project's own fleet.
`all` does it in every project the person is master of. The CLI equivalent is
`ferry engines policy accept`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
all: {type: boolean}
responses:
'200': {description: 'how many changed, and per project: set, already, skipped or error'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/engine-policy/choose:
post:
summary: The master picks the improvement engine and the review engine
description: >
The simple choice. `improve` goes first for planning and building, `review` first
for review - the first of the two keys every improvement needs before it can go
live. `recommended` uses what auto recommends for both. The rest of the policy is
kept. `auto_merge` is `none` or `low-risk`: whether fm merges an improvement on its
own once it holds both keys, when every changed file is docs, tests or a dependency
version bump. `adversary_engine` goes first for the adversary role, who challenges
the builders' work at three moments (before a contract locks, when an order fails
twice, before an improvement is called done); `adversary` is `off`, `advisory` (the
default) or `blocking`, what its Block verdicts do. `all` does it in every project
the person is master of. The CLI equivalent is `ferry engines policy set --improve
<sel> --review <sel> --auto-merge <mode>`, and for the adversary `ferry engines
policy set --role adversary --prefer <sel> --adversary <mode>`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
improve: {type: string}
review: {type: string}
adversary_engine: {type: string}
adversary: {type: string, enum: [off, advisory, blocking]}
recommended: {type: boolean}
auto_merge: {type: string, enum: [none, low-risk]}
all: {type: boolean}
responses:
'200': {description: 'how many changed, and per project: set, already, skipped or error'}
'400': {description: nothing picked, an auto_merge that is neither none nor low-risk, or an adversary mode that is not off, advisory or blocking}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/engine-policy/team:
get:
summary: What the team preset would sign, with a reason per choice
description: >
"Plan on high, build on medium, swarm the cheap work": a large judge plans and
reviews at high effort, one at a time; mid-size engines build at medium effort,
three at once; the smallest do chores at low effort, four at once; the adversary
is a large judge of another model family than the top builder. Built from the
engines the fleet published. Subscriptions stay out of background work except for
the roles in `subscription_roles`, and then only an engine with a `weekly_requests`
cap. Nothing is signed. `subscription_roles` (comma-separated roles; absent keeps
the policy in force's), `width` (`build=4,chore=2`) and `effort` (`build=high`) try
other choices. The CLI equivalent is `ferry engines policy team`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
- {name: subscription_roles, in: query, required: false, schema: {type: string}}
- {name: width, in: query, required: false, schema: {type: string}}
- {name: effort, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'policy, describe, reasons, warnings and effective'}
'400': {description: a role, width or effort that means nothing}
'401': {description: no active session}
post:
summary: The master signs the team preset
description: >
Signs the team preset as the project's policy, computed from each project's own
fleet, with the same optional choices as the GET. `all` does it in every project
the person is master of. The CLI equivalent is `ferry engines policy team
--accept`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
subscription_roles: {type: array, items: {type: string, enum: [plan, build, review, chore, adversary]}}
width: {type: object, additionalProperties: {type: integer, minimum: 1, maximum: 255}}
effort: {type: object, additionalProperties: {type: string, enum: [low, medium, high]}}
all: {type: boolean}
responses:
'200': {description: 'how many changed, and per project: set, already, skipped or error'}
'400': {description: a width of 0, or a role or effort that means nothing}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/engine-policy/settings:
post:
summary: The master sets effort, width and subscription roles per role
description: >
`effort` is how hard a role thinks (`low`, `medium`, `high`); `width` is the most
orders of the role claimed at once across the fleet, with `null` removing the cap;
`subscription_roles` lists the roles whose background work may use a subscription,
and only one with a `weekly_requests` cap (`[]` clears it). The rest of the policy
is kept. `all` does it in every project the person is master of. The CLI equivalent
is `ferry engines policy set --effort role=level --width role=n
--allow-subscriptions-for role,...`.
The smart router's settings go the same way: `routing` is `smart` (the default) or
`ordered`; `thresholds` maps a kind of work to the success probability that counts as
sufficient (`{"docs": 0.7}`, `{}` clears them); `bias` maps an engine selector to a
tie-break weight (`{"nvidia*": 3}`, `{}` clears them). Each of the two replaces what was signed. They are signed in the
policy's v2 view only, so a v0.5.17 machine still verifies the file.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
effort: {type: object, additionalProperties: {type: string, enum: [low, medium, high]}}
width: {type: object, additionalProperties: {type: integer, nullable: true, minimum: 1, maximum: 255}}
subscription_roles: {type: array, items: {type: string, enum: [plan, build, review, chore, adversary]}}
routing: {type: string, enum: [smart, ordered]}
thresholds: {type: object, additionalProperties: {type: number, exclusiveMinimum: 0, maximum: 1}}
bias: {type: object, additionalProperties: {type: number}}
all: {type: boolean}
responses:
'200': {description: 'how many changed, and per project: set, already, skipped or error'}
'400': {description: 'nothing to set, a width of 0, a role, effort, kind or routing that means nothing, or a threshold outside 0 to 1'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/focus:
get:
summary: The swarm's focus - which projects it is spending itself on
description: >
The master-signed FOCUS record in the home project's channel (`ferryman`, or
FERRYMAN_FOCUS_HOME): a tier per project - focus, normal, background or paused -
each with an optional expiry. `projects` lays it over every live project: its tier,
the entry that set it, `orders` (improvement orders planned this week, for a project
with self-improve on) and `width` (how many of a role may run at once under the
tier). `notice` says when this machine is holding on to an earlier record because
the file went back or vanished. `?suggest=1` also returns `suggestions`: what the
fleet's own signals say per project, with one reason each; nothing is signed.
`may_set` is true only for the signed-in master of the home project. The CLI
equivalent is `ferry focus show` and `ferry focus suggest`.
parameters:
- {name: suggest, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'available, home, set_by, set_at, seq, set, notice, expired, projects, tiers, may_set, master and, when asked, suggestions'}
'401': {description: no active session}
post:
summary: The master puts projects in a tier
description: >
Signs the entries into the home project's channel with the session's key, where
every machine reads them; other projects keep their entries. `days` makes the
entries expire (1 to 366). Only the master signs it. The CLI equivalent is
`ferry focus set`.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [projects, tier]
properties:
projects: {type: array, items: {type: string}}
tier: {type: string, enum: [focus, normal, background, paused]}
days: {type: integer, minimum: 1, maximum: 366}
responses:
'200': {description: 'whether anything changed'}
'400': {description: no project named, a project that is unknown or archived, a tier that means nothing, or days out of range}
'401': {description: no active session}
'403': {description: read-only, or not the master}
'404': {description: the home project's channel is not on this machine}
/api/focus/clear:
post:
summary: The master takes focus entries off
description: >
Signs a newer record without the named projects, or without any entry when none
is named, so every project reads as normal again. The CLI equivalent is
`ferry focus clear`.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
projects: {type: array, items: {type: string}}
responses:
'200': {description: 'whether anything changed'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
'404': {description: the home project's channel is not on this machine}
/api/improve/pending:
get:
summary: Improvements waiting on a key
description: >
Every improvement whose newest result is not yet approved for live: its diff stat,
what its evidence says, the review engine's verdict when it has given the first key,
and whether only the master's approval is missing (`ready_for_you`). Each card also
carries the adversary's finding beside the two keys (`adversary`: its verdict,
findings and any override, null when it has not read the work or is off) and
`adversary_blocked`, true while an unresolved Block stands. `may_decide` is true
only for the signed-in master.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'project, may_decide and waiting'}
'401': {description: no active session}
/api/improve/decide:
post:
summary: The master's key - approve an improvement for live, or send it back
description: >
Signed with the session's key. Approving is refused until the policy's review
engine has accepted the same revision, on a result whose evidence passes
verification; sending back needs notes. Nothing merges: an approved improvement is
"approved, ready to merge", and merging stays the master's act.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [order, accept]
properties:
order: {type: string}
accept: {type: boolean}
notes: {type: string}
responses:
'200': {description: 'the order, the revision decided and its state'}
'401': {description: no active session}
'403': {description: read-only, not the master, or the review engine has not given its key}
/api/suggestions:
get:
summary: Suggestions outsiders have sent, and the ones waiting for the master
description: >
The project's suggestions inbox as this fleet sees it: every suggestion taken in
from the public inbox the master named (newest first), each with the contributor's
login, kind, title, pitch and why (their words, to be shown as text), the terms
version it was sent under and whether those are still the current terms, what
triage made of it (`verdict`: decision, scores, reason, a draft of what to build),
its stage (received, clarifying, pending, accepted, building, shipped, declined,
duplicate, withdrawn, expired) and the order it became. `pending` is "needs me".
`available` is false for a project that never opened an inbox. `may_decide` is true
only for the signed-in master. The CLI equivalent is `ferry suggestions pending`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'available and, when it is, project, open, inbox, terms_version, terms_sha256, limits, signed_by, notice, waiting, may_decide, master and cards'}
'401': {description: no active session}
/api/suggestions/decide:
post:
summary: The master decides a pending suggestion
description: >
Accept, decline (with an optional reason shown to the contributor) or ask more
(with an optional question). Recorded as the master's signed answer to the signed
question triage asked, the same answer a Telegram button gives; nothing is built
here. The workers read the answer on their next pass: an accept becomes an ordinary
signed order tagged `suggestion` for the normal build and review path, and a
suggestion whose sender has not agreed to changed terms waits for that. A question
is answered once. The CLI equivalents are `ferry suggestions accept`, `decline` and
`ask`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [issue, choice]
properties:
issue: {type: integer, description: the issue number in the inbox}
choice: {type: string, enum: [accept, decline, ask]}
text: {type: string, description: a reason for decline or a question for ask}
responses:
'200': {description: 'the issue, the answer recorded and its state'}
'400': {description: a choice that is not accept, decline or ask}
'401': {description: no active session}
'403': {description: read-only, not the master (or a delegate), nothing pending for that issue, or already answered}
'409': {description: the project has no master}
/api/delegations:
get:
summary: Who may act for the master here, and whether each delegation counts
description: >
Every delegation in this project's channel (delegate, principal, scopes, issue
and expiry times, and its standing: active, expired, revoked or invalid and why),
the agents on the roster that could be delegated to (a Telegram bridge publishes
itself with the role `delegate`), and `may_set`, true only for the master.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'project, master, may_set, scopes, delegations and candidates'}
'401': {description: no active session}
post:
summary: The master lets an agent act for them
description: >
Signs a delegation with the session's key: the named agent's current key may act
for the master in the listed scopes (orders, review, improve; all three when none
are given), until revoked or, with `expires_days`, until it expires. `all` does
this in every project in the ferry root the person is master of. Everything the
delegate signs is recorded as "<master> via <delegate>". Master only.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [delegate]
properties:
delegate: {type: string}
scopes: {type: array, items: {type: string, enum: [orders, review, improve]}}
expires_days: {type: integer}
all: {type: boolean}
responses:
'200': {description: 'how many projects were delegated, and per project the outcome'}
'401': {description: no active session}
'403': {description: read-only, not the master, an unknown scope, or no key for the delegate}
/api/delegations/revoke:
post:
summary: The master ends a delegation
description: >
Writes a master-signed revocation of the agent's delegation in this project, or
with `all` in every project that has one. From then on nothing it signs counts
as the master's, including what it signed before.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [delegate]
properties:
delegate: {type: string}
all: {type: boolean}
responses:
'200': {description: 'how many delegations were revoked'}
'401': {description: no active session}
'403': {description: read-only, or not the master}
/api/contracts:
get:
summary: Every interface contract in the project
description: >
Each contract with its status (`proposed`, `locked` or `rejected`), its request and
response shapes, who proposed and locked it, and the orders on each side with the
reason a worker is holding any of them. Forged, unsigned and edited-after-lock files
are not listed. `may_decide` is true only for the signed-in master. Each contract
carries what the adversary found (`adversary`, one standing finding per moment it
has read it, empty when the adversary is off) and `lock_refusal`, the reason Lock
is refused while an unresolved Block stands, or no eligible adversary has read the
contract at all, in `blocking` mode (null otherwise). `digest` is the contract as it
reads now and `finding_digest` the adversary's word on it (`none` when no eligible
adversary has read it): a Lock must send the first, an override both, so what is
locked or overridden is what this response showed. `adversary_mode` is `off`,
`advisory` or `blocking`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'the project, its master, may_decide, adversary_mode, and the contracts'}
'401': {description: no active session}
/api/contracts/{reference}/lock:
post:
summary: The master locks a proposed contract
description: >
Freezes `name@version` with a signature of the signed-in master (or a delegate
holding the improve scope). A locked contract never changes; a change is a new
version. Also answers the question Telegram asked, so its buttons go away. With the
adversary on `blocking`, an unresolved Block (or no eligible finding) refuses this
with 403 until the master overrides or waives it; `override` signs that and locks in
one step. The body must name what the master looked at: `digest` (the contract's,
at least 8 characters) and, with `override`, `finding` (the adversary's word, or
`none`), both from GET /api/contracts. A contract or finding that changed since is
refused with 403, "the contract changed since you looked".
parameters:
- {name: reference, in: path, required: true, schema: {type: string}}
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [digest]
properties:
digest: {type: string, description: 'the contract digest from GET /api/contracts'}
override: {type: boolean}
finding: {type: string, description: 'with override: the finding_digest from GET /api/contracts, or none'}
reason: {type: string}
responses:
'200': {description: the contract, now locked}
'400': {description: not a name@version reference, or no digest (or no finding with override)}
'401': {description: no active session}
'403': {description: read-only, not the master, the contract or finding changed since you looked, or an unresolved adversary Block in blocking mode}
'404': {description: no genuine contract by that name}
'409': {description: already locked or rejected}
/api/contracts/{reference}/reject:
post:
summary: The master rejects a proposed contract
description: >
Declines `name@version`. The proposer's way forward is a new version.
parameters:
- {name: reference, in: path, required: true, schema: {type: string}}
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: the contract, now rejected}
'400': {description: not a name@version reference}
'401': {description: no active session}
'403': {description: read-only, or not the master}
'404': {description: no genuine contract by that name}
'409': {description: already locked or rejected}
/api/adversary:
get:
summary: What the adversary found in this project
description: >
The adversary is a separate model that challenges the builders' work at three
moments: before an interface contract locks, when the same order fails twice, and
before an improvement is called done. Returns the mode (`off`, `advisory` or
`blocking`), the engine first in line, and every genuine finding newest first (its
subject, revision, trigger, verdict, findings, engine, `same_engine` and any
override) and its `digest`. Forged and unsigned findings are not listed. A finding
that does not count - from the agent that built the work, from a machine with no
signed engine inventory, one the policy's `machines` does not allow - is listed
under `ignored` with its `reason`. `may_override` is true only for the signed-in
master.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
responses:
'200': {description: 'project, mode, engine, may_override, findings and ignored'}
/api/adversary/override:
post:
summary: The master goes ahead despite the adversary's Block
description: >
Signed with the session's key. For a contract it records the override, after which
Lock goes through; for an improvement it lets the review engine's key be granted
and auto-merge proceed. Only a genuine Block can be overridden, and the override is
for that revision only (the revision under decision when none is given). `finding` is
required: the finding's `digest` as the page showed it, or `none` to waive a decision
no eligible adversary has read; if what stands changed since, this is refused. The CLI
equivalent is `ferry adversary override`.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [subject, trigger, finding]
properties:
subject: {type: string, description: 'an order id, or a contract as name@version'}
trigger: {type: string, enum: [contract-lock, repeat-failure, pre-done]}
revision: {type: integer}
reason: {type: string}
finding: {type: string, description: 'the finding digest the master saw, or none'}
responses:
'200': {description: 'subject, revision, trigger, overridden_by and reason'}
'400': {description: not a trigger, or no finding named}
'401': {description: no active session}
'403': {description: read-only, not the master, the finding changed since you looked, or there is no Block to override}
'404': {description: the subject has no such moment to decide}
/api/master/claim-all:
post:
summary: The signed-in person becomes master of every project with none
description: >
The browser half of `ferry root master`. For every project in this machine's ferry
root whose channel has no master, puts the person's public key on the roster and
declares them master, signed with the session's key. A project with a master, or
whose channel knows the name by a different key, is left alone and reported. Does
not turn on required grants. The team page does the same unasked when the person
signed in is master of the project on screen.
responses:
'200': {description: 'the master, how many were declared, and per project: declared, already, other, key_conflict or error'}
'401': {description: no active session}
'403': {description: dashboard is read-only}
'404': {description: no ferry root on this machine}
/api/team/{name}/revoke:
post:
summary: End a person's access, signed by the master
description: >
Writes a master-signed revocation beside their grant and their agents' grants,
unshares the folder from devices this channel knows as theirs, and burns open
invitations in their name. Cannot unsync what already synced; the response lists
secrets sealed to them, which should be rotated.
parameters:
- {name: name, in: path, required: true, schema: {type: string}}
responses:
'200': {description: who was revoked, which devices were unshared, which secrets to rotate}
'403': {description: only the master may revoke}
'409': {description: cannot revoke yourself}
/api/team/{name}/access:
post:
summary: Sign a master grant saying what this person may do, and on which projects
parameters:
- {name: name, in: path, required: true, schema: {type: string}}
responses:
'200': {description: the grant}
'403': {description: only the channel master may grant}
'404': {description: not in this channel}
'409': {description: invited but not yet online so there is no key to grant to, or their timed access has already ended}
/api/release:
get:
summary: The release waiting for a person, with the gate's own verdict
description: >
`would_authorise` is computed by the same function the signing path calls, so the
page can never offer to approve something the gate would refuse.
responses:
'200': {description: pending request, approval, denial, superseded approval, verdict}
'401': {description: no active session}
/api/release/{version}/approve:
post:
summary: A person says yes to one exact commit
parameters:
- {name: version, in: path, required: true, schema: {type: string}}
responses:
'200': {description: the signed approval}
'401': {description: no active session}
'409': {description: the commit moved, CI is not green, or the request is stale}
/api/release/{version}/deny:
post:
summary: A person says no to one exact commit, with a reason
description: >
Never gated on CI or staleness. Those refuse a release; they must not refuse a
person's refusal.
parameters:
- {name: version, in: path, required: true, schema: {type: string}}
responses:
'200': {description: the signed denial}
'401': {description: no active session}
'409': {description: the commit moved under the page}
/api/conversations:
get:
summary: Every conversation topic, with its last line and signature status
responses:
'200': {description: topics}
'401': {description: no active session}
/api/conversations/{topic}:
get:
summary: One conversation, as a transcript
parameters:
- {name: topic, in: path, required: true, schema: {type: string}}
responses:
'200': {description: turns}
'401': {description: no active session}
/api/roster:
get:
summary: The channel roster, as signed
responses:
'200': {description: roster entries}
'401': {description: no active session}
/api/fleet:
get:
summary: Machines in the fleet and what they are doing
responses:
'200': {description: fleet status}
'401': {description: no active session}
/api/memory:
get:
summary: The shared memory bank
responses:
'200': {description: memory}
'401': {description: no active session}
/api/memory/suggest:
post:
summary: Propose an addition to the shared memory bank
responses:
'200': {description: the proposal}
'401': {description: no active session}
/api/secrets:
get:
summary: Secret names held in the channel. Never their values
responses:
'200': {description: names only}
'401': {description: no active session}
post:
summary: Set a secret, sealed and signed by the operator
responses:
'200': {description: written}
'401': {description: no active session}
/api/secrets/{name}:
delete:
summary: Remove a secret from the channel
parameters:
- {name: name, in: path, required: true, schema: {type: string}}
responses:
'200': {description: removed}
'401': {description: no active session}
/api/cost/rates:
get:
summary: Published per-engine list prices, computed offline
responses:
'200': {description: rates}
/api/cost/plan:
post:
summary: Estimate what one prompt would cost on each engine
responses:
'200': {description: estimate}
/api/team:
get:
summary: Human teammates and machine agents, returned as separate identity classes
description: >
Combines locally available operator identities with remote operator entries from
the signed channel roster. Agent ownership is deliberately null until a signed
ownership and access-policy contract exists; clients must not infer authority.
Each teammate and grant carries `access_ends` (RFC 3339, or null for access that does
not end) and `access_lapsed` (true once a timed access has run out unrenewed);
`revoked` stays true only for a revocation the master signed. `pending_access` lists
the renewal questions waiting for the master (`id`, `text`, `options`, `asked_at`).
responses:
'200': {description: current operator, channel master, human teammates, agents, grants, and pending access questions}
'401': {description: no active session}
/api/orders:
post:
summary: Issue an order, optionally carrying files, signed by the session's operator
description: >
Each file is first sent with `PUT /api/orders/{id}/files/{name}` under the order id
the caller chose; this call then copies them into the channel, records their names,
sizes and sha256 in the signed payload, and issues the order. Caps and the refusal
of secret-looking files are the same as for `ferry channel order --file`.
requestBody:
required: true
content:
application/json: {schema: {type: object}}
responses:
'200': {description: the order id and the files it carries}
'400': {description: a file is refused (too large, secret-looking, unsafe name) or the order is malformed}
'401': {description: no active session}
'403': {description: dashboard is read-only}
'409': {description: the channel refused the order (an id already used, or not allowed to issue it)}
/api/orders/{id}/files/{name}:
put:
summary: Upload one file for an order that is about to be issued
description: >
The raw bytes are the request body. The file waits in the operator's upload area
until `POST /api/orders` names it, and is removed when the order is issued.
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
- {name: name, in: path, required: true, schema: {type: string}}
responses:
'204': {description: stored}
'400': {description: unsafe name}
'401': {description: no active session}
'403': {description: dashboard is read-only}
'413': {description: over the per-file cap}
/api/tasks:
get:
summary: Summaries of every task, with signature status
responses:
'200': {description: task summaries}
/api/tasks/{id}:
get:
summary: Full detail for one task (order, claims, results, reviews)
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
responses:
'200': {description: task detail}
'404': {description: unknown task}
/api/tasks/{id}/review:
post:
summary: Accept a result or send it back with notes, signed by the session's operator
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
requestBody:
required: true
content:
application/json: {schema: {$ref: '#/components/schemas/Review'}}
responses:
'200': {description: verdict submitted}
'401': {description: no active session}
'403': {description: dashboard is read-only}
'409': {description: no result to review, or result fails the order's contract}
/api/route/simulate:
get:
summary: Where work of a kind and size would go, and why, without running anything
description: >
Runs the smart router over the engines the fleet published, under this project's
engine policy: each engine's chance of getting the work right, its price, whether it
clears the kind's threshold, and why the others are out. The winner is the cheapest
sufficient engine, or the likeliest when none is sufficient. The CLI equivalent is
`ferry route simulate --kind docs --size small --needs vision`. Order cards show the
`routing` reason a worker recorded for work already done; `GET /api/tasks/{id}` has
the whole decision per result.
parameters:
- {name: project, in: query, required: false, schema: {type: string}}
- {name: kind, in: query, required: false, schema: {type: string}, description: 'code-change, docs, tests, review, plan, chore, research, translate, transcribe, image, video, audio or other (default code-change)'}
- {name: size, in: query, required: false, schema: {type: string, enum: [small, medium, large]}, description: default medium}
- {name: needs, in: query, required: false, schema: {type: string}, description: 'extra modalities, comma separated: vision, audio_in, audio_out, image, video, embed'}
responses:
'200': {description: 'the decision (candidates, winner, reason), the lines `ferry route simulate` prints, and the order engines would be tried in'}
'400': {description: a kind, size or modality that means nothing}
'401': {description: no active session}
/api/stats:
get:
summary: Per-engine acceptance rate and estimated cost, merged into one table
responses:
'200': {description: engine stats}
/api/ledger:
get:
summary: Most recent ledger entries, newest first, with integrity flag
responses:
'200': {description: ledger entries}
/api/learnings:
get:
summary: Most recent learning records, newest first
responses:
'200': {description: learning records}