openapi: 3.0.3 info: title: Dcntrlized Maestro API version: 1.0.0 description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them. Every /api/* route except /api/auth/* and the deliberately public GET /api/instance (hub-app theme) requires an instance-owner session (cookies issued by the auth endpoints); in setup mode gated routes answer 428 ERR_SETUP_REQUIRED, otherwise 401 ERR_UNAUTHENTICATED (an unreadable owner store fails closed with 500 ERR_INTERNAL). Mutating requests from browser origins are additionally checked against the request host (CSRF). components: schemas: ActionRequest: type: object properties: payload: type: object description: Action-specific parameters ActionResponse: type: object properties: status: type: string data: type: object SuccessResponse: type: object properties: status: type: string example: "success" message: type: string ApiError: type: object description: The uniform API error shape (onboarding theme M1). properties: error: type: object properties: code: type: string example: "ERR_UNAUTHENTICATED" AuthStatus: type: object description: The instance-owner gate state; the SPA bootstraps from it. properties: setup_required: type: boolean description: True until the first owner account exists. authenticated: type: boolean username: type: string Credentials: type: object required: [username, password] properties: username: type: string description: "3-64 chars: lowercase letters, digits and . _ - ; must start alphanumeric." password: type: string minLength: 10 SessionResponse: type: object description: >- Login/setup/rotation result. The access and refresh tokens ride as HttpOnly SameSite=Lax cookies (maestro_access / maestro_refresh). properties: username: type: string role: type: string example: "admin" access_ttl_s: type: integer example: 900 PasswordChange: type: object required: [current_password, new_password] properties: current_password: type: string new_password: type: string minLength: 10 PendingAuthorization: type: object description: A first-contact authorization request awaiting the instance owner (M2 §3). properties: service: type: string example: "keepnote" fingerprint: type: string example: "sha256:aa:bb:cc:dd" first_seen: type: string last_seen: type: string AuthorizedKey: type: object description: An approved peer row of the auth service's trust store. properties: service: type: string fingerprint: type: string approved_by: type: string approved_at: type: string revoked_at: type: string nullable: true PeerAction: type: object required: [service, fingerprint] properties: service: type: string description: The service name (cert CN / org.dcntrlized.service). fingerprint: type: string description: The SHA256 public-key fingerprint (sha256:…) shown to the owner. Attestation: type: object description: The auth service's public signing key statement (D3). properties: public_key: type: string fingerprint: type: string example: "sha256:…" kid: type: string issued_at: type: string InstanceIdentity: type: object description: The instance's public self-description (hub-app M1). required: [name] properties: name: type: string example: "home-node" title: type: string description: type: string fingerprint: type: string description: Instance CA public-key SHA256 (lib/pki); omitted when no CA is provisioned. example: "sha256:…" PublicService: type: object description: | Deliberately reduced view of a running service — no container id, docker name, private IP, or port mapping leaves the instance (hub-app D3). required: [service_name, status, image] properties: service_name: type: string title: type: string description: type: string status: type: string version: type: string capabilities: type: array items: type: string has_auth: type: boolean image: type: string TrustedInstance: type: object description: A remote instance this instance trusts (federation store row). required: [name, url, fingerprint, status] properties: name: type: string url: type: string fingerprint: type: string example: "sha256:…" status: type: string example: "authorized" scopes: type: array items: type: string InstancePublicProfile: type: object description: | The unauthenticated GET /api/instance response (hub-app theme). With DCNTRLIZED_PUBLIC_PROFILE=false only `instance` is returned. Absent federation store → trusted_instances:null; present-but-empty → []. required: [instance] properties: instance: $ref: '#/components/schemas/InstanceIdentity' services: type: array nullable: true items: $ref: '#/components/schemas/PublicService' trusted_instances: type: array nullable: true items: $ref: '#/components/schemas/TrustedInstance' RegistryContainer: type: object required: [name, image] properties: name: type: string example: "keepnote" version: type: string example: "0.0.1" image: type: string example: "192.168.122.183:3000/dcntrlized/keepnote:dev" digest: type: string title: type: string example: "KeepNote" vendor: type: string author: type: string description: type: string source: type: string revision: type: string licenses: type: string url: type: string documentation: type: string created_at: type: string hidden: type: boolean description: Hidden infrastructure containers (e.g. the shared dcntrlized-db) are excluded from the service grid. example: true PortMapping: type: object properties: host: type: integer guest: type: integer IPAddress: type: object properties: version: type: string enum: [v4, v6] address: type: string Network: type: object properties: name: type: string ipAddress: $ref: '#/components/schemas/IPAddress' mode: type: string enum: [bridge, host, none, overlay, ipvlan, macvlan] ports: type: array items: $ref: '#/components/schemas/PortMapping' Container: type: object properties: id: type: string name: type: string service_name: type: string role: type: string example: "db" hidden: type: boolean description: Hidden infrastructure containers (e.g. the shared dcntrlized-db) are excluded from the service grid. capabilities: type: array items: type: string example: "database" description: Capabilities declared by the service manifest (e.g. database). has_auth: type: boolean description: >- Whether the service consumes the shared auth service (manifest has_auth). True for dcntrlized-auth itself and for auth-consuming services. public_key: type: string description: >- The auth service's RS256 public key (manifest public_key); the hook for a future Maestro dashboard auth that validates dcntrlized-auth tokens. config: type: array description: >- Operator-configurable fields the service declares in its manifest config block (Service Config & Secrets theme). Each element is a single-key object: the key names the field, the value is its spec. items: $ref: '#/components/schemas/ServiceConfigDeclaration' docker_name: type: string image: type: string version: type: string status: type: string created_at: type: string network: $ref: '#/components/schemas/Network' title: type: string vendor: type: string author: type: string description: type: string source: type: string ServiceConfigField: type: object description: >- The spec of one operator-configurable field declared by a service's manifest config block (Service Config & Secrets theme). The manifest is a pure schema — it never carries a value. required: [type, key] properties: type: type: string enum: [cleartext, password] description: cleartext is read-write; password is write-only and encrypted at rest. description: type: string description: Optional human-readable explanation shown in the UI. key: type: string description: The container env var name Maestro injects the value under. ServiceConfigDeclaration: type: object description: >- A single-key config declaration inside a service manifest's config array. The key names the field; the value is the field spec. additionalProperties: $ref: '#/components/schemas/ServiceConfigField' minProperties: 1 maxProperties: 1 ServiceConfigEntry: type: object description: >- The read-only merged view of one config field served by GET /api/services/{service}/config: the schema plus the operator's set state. Password values are never included; a cleartext value (read-write) is. required: [field, key, type, set] properties: field: type: string description: The manifest config field name (the config array element key). key: type: string description: The container env var name. type: type: string enum: [cleartext, password] description: type: string set: type: boolean description: Whether a value is stored. The only thing a password row reveals. value: type: string description: The stored value of a cleartext field; never present for passwords. ServiceConfigValue: type: object description: Request body for setting a config value (PUT .../config/{key}). required: [value] properties: value: type: string paths: /api/actions/{containerId}/{actionName}: parameters: - name: actionName in: path required: true description: The name of the specific action to execute schema: type: string - name: containerId in: path required: true description: The ID or name of the target container/service. Accepts a full or partial container ID, a container name, or the service name (matched case-insensitively). schema: type: string get: summary: Retrieve data or status from a service action tags: ["actions"] description: Forwards the request (method, headers, query string, and body) to the target container's /action/{actionName} endpoint. The target service is responsible for authentication; any client-supplied auth headers are forwarded as-is. responses: '200': description: Request successful content: application/json: schema: $ref: '#/components/schemas/ActionResponse' '400': description: Bad Request - Invalid input parameters '401': description: Unauthorized - the target service rejected the request '404': description: Action or container not found '500': description: Failed to forward the request to the container post: summary: Execute a state-changing action in the service tags: ["actions"] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ActionRequest' responses: '200': description: Action executed successfully content: application/json: schema: $ref: '#/components/schemas/ActionResponse' '400': description: Bad Request - Invalid input parameters '401': description: Unauthorized - the target service rejected the request '404': description: Action or container not found '500': description: Failed to forward the request to the container put: summary: Update configuration for a specific action tags: ["actions"] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ActionRequest' responses: '200': description: Configuration updated '400': description: Bad Request - Invalid input parameters '401': description: Unauthorized - the target service rejected the request '404': description: Action or container not found '500': description: Failed to forward the request to the container delete: tags: ["actions"] summary: Reset or remove an action's state requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ActionRequest' responses: '200': description: Action state reset '400': description: Bad Request - Invalid input parameters '401': description: Unauthorized - the target service rejected the request '404': description: Action or container not found '500': description: Failed to forward the request to the container /api/containers: get: summary: List the services Maestro manages tags: ["containers"] responses: '200': description: List of services content: application/json: schema: type: array items: $ref: '#/components/schemas/Container' '500': description: Internal server error /api/containers/install: post: summary: Install a service container (downloads the compose file and pulls the image) tags: ["containers"] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegistryContainer' responses: '200': description: Container downloaded and image pulled successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Invalid JSON payload or missing required fields (name, image) '500': description: Failed to install container /api/containers/uninstall: post: summary: Uninstall a service container and remove it from the cache tags: ["containers"] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: '200': description: Container uninstalled successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Invalid JSON payload or missing required field (name) '500': description: Failed to uninstall container /api/containers/start: post: summary: Start a previously installed service container tags: ["containers"] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: '200': description: Container started successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Invalid JSON payload or missing required field (name) '500': description: Failed to start container /api/containers/stop: post: summary: Stop a running service container tags: ["containers"] requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string responses: '200': description: Container stopped successfully content: application/json: schema: $ref: '#/components/schemas/SuccessResponse' '400': description: Invalid JSON payload or missing required field (name) '500': description: Failed to stop container /api/registry: get: summary: List the services available in the Dcntrlized registry tags: ["registry"] responses: '200': description: List of registry services content: application/json: schema: type: array items: $ref: '#/components/schemas/RegistryContainer' '500': description: Internal server error '502': description: Failed to connect to upstream registry /api/instance: get: summary: Public instance profile (unauthenticated) tags: ["instance"] security: [] description: >- The one read surface the owner gate deliberately allows through (hub-app theme M1/D2): what this instance is, which non-hidden services it runs (public fields only), and which remote instances it trusts. DCNTRLIZED_PUBLIC_PROFILE=false omits services + trusted_instances. responses: '200': description: The instance public profile content: application/json: schema: $ref: '#/components/schemas/InstancePublicProfile' /api/auth/status: get: summary: Instance-owner gate state (public) tags: ["auth"] description: >- The SPA bootstraps from this endpoint. All other /api/* routes are gated by the instance-owner session (onboarding theme M1): in setup mode they answer 428 ERR_SETUP_REQUIRED, otherwise 401 ERR_UNAUTHENTICATED without a valid session; an unreadable owner store fails closed with 500 ERR_INTERNAL everywhere. responses: '200': description: Gate state content: application/json: schema: $ref: '#/components/schemas/AuthStatus' '500': description: Internal server error /api/auth/setup: post: summary: Create the first owner account (admin) tags: ["auth"] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Credentials' responses: '200': description: Owner created; session cookies issued content: application/json: schema: $ref: '#/components/schemas/SessionResponse' '400': description: Invalid credentials (ERR_INVALID_REQUEST / ERR_INVALID_CREDENTIALS) '409': description: Setup already completed (ERR_SETUP_COMPLETED) '429': description: Rate limited (ERR_RATE_LIMITED) '500': description: Owner store unreadable/unwritable (ERR_INTERNAL) /api/auth/login: post: summary: Log in as the instance owner tags: ["auth"] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Credentials' responses: '200': description: Session cookies issued content: application/json: schema: $ref: '#/components/schemas/SessionResponse' '400': description: Invalid JSON payload '401': description: Unknown user or wrong password (ERR_INVALID_CREDENTIALS) '429': description: Rate limited (ERR_RATE_LIMITED) with Retry-After '500': description: Owner store unreadable (ERR_INTERNAL) /api/auth/refresh: post: summary: Rotate the access/refresh pair tags: ["auth"] description: >- Uses the maestro_refresh cookie. Reuse of an already-rotated jti is a theft signal and revokes every session of the user (ERR_SESSION_REVOKED). responses: '200': description: Fresh pair issued content: application/json: schema: $ref: '#/components/schemas/SessionResponse' '401': description: Missing/invalid/replayed refresh cookie /api/auth/logout: post: summary: Revoke the current refresh token and clear cookies tags: ["auth"] responses: '200': description: Logged out (idempotent, bodyless-safe) /api/auth/change-password: post: summary: Change the owner's password (session required) tags: ["auth"] description: Revokes every existing session of the user on success. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PasswordChange' responses: '200': description: Password changed; all sessions revoked '400': description: Invalid payload or wrong current password '401': description: Not authenticated '429': description: Rate limited (ERR_RATE_LIMITED) '500': description: Owner store unreadable/unwritable (ERR_INTERNAL) /api/ws: get: summary: WebSocket endpoint for live container events tags: ["containers"] description: >- Establishes a WebSocket connection that streams container lifecycle events as JSON. The upgrade is gated by the instance-owner session; anonymous handshakes close with 401 (428 in setup mode). responses: '101': description: Switching Protocols - WebSocket upgrade '401': description: Not authenticated '428': description: Setup required /api/peers: get: summary: List pending authorizations and the approved peer trust store tags: ["security"] description: > Security theme M2 §3. Returns the shared auth service's first-contact authorization requests (services whose certificate is valid but has not been approved by the instance owner yet) and the approved keys. Maestro is only the UI; the auth service owns the enforcement. responses: '200': description: Combined pending + approved trust state content: application/json: schema: type: object properties: pending: type: array items: $ref: '#/components/schemas/PendingAuthorization' authorized: type: array items: $ref: '#/components/schemas/AuthorizedKey' '500': description: Internal server error (auth management client unavailable) '502': description: Failed to reach the shared auth service /api/peers/attestation: get: summary: Read the auth service's public signing key statement tags: ["security"] description: > The auth service's RSA public key, its kid, and the sha256 fingerprint peers should pin after first contact (TOFU, D3). responses: '200': description: Auth public key attestation content: application/json: schema: $ref: '#/components/schemas/Attestation' '502': description: Failed to reach the shared auth service /api/peers/approve: post: summary: Approve a peer's fingerprint in the auth service trust store tags: ["security"] description: > Forwards the instance owner's approval for a service's identity fingerprint. After this call the service's requests to /auth/* proceed normally (enforced by the auth service itself). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PeerAction' responses: '200': description: Peer approved '400': description: Missing service or fingerprint '502': description: Failed to reach the shared auth service /api/peers/revoke: post: summary: Revoke a peer's fingerprint (returns it to the no-op path) tags: ["security"] description: > Forwards the instance owner's revocation. Calls from that fingerprint become uniform 403 no-ops again immediately (the SSH authorized_keys "delete the line" action). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PeerAction' responses: '200': description: Peer revoked '400': description: Missing service or fingerprint '502': description: Failed to reach the shared auth service /api/peers/reject: post: summary: Reject a pending peer authorization request tags: ["security"] description: > Denies a first-contact authorization request without authorizing it. The auth service drops the request from the pending list and audits the decision; the peer stays on the no-op path (a later contact simply re-records a fresh pending request). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PeerAction' responses: '200': description: Pending request rejected '400': description: Missing service or fingerprint '502': description: Failed to reach the shared auth service /api/services/{service}/config: parameters: - name: service in: path required: true description: >- The service (matched case-insensitively against the install record / container label, as in /api/containers). schema: type: string get: summary: Merge a service's declared config schema with its stored values tags: ["service-config"] description: >- Owner-gated (Service Config & Secrets theme M1). Lists each field the service's manifest config block declares, merged with the operator's set state. Password fields are only ever reported as set:false/true; cleartext fields (read-write) carry their stored value. responses: '200': description: The merged per-field view content: application/json: schema: type: array items: $ref: '#/components/schemas/ServiceConfigEntry' '401': description: ERR_UNAUTHENTICATED - no valid owner session '404': description: ERR_NOT_FOUND - unknown service '428': description: ERR_SETUP_REQUIRED - owner setup not completed /api/services/{service}/config/{key}: parameters: - name: service in: path required: true schema: type: string - name: key in: path required: true description: The manifest config field name (the config array element key). schema: type: string put: summary: Set a service config value tags: ["service-config"] description: >- Owner-gated (Service Config & Secrets theme M1). Stores the value for the field declared by the service manifest. Cleartext fields are stored plaintext (read-write); password fields are AES-256-GCM encrypted at rest, keyed by /maestro/secrets.key, and never returned again — the response is only a ServiceConfigEntry with set:true. The value is injected as the field's env key at start, and, when the service is running and exposes POST /config (D6 write-back), delivered to it at runtime. Delivery always targets the service's dedicated Maestro peer listener (org.dcntrlized.peer_port label, default 9443) over https with Maestro's own mTLS identity — CN=maestro is how the service's /config gate authorizes the caller. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ServiceConfigValue' responses: '200': description: Value stored content: application/json: schema: $ref: '#/components/schemas/ServiceConfigEntry' '400': description: ERR_INVALID_PAYLOAD - unparseable body or empty password '401': description: ERR_UNAUTHENTICATED - no valid owner session '404': description: ERR_NOT_FOUND - unknown service or config field '428': description: ERR_SETUP_REQUIRED - owner setup not completed delete: summary: Clear a service config value tags: ["service-config"] description: >- Owner-gated (Service Config & Secrets theme M1). Removes the stored value; on the next start the compose file's own default applies. When the service is running and exposes POST /config (D6 write-back), a best-effort clear {field, action:"clear"} is delivered at runtime (same https + Maestro mTLS identity edge, to the service's peer port, 9443 by default) so the running process drops the value immediately instead of waiting for a restart. responses: '200': description: Value cleared '401': description: ERR_UNAUTHENTICATED - no valid owner session '404': description: ERR_NOT_FOUND - unknown service or config field '428': description: ERR_SETUP_REQUIRED - owner setup not completed