openapi: 3.1.0 info: title: Ferryman API version: 0.1.0 servers: [{url: http://127.0.0.1:8787}] components: securitySchemes: ProjectBearer: type: http scheme: bearer description: Project-scoped operator token. ActorBearer: type: http scheme: bearer description: Eight-hour communications actor token returned once by the actor token endpoint. schemas: Error: {type: object, required: [code, message], properties: {code: {type: string}, message: {type: string}}} Job: {type: object, required: [id, project_id, status, input], properties: {id: {type: string}, project_id: {type: string}, status: {type: string, enum: [pending_approval, queued, leased, succeeded, failed, cancelled]}, input: {type: object}, policy: {type: object}}} Artifact: {type: object, required: [id, job_id, sha256, content_type, byte_len], properties: {id: {type: string}, job_id: {type: string}, sha256: {type: string}, content_type: {type: string}, byte_len: {type: integer}}} CommunicationParticipant: type: object additionalProperties: false required: [name, role, capabilities] properties: name: {type: string, pattern: '^[A-Za-z0-9._-]+$', maxLength: 128} role: {type: string, pattern: '^[A-Za-z0-9._-]+$', maxLength: 128} capabilities: type: array maxItems: 128 items: {type: string, minLength: 1, maxLength: 128} CommunicationMessage: type: object required: [format, id, project_id, idempotency_key, sender, recipient, created_at, acknowledgement_deadline, payload_reference, payload, reply_required] properties: format: {type: string, const: ferryman-message/v1} id: {type: string, format: uuid} project_id: {type: string} idempotency_key: {type: string, maxLength: 256} sender: {type: string, maxLength: 128} recipient: {type: string, maxLength: 128} created_at: {type: string, format: date-time} acknowledgement_deadline: {type: string, format: date-time} payload_reference: {type: string, maxLength: 2048} payload: description: JSON limited to 256 KiB after serialization; credential-bearing keys are rejected recursively. reply_required: {type: boolean} CommunicationStatus: type: object required: [project_id, external_probes_performed, local_health, shared_health, git_health, git_live, git_inbound_active, preferred_successes, outbox_depth, acknowledgement_outbox_depth, quarantine_files, git_backoff_attempt, updated_at] properties: project_id: {type: string} external_probes_performed: {type: boolean} local_health: {type: string, enum: [healthy, unavailable, rate_limited]} shared_health: {type: string, enum: [healthy, unavailable, rate_limited]} git_health: {type: string, enum: [healthy, unavailable, rate_limited]} git_live: {type: boolean} git_inbound_active: {type: boolean} preferred_successes: {type: integer, minimum: 0} outbox_depth: {type: integer, minimum: 0} acknowledgement_outbox_depth: {type: integer, minimum: 0} oldest_outbox_age_seconds: {type: [integer, 'null'], minimum: 0} quarantine_files: {type: integer, minimum: 0} git_backoff_attempt: {type: integer, minimum: 0} git_retry_after_unix_ms: {type: [integer, 'null'], minimum: 0} updated_at: {type: string, format: date-time} CommunicationActorRegistration: type: object required: [project_id, actor, actor_token, expires_at] properties: project_id: {type: string} actor: {type: string} actor_token: {type: string, writeOnly: true} expires_at: {type: string, format: date-time} paths: /healthz: {get: {responses: {'200': {description: health}}}} /v1/projects: post: {summary: Create a project (local setup endpoint), requestBody: {required: true, content: {application/json: {schema: {type: object, required: [id,name,token]}}}}, responses: {'201': {description: created}, '400': {description: invalid, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}}} /v1/projects/{project_id}/communications: parameters: [{name: project_id, in: path, required: true, schema: {type: string}}] post: {security: [{ProjectBearer: []}], summary: Configure the outer-local and inner-portable project communications mapping, responses: {'200': {description: project route}, '400': {description: unsafe or mismatched route}}} get: {security: [{ProjectBearer: []}], summary: Read the project communications mapping, responses: {'200': {description: project route}, '404': {description: no mapping}}} delete: security: [{ProjectBearer: []}] summary: Unregister the hub mapping and revoke actor tokens without deleting files responses: '204': {description: mapping removed; attachment files remain} '409': {description: refused because durable outbox items remain} /v1/projects/{project_id}/communications/messages: parameters: [{name: project_id, in: path, required: true, schema: {type: string}}] post: security: [{ProjectBearer: []}] summary: Durably enqueue and deliver a project-scoped message requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [sender, recipient] properties: sender: {type: string, maxLength: 128} recipient: {type: string, maxLength: 128} payload_reference: {type: string, default: inline, maxLength: 2048} payload: {default: null} reply_required: {type: boolean, default: false} idempotency_key: {type: [string, 'null'], maxLength: 256} acknowledgement_timeout_seconds: {type: integer, minimum: 5, maximum: 3600, default: 30} responses: '201': {description: stable message and delivery receipt} '400': {description: invalid route, payload, or idempotency conflict} get: {security: [{ProjectBearer: []}], summary: List portable project messages, responses: {'200': {description: messages}}} /v1/projects/{project_id}/communications/messages/{message_id}/claim: parameters: - {name: project_id, in: path, required: true, schema: {type: string}} - {name: message_id, in: path, required: true, schema: {type: string, format: uuid}} post: security: [{ActorBearer: []}] summary: Atomically claim a message idempotency key before execution requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [recipient] properties: {recipient: {type: string}} responses: '200': {description: claim result; claimed false means do not execute} '401': {description: missing, expired, or wrong actor token} /v1/projects/{project_id}/communications/messages/{message_id}/acknowledge: parameters: - {name: project_id, in: path, required: true, schema: {type: string}} - {name: message_id, in: path, required: true, schema: {type: string, format: uuid}} post: security: [{ActorBearer: []}] summary: Record a separate acknowledgement and retire the outbox item requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [recipient] properties: {recipient: {type: string}} responses: '200': {description: acknowledgement, idempotent record result, and durable delivery receipt} '401': {description: missing, expired, or wrong actor token} /v1/projects/{project_id}/communications/actors/{actor}/token: parameters: - {name: project_id, in: path, required: true, schema: {type: string}} - {name: actor, in: path, required: true, schema: {type: string, pattern: '^[A-Za-z0-9._-]+$'}} post: security: [{ProjectBearer: []}] summary: Mint an eight-hour token for one registered communications actor responses: '201': description: actor token returned once content: application/json: schema: {$ref: '#/components/schemas/CommunicationActorRegistration'} /v1/projects/{project_id}/communications/actors/{actor}/messages: parameters: - {name: project_id, in: path, required: true, schema: {type: string}} - {name: actor, in: path, required: true, schema: {type: string, pattern: '^[A-Za-z0-9._-]+$'}} get: security: [{ActorBearer: []}] summary: List only messages addressed to this actor name or registered role parameters: - {name: synchronize, in: query, schema: {type: boolean, default: true}, description: Pull inbound private-Git state when shared delivery is unavailable or Git inbound mode is active.} responses: '200': description: actor-scoped inbox content: application/json: schema: type: object required: [items, synchronized_via] properties: items: type: array items: {$ref: '#/components/schemas/CommunicationMessage'} synchronized_via: {type: [string, 'null'], enum: [local_filesystem, shared_folder, private_git, null]} '401': {description: missing, expired, or wrong actor token} /v1/projects/{project_id}/communications/reconcile: parameters: [{name: project_id, in: path, required: true, schema: {type: string}}] post: {security: [{ProjectBearer: []}], summary: Reconcile the durable outbox and transport failover state immediately, responses: {'200': {description: delivery receipts}}} /v1/projects/{project_id}/communications/status: parameters: [{name: project_id, in: path, required: true, schema: {type: string}}] get: security: [{ProjectBearer: []}] summary: Inspect transport health, failover state, queue age, and quarantine parameters: - {name: probe_external, in: query, schema: {type: boolean, default: true}, description: Set false for a strictly local snapshot without Syncthing or private-Git health probes.} responses: '200': description: current communications status content: application/json: schema: {$ref: '#/components/schemas/CommunicationStatus'} /v1/projects/{project_id}/jobs: parameters: [{name: project_id, in: path, required: true, schema: {type: string}}] post: {security: [{ProjectBearer: []}], summary: Submit an idempotent job, responses: {'202': {description: accepted, content: {application/json: {schema: {$ref: '#/components/schemas/Job'}}}}}} get: {security: [{ProjectBearer: []}], summary: List jobs, parameters: [{name: limit, in: query, schema: {type: integer}}, {name: cursor, in: query, schema: {type: string}}, {name: status, in: query, schema: {type: string}}], responses: {'200': {description: jobs}}} /v1/projects/{project_id}/agents: post: {security: [{ProjectBearer: []}], summary: Create a project-role-derived agent and its Markdown profile, requestBody: {required: true, content: {application/json: {schema: {type: object, required: [role,description,persistence], properties: {role: {type: string}, description: {type: string}, persistence: {type: string, enum: [temporal, permanent]}}}}}}, responses: {'201': {description: agent created}}} get: {security: [{ProjectBearer: []}], summary: List project agents and profile paths, responses: {'200': {description: agents}}} /v1/projects/{project_id}/memory: post: {security: [{ProjectBearer: []}], summary: Append bridge-owned project recovery memory, requestBody: {required: true, content: {application/json: {schema: {type: object, required: [category,content], properties: {category: {type: string}, content: {type: string}, source: {type: string}}}}}}, responses: {'201': {description: entry created}}} get: {security: [{ProjectBearer: []}], summary: Read project recovery memory, parameters: [{name: limit, in: query, schema: {type: integer}}], responses: {'200': {description: entries}}} /v1/projects/{project_id}/continuity-packs: post: {security: [{ProjectBearer: []}], summary: Create an authenticated encrypted local continuity pack, responses: {'201': {description: pack created}, '500': {description: recovery key unavailable or integrity failure}}} /v1/projects/{project_id}/continuity-packs/{pack_hash}/recover: post: {security: [{ProjectBearer: []}], summary: Verify and reconstruct a read-only recovery workspace without dispatching work, parameters: [{name: pack_hash, in: path, required: true, schema: {type: string}}], responses: {'200': {description: resume briefing}, '500': {description: corrupt or unauthenticated pack}}} /v1/projects/{project_id}/continuity-packs/{pack_hash}/delivery-consents: post: {security: [{ProjectBearer: []}], summary: Create the exact consent required to send an encrypted pack to configured private Git, responses: {'201': {description: pending recovery-upload consent}}} /v1/projects/{project_id}/continuity-packs/{pack_hash}/deliver: post: {security: [{ProjectBearer: []}], summary: Send an approved encrypted pack and manifest to the configured private Git recovery branch, responses: {'200': {description: verified remote receipt}}} /v1/projects/{project_id}/recovery-drill: post: {security: [{ProjectBearer: []}], summary: Create, verify, and recover a test continuity pack without dispatching work, responses: {'200': {description: drill result}}} /v1/projects/{project_id}/timeline: get: {security: [{ProjectBearer: []}], summary: Cursor-paginated append-only decision timeline, parameters: [{name: after, in: query, schema: {type: integer}}, {name: category, in: query, schema: {type: string}}, {name: job_id, in: query, schema: {type: string}}], responses: {'200': {description: ordered events}}} /v1/projects/{project_id}/policy/simulate: post: {security: [{ProjectBearer: []}], summary: Non-mutating capability, consent, quota, and retention-policy simulation, responses: {'200': {description: simulation result}}} /v1/projects/{project_id}/outbound-submissions: post: {security: [{ProjectBearer: []}], summary: Create an immutable pending-consent outbound manifest; performs no delivery, responses: {'201': {description: pending submission manifest}}} /v1/projects/{project_id}/improvement-proposals: post: {security: [{ProjectBearer: []}], summary: Create a reviewable improvement proposal and pending consent; never modifies source, responses: {'201': {description: proposal only}}} /v1/projects/{project_id}/jobs/{job_id}/events: get: {security: [{ProjectBearer: []}], summary: Stream job events with Server-Sent Events, responses: {'200': {description: event stream, content: {text/event-stream: {schema: {type: string}}}}}} /v1/projects/{project_id}/jobs/{job_id}: get: {security: [{ProjectBearer: []}], summary: Inspect one job, responses: {'200': {description: job, content: {application/json: {schema: {$ref: '#/components/schemas/Job'}}}}, '404': {description: missing}}} /v1/projects/{project_id}/jobs/{job_id}/approve: post: {security: [{ProjectBearer: []}], summary: Approve a pending job, responses: {'200': {description: approved job}, '404': {description: job is not awaiting approval}}} /v1/projects/{project_id}/jobs/{job_id}/cancel: post: {security: [{ProjectBearer: []}], summary: Cancel a queued or leased job, responses: {'200': {description: cancelled job}, '404': {description: job cannot be cancelled}}} /v1/projects/{project_id}/jobs/{job_id}/artifacts: post: {security: [{ProjectBearer: []}], summary: Store an artifact body, requestBody: {required: true, content: {application/octet-stream: {schema: {type: string, format: binary}}}}, responses: {'201': {description: stored artifact, content: {application/json: {schema: {$ref: '#/components/schemas/Artifact'}}}}}} get: {security: [{ProjectBearer: []}], summary: List a job's artifact metadata, responses: {'200': {description: artifacts}}} /v1/projects/{project_id}/artifacts/{artifact_id}: get: {security: [{ProjectBearer: []}], summary: Inspect artifact metadata, responses: {'200': {description: artifact, content: {application/json: {schema: {$ref: '#/components/schemas/Artifact'}}}}}} /v1/projects/{project_id}/artifacts/{artifact_id}/content: get: {security: [{ProjectBearer: []}], summary: Download artifact bytes, responses: {'200': {description: artifact content, content: {application/octet-stream: {schema: {type: string, format: binary}}}}}} /v1/metrics: get: {summary: Local queue, acknowledgement queue, quarantine, and worker-health metrics, responses: {'200': {description: metrics}}} /v1/projects/{project_id}/workers/{worker_id}/lease: post: {security: [{ProjectBearer: []}], summary: Atomically lease one ready job, responses: {'200': {description: lease}, '204': {description: no job}}}