Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
264a397
feat(taxonomy): add phase0 guardrails and metadata
Bad3r Oct 12, 2025
3ddcebe
fix(ci): scope checks under per-system outputs
Bad3r Oct 12, 2025
88cede4
docs(rfc-0001): note per-system phase0 checks
Bad3r Oct 12, 2025
29f40e9
chore(phase0): land guardrail baseline
Bad3r Oct 12, 2025
4bfecae
docs(taxonomy): land phase0 documentation scaffolding
Bad3r Oct 13, 2025
42ca458
chore(rfc-0001): finalize canonical taxonomy rollout
Bad3r Oct 14, 2025
d1ec1ac
feat(profiles): export workstation namespace and refresh docs
Bad3r Oct 14, 2025
6cb5ec0
docs(parity): record phase2 baseline and manifest deltas
Bad3r Oct 14, 2025
37ed028
chore(phase2): fix role extenders and refresh manifest
Bad3r Oct 14, 2025
94bb1ba
chore(rfc-0001): finalize phase3 workstation cutover
Bad3r Oct 15, 2025
80ff0df
feat(rfc-0001): add phase4 workstation parity check
Bad3r Oct 15, 2025
71fc252
docs(rfc-0001): refresh canonical profile guidance
Bad3r Oct 15, 2025
3c846dd
docs: add RFC-0001 release notes
Bad3r Oct 15, 2025
e7d9742
docs(taxonomy): record versioning, tags, and profiles
Bad3r Oct 15, 2025
ea5e795
fix(roles): ensure role extras augment canonical modules
Bad3r Oct 16, 2025
160ba77
test(roles): guard roleExtras by asserting nix-ld import
Bad3r Oct 16, 2025
4811c50
feat(system76): wire duplicati and refresh workstation manifest
Bad3r Oct 16, 2025
7f73260
fix(ci): seed roleExtras when evaluating roles
Bad3r Oct 16, 2025
3598d71
chore(secrets): tune duplicati bankdata job
Bad3r Oct 16, 2025
e84621c
chore: update docs
Bad3r Oct 16, 2025
b2d8816
docs: add summary of issue
Bad3r Oct 19, 2025
5888646
fix: remove needs to for secrets
Bad3r Oct 19, 2025
4d397e6
chore: wip
Bad3r Oct 19, 2025
624c64d
Revert "chore: wip"
Bad3r Oct 19, 2025
9ebfd36
chore: to discard
Bad3r Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ This flake exposes two mergeable aggregators:
- `flake.nixosModules`: NixOS modules (freeform, nested namespaces allowed)
- `flake.homeManagerModules`: Home Manager modules (freeform; with `base`, `gui`, and per-app under `apps`)

Modules register themselves under these namespaces (e.g., `flake.nixosModules.workstation`, `flake.homeManagerModules.base`).
Modules register themselves under these namespaces (e.g., `flake.nixosModules.profiles.workstation`, `flake.homeManagerModules.base`).
Composition uses named references, for example:

```nix
{ config, ... }:
{
configurations.nixos.myhost.module = {
imports = with config.flake.nixosModules; [ base workstation ];
imports = [
config.flake.nixosModules.base
config.flake.nixosModules.profiles.workstation
];
};
}
```
Expand All @@ -47,13 +50,11 @@ Example host composition using the role namespace:
{ config, ... }:
{
configurations.nixos.system76.module = {
imports =
(with config.flake.nixosModules; [
workstation
])
++ [
config.flake.nixosModules.roles.dev
];
imports = [
config.flake.nixosModules.base
config.flake.nixosModules.profiles.workstation
config.flake.nixosModules.roles.dev
];
};
}
```
Expand Down
129 changes: 129 additions & 0 deletions checks/phase0/alias-resolver.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{ lib }:
let
taxonomy = import ../../lib/taxonomy { inherit lib; };
aliases = taxonomy.aliases or { };
inherit (taxonomy) matrix;
roots = matrix.canonicalRootsList;
aliasHash = taxonomy.aliasHash or "";
sentinelPlaceholder = "pending-phase0-baseline";

vendorPrefix = "vendor";

rootForCanonical =
canonical:
lib.findFirst (
root: canonical == root.namespace || lib.hasPrefix (root.namespace + ".") canonical
) null roots;

toSegments = path: lib.splitString "." path;

joinSegments = segments: if segments == [ ] then "" else lib.concatStringsSep "." segments;

remainderSegmentsFor =
root: canonical:
let
namespaceSegments = toSegments root.namespace;
targetSegments = toSegments canonical;
nsLen = builtins.length namespaceSegments;
in
if builtins.length targetSegments < nsLen then [ ] else lib.lists.drop nsLen targetSegments;

validateAlias =
aliasEntry:
let
aliasName = aliasEntry.name;
canonical = aliasEntry.value;
canonicalIsString = builtins.isString canonical;
aliasPrefixErrors =
if lib.hasPrefix "roles." aliasName then
[ ]
else
[ "Alias '${aliasName}' must live under the roles.* namespace" ];
canonicalTypeErrors =
if canonicalIsString then
[ ]
else
[
"Alias '${aliasName}' must resolve to a string canonical path (got ${builtins.typeOf canonical})"
];
canonicalPrefixErrors =
if canonicalIsString && lib.hasPrefix "roles." canonical then
[ ]
else if canonicalIsString then
[ "Canonical target for '${aliasName}' must live under roles.* (got ${canonical})" ]
else
[ ];
canonicalSameAsAliasErrors =
if canonicalIsString && aliasName == canonical then
[ "Alias '${aliasName}' must not point to itself" ]
else
[ ];
root = if canonicalIsString then rootForCanonical canonical else null;
rootErrors =
if canonicalIsString && root == null then
[ "Canonical target '${canonical}' for alias '${aliasName}' does not map to a known taxonomy root" ]
else
[ ];
remainderSegments = if root == null then [ ] else remainderSegmentsFor root canonical;
remainder = joinSegments remainderSegments;
reserved = if root == null then [ ] else (root.reservedSubroles or [ ]);
allowedSubroles = if root == null then [ ] else root.subroles ++ reserved;
allowedList =
if allowedSubroles == [ ] then "(none documented)" else lib.concatStringsSep ", " allowedSubroles;
vendorErrors =
if root == null || remainderSegments == [ ] || builtins.head remainderSegments != vendorPrefix then
[ ]
else
let
allowVendor = root.allowVendor or false;
vendorHasName = builtins.length remainderSegments >= 2;
allowError =
if allowVendor then
[ ]
else
[
"Alias '${aliasName}' points to vendor namespace '${canonical}' but root '${root.namespace}' does not allow vendor subroles"
];
nameError =
if vendorHasName then
[ ]
else
[
"Alias '${aliasName}' must specify a vendor name (found '${canonical}')"
];
in
allowError ++ nameError;
subroleErrors =
if root == null || remainderSegments == [ ] || builtins.head remainderSegments == vendorPrefix then
[ ]
else if lib.elem remainder allowedSubroles then
[ ]
else
[
"Alias '${aliasName}' points to unknown subrole '${canonical}'. Expected one of: ${allowedList}"
];
in
lib.concatLists [
aliasPrefixErrors
canonicalTypeErrors
canonicalPrefixErrors
canonicalSameAsAliasErrors
rootErrors
vendorErrors
subroleErrors
];

aliasEntries = lib.attrsToList aliases;
sentinelErrors =
if aliasHash == sentinelPlaceholder then
[
"Phase 0 sentinel: alias registry still reports placeholder hash '${sentinelPlaceholder}'. Regenerate the alias registry and update lib/taxonomy/version.nix before enabling this check."
]
else
[ ];
errors = lib.concatMap validateAlias aliasEntries ++ sentinelErrors;
in
{
valid = errors == [ ];
inherit errors aliases;
}
105 changes: 105 additions & 0 deletions checks/phase0/host-package-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ ${1-} ]]; then
REPO_ROOT=$(realpath "$1")
else
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)
fi

REGISTRY_DEFAULT="$REPO_ROOT/docs/RFC-0001/manifest-registry.json"
REGISTRY="${HOST_PACKAGE_GUARD_REGISTRY:-$REGISTRY_DEFAULT}"
ACTUAL_JSON_PATH="${HOST_PACKAGE_GUARD_ACTUAL_JSON:-}"

if [[ ! -f $REGISTRY ]]; then
echo "host-package-guard: manifest registry not found at $REGISTRY" >&2
exit 1
fi

if [[ -z $ACTUAL_JSON_PATH || ! -f $ACTUAL_JSON_PATH ]]; then
echo "host-package-guard: precomputed package list not found at \$HOST_PACKAGE_GUARD_ACTUAL_JSON (${ACTUAL_JSON_PATH:-unset})" >&2
exit 1
fi

export NIX_CONFIG="${NIX_CONFIG:-experimental-features = nix-command flakes}"

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

jq -c '.[]' "$REGISTRY" >"$tmpdir/entries.json"

role_inventory="${HOST_PACKAGE_GUARD_ROLE_INVENTORY:-}"
if [[ -z $role_inventory ]]; then
role_inventory="$tmpdir/role-inventory.json"
python3 "$REPO_ROOT/scripts/list-role-imports.py" --offline --repo "$REPO_ROOT" --format json >"$role_inventory"
fi

if [[ ! -s $role_inventory ]]; then
echo "host-package-guard: role import inventory is empty; cannot compute allowlist" >&2
exit 1
fi
PACKAGE_UTILS="$REPO_ROOT/scripts/package_utils.py"

allowed_sorted="$tmpdir/allowed.txt"
python3 "$PACKAGE_UTILS" normalize --mode role-inventory --input "$role_inventory" --output "$allowed_sorted"

status=0

while IFS= read -r entry; do
host=$(jq -r '.host // empty' <<<"$entry")
manifest_rel=$(jq -r '.manifest' <<<"$entry")

if [[ -z $manifest_rel ]]; then
echo "host-package-guard: registry entry missing manifest" >&2
status=1
continue
fi

if [[ -z $host ]]; then
echo "host-package-guard: skipping entry without host binding ($manifest_rel)" >&2
continue
fi

manifest="$REPO_ROOT/$manifest_rel"
if [[ ! -f $manifest ]]; then
echo "host-package-guard: manifest $manifest not found" >&2
status=1
continue
fi

manifest_sorted="$tmpdir/${host}-expected.txt"
python3 "$PACKAGE_UTILS" normalize --mode manifest --input "$manifest" --output "$manifest_sorted"

actual_sorted="$tmpdir/${host}-actual.txt"
if ! python3 "$PACKAGE_UTILS" normalize --mode actual --input "$ACTUAL_JSON_PATH" --host "$host" --output "$actual_sorted"; then
status=1
continue
fi

missing=$(comm -23 "$manifest_sorted" "$actual_sorted" || true)
unexpected=$(comm -13 "$manifest_sorted" "$actual_sorted" || true)

if [[ -n $missing || -n $unexpected ]]; then
status=1
echo "host-package-guard: ${host} diverges from $manifest_rel" >&2
if [[ -n $missing ]]; then
echo " missing:" >&2
echo "$missing" >&2
fi
if [[ -n $unexpected ]]; then
echo " unexpected:" >&2
echo "$unexpected" >&2
fi
fi

untracked=$(comm -13 "$allowed_sorted" "$actual_sorted" || true)
if [[ -n $untracked ]]; then
status=1
echo "host-package-guard: ${host} includes packages not covered by roles" >&2
echo " untracked:" >&2
echo "$untracked" >&2
fi
done <"$tmpdir/entries.json"

exit $status
Loading