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 --review --auto-merge `, and for the adversary `ferry engines policy set --role adversary --prefer --adversary `. 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 " via ". 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}