Authoritative source: each NodeModule ships a manifest.yaml that describes
its filesystem content selection, declared dependencies, init lifecycle hooks,
and (added in the Decentralized Federation work) service definitions.
The platform's System::ManifestImportService parses this YAML and writes
both the NodeModule fields (mask, file_spec, etc.) and the new
system_module_services rows (Decentralized Federation plan §A).
The Go on-node agent reads the same YAML directly to launch services. The structured DB rows exist for query workloads (Platform Infrastructure dashboard, scaling composer, service discovery) — they are NOT the on-node runtime source.
schema_version: 1
# Identity (validated against the platform's NodeModule.name)
name: powernode-hub-backend
display_name: "Powernode Hub Backend"
description: "Rails 8 API server + ActionCable channel"
license: MIT
# Content selection (rsync-glob lines)
mask: []
file_spec:
- "/opt/powernode-rails/**"
- "/etc/systemd/system/powernode-backend.service"
package_spec:
- ruby3.3
- bundler
- libpq-dev
dependency_spec: []
protected_spec:
- "/etc/systemd/system/powernode-backend.service"
# Module-to-module dependencies (Gitea repo @ version constraint)
dependencies:
requires:
- powernode/runtime-ruby@^1.0
- powernode/postgres-primary@^1.0
provides:
- rails-runtime
# Module-wide init lifecycle (kept for legacy modules; new modules should
# use the `services:` key instead)
init:
start: "/usr/sbin/service powernode-backend start"
stop: "/usr/sbin/service powernode-backend stop"
restart: "/usr/sbin/service powernode-backend restart"
reboot_required: false
security:
capabilities: [CAP_NET_BIND_SERVICE]
egress_allow: []
privileged: false
skills: [] # ModuleSkillRegistrar consumes this
build:
ubuntu_digest: null
apt_snapshot: "20260514T000000Z"A module can declare one or more services that the agent should run.
Each service maps to one system_module_services row.
services:
- name: rails
start_command: "bundle exec puma -C config/puma.rb"
stop_command: "kill -SIGTERM $MAINPID" # optional
restart_policy: always # always | on-failure | never
user: powernode # optional; defaults to agent's user
working_directory: /opt/powernode-rails # optional
env:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "1"
BACKEND_API_URL: "http://localhost:3000"
exposed_ports:
- { port: 3000, protocol: tcp, name: http }
capabilities: [] # Linux capabilities to retain
health:
endpoint: /up # optional; omit for non-HTTP services
method: GET # GET | POST | PUT
interval_seconds: 30
timeout_seconds: 5
initial_delay_seconds: 10
dependencies:
- { service: postgres, kind: requires_health } # references another service IN THIS MANIFEST
metadata: {} # forward-compat free-formnameis required, unique within the manifest's services list, max 100 chars.start_commandis required (non-empty string).restart_policy(if present) must be one ofalways | on-failure | never.health.method(if present) must be one ofGET | POST | PUT.dependencies[*].servicemust reference another service declared in the same manifest (cross-module service dependencies are NOT supported — modules depend on modules, services depend on services within a module).dependencies[*].kind(if present) must be one ofstart_before | requires_health | softdep.
ManifestImportService.import! is idempotent:
- Re-importing the same manifest updates existing
ModuleServicerows by matching on(node_module, name). - Re-importing a manifest with a service removed deletes the orphaned row (manifest YAML is the authoritative source).
- Cross-service dependencies that disappear from the manifest delete the
corresponding
ModuleServiceDependencyedge.
This means: edit the manifest, re-publish, run system.import_manifest MCP
action (or the equivalent operator path) — the platform's view converges on
the new manifest without manual cleanup.
The Go agent reads manifest.yaml directly from the OCI artifact at module
attach time. It does NOT query the platform's system_module_services rows.
This separation means:
- The platform's view (DB rows) drives operator UX + scaling decisions.
- The on-node view (manifest.yaml) drives actual service execution.
- Both views derive from the same authoritative source (the OCI artifact's manifest), so they cannot diverge as long as ingestion is correctly triggered when manifest_yaml changes.
If the operator edits manifest_yaml in the dashboard and saves: the platform
runs ManifestImportService.import! to refresh the DB rows immediately;
the agent picks up the change on its next module-attach cycle (typically the
next reconcile tick).
docs/federation/SOCIAL_CONTRACT.md(v1 ships in P4) — operator commitments around manifest accuracydocs/federation/REVERSE_PROXY_GUIDE.md(P2.5) — how Traefik consumesexposed_portsfrom these rowsdocs/federation/OPERATOR_GUIDE.md(P7) — dashboard-side service operations