This directory is the canonical layout for a Powernode module source repository.
Fork it (or use pnmod init once the SDK ships in M-MK-1) to create a new module.
.
├── Containerfile # Stage 1 builder — installs package_spec + copies rootfs/
├── manifest.yaml # Authoring-time hints (name, license, security policy, etc.)
├── rootfs/ # Files shipped verbatim into the module's filesystem
│ └── .gitkeep
├── .gitea/workflows/build.yaml # Two-stage CI (builder + composer)
└── README.md
- Author: edit
Containerfile,manifest.yaml, and add files underrootfs/. - Register: in the Powernode UI, create a
NodeModuleand setgitea_repo_full_nameto this repo's path. Generate + paste awebhook_secret. Configure the secret in this repo's settings asPOWERNODE_WEBHOOK_SECRET(Actions secret) so the workflow can sign back. - Build: push a tag (
v1.0.0) OR trigger via Powernode (which dispatches workflow_dispatch withrsync_spec+package_spec+fingerprint). - Sign + push: the workflow builds, runs syft (SBOM) + grype (VEX), composefs-encodes, fs-verity-hashes, cosign-signs (Sigstore keyless), and pushes the artifact to the configured OCI registry.
- Ingest: the workflow's final step posts back to Powernode's
/api/v1/system/webhooks/gitea/module, which triggersModuleOciIngestService. A newNodeModuleVersion+ per-archModuleArtifactrows land in the database.
The four glob-spec fields (mask, file_spec, package_spec, dependency_spec)
on the System::NodeModule record in the platform are authoritative for builds.
The manifest.yaml is consulted only on first-import to seed those fields; once
they're set in the platform, edit them via the UI / API and dispatch a new build.
The platform computes the effective rsync_spec at dispatch time, accounting for higher-priority neighbor modules' file_spec carve-outs (this is why the same module can deploy slightly different content depending on what other modules sit beside it in a particular template's union mount).
| Secret | Purpose |
|---|---|
GITEA_PUSH_USERNAME |
OCI registry login |
GITEA_PUSH_TOKEN |
OCI registry token (write to packages scope) |
POWERNODE_WEBHOOK_SECRET |
HMAC secret matching NodeModule.webhook_secret |
| Var | Default | Purpose |
|---|---|---|
POWERNODE_OCI_REGISTRY |
registry.example.com |
Where to push the artifact |
POWERNODE_WEBHOOK_URL |
https://platform.example.com/api/v1/system/webhooks/gitea/module |
Where to notify on build completion |
The matrix in build.yaml builds for both amd64 and arm64 on appropriate
runners. Self-hosted Gitea runners must be labeled ubuntu-24.04 and
ubuntu-24.04-arm respectively. Single-arch repos can drop the matrix entry.
The Containerfile pins UBUNTU_DIGEST to a specific Ubuntu 24.04 LTS image
digest, and APT_SNAPSHOT to a specific snapshot.ubuntu.com timestamp. Two
builds of the same source MUST produce identical oci_digest and
fsverity_root_hash — that's the SLSA Build Level 3+ contract Powernode requires.
Refresh both pins periodically (Renovate is configured to track ubuntu:24.04).
- Plan:
~/.claude/plans/we-are-working-on-golden-eclipse.md(M1 supply chain) - Platform models:
extensions/system/server/app/models/system/node_module.rb - Build dispatch:
extensions/system/server/app/services/system/module_build_dispatch_service.rb - Webhook ingest:
extensions/system/server/app/controllers/api/v1/system/webhooks/gitea_module_controller.rb