feat(config): per-service operator config + secrets (theme M1)

Services can declare an operator-configurable config block in /_manifest
(ordered array of single-key objects, pure schema: type cleartext|password,
description?, key = container env var name). Maestro:

- lib/secrets: AES-256-GCM FileStore; dedicated secrets.key (32 bytes,
  chmod 600, separate from session.key); secrets.json rows keyed
  service/field; cleartext read-write, password write-only & encrypted at
  rest, decrypted only at injection. Footure key rotation = delete the key.
- handlers/config (owner-gated via auth.RequireSession):
  GET /api/services/{service}/config (merged schema+set view, password
  values never returned), PUT/DELETE /api/services/{service}/config/{key}
  (uniform ERR_NOT_FOUND / ERR_INVALID_PAYLOAD error shape).
- start.go: injectConfigSecrets runs after injectServiceIdentity; only
  operator-set rows become env on non-oneoff services; unset fields keep
  the compose default.
- Manifest.Config carried onto Container/RegistryContainer/InstalledMetadata
  via updateInstalledMetadataFromManifest + watcher.
- Frontend: per-service Settings dialog (ServiceConfigDialog) + api
  helpers; cleartext editable, password set/unset indicator only.
- spec.yaml documents the new endpoints/schemas.
This commit is contained in:
2026-08-31 00:48:51 +01:00
parent ca4c66110e
commit a9e2182396
22 changed files with 1831 additions and 12 deletions
+166
View File
@@ -334,6 +334,14 @@ components:
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:
@@ -357,6 +365,70 @@ components:
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:
@@ -858,3 +930,97 @@ paths:
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 <dataRoot>/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 on the next start.
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.
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