Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Repository agent instructions

## Installation parity: ISO is authoritative

- The current ISO installation defines the CybexOS product. Keep `./install`,
`./bootstrap`, and ISO installations equivalent for the same release,
hardware, and explicit user choices. Resolve differences by bringing the
checkout path into line with the ISO, not by changing the ISO to match an
older checkout default.
- Treat applications, desktop and personal defaults, authentication, enabled
services, firewall policy, hardware support, and recovery as one shared
installation contract. Reuse the shared policy, task files, templates, and
package selections; do not add independent installer defaults.
- Preserve explicit saved choices and user-owned data during installs,
reconfiguration, and updates. Matching a fresh installation never authorizes
repartitioning an existing Fedora system or resetting personal settings.
Automatic login must retain the ISO's verified-encryption requirement.
- Every change to the installed product must cover both installation paths in
the same change, with regression coverage that compares their outcomes.
Run the source and image parity checks before handoff. A passing fixture
suite must not be reported as a completed end-to-end installation test.
- Release comparisons must use artifacts from the same source revision.
When preparing a release, rebuild and qualify the ISO/RPM if its installed
payload changed; an older ISO or an older qualification report does not
validate the new release.

## ISO testing location

- Always place completed ISOs for testing in `/data/pxe/iso`.
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,16 @@ cd CybexOS

No inventory or configuration file needs to be edited first. The installer
detects the current desktop user, home directory, hostname, timezone, locale,
and keyboard settings and offers them as defaults. All application groups
are selected by default, including in non-interactive installs; interactive
setup allows explicit opt-outs. Fastfetch is a required baseline package.
The installer explicitly asks whether to enable passwordless sudo, passwordless
local Polkit authorization, and encrypted-boot desktop autologin. The two passwordless choices
have no implicit answer. When Docker is selected, it also asks whether the
desktop user may run Docker without sudo; the default is no, because the
`docker` group grants root-equivalent control of the machine.
and keyboard settings and offers them as defaults. The ISO defines the shared
fresh-install policy: all application groups and personal dotfiles are enabled,
sudo and local Polkit require authentication, and Docker requires sudo.
Automatic login defaults on only when the complete root filesystem is verified
as encrypted. These defaults also apply in non-interactive installs; interactive
setup allows explicit opt-outs. Existing saved choices remain authoritative.
Fastfetch is a required baseline package. Passwordless sudo, local Polkit and
Docker access remain explicit opt-ins; the `docker` group grants root-equivalent
control of the machine. See [installation parity](docs/installation-parity.md)
for the shared contract and release checks.

On an encrypted single-user installation, SDDM can open the desktop after the
LUKS unlock and unlock GNOME Keyring with the briefly cached boot password.
Expand Down
67 changes: 67 additions & 0 deletions docs/installation-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Installation parity

The ISO defines CybexOS's installed product. A checkout installation and an ISO
installation of the same release, on equivalent hardware with the same user
choices, must receive the same desktop, applications, account defaults,
authentication policy, services, firewall, and hardware configuration.

Fresh installations use these ISO defaults:

| Choice | Default on both paths |
| --- | --- |
| Applications | Every standard application group enabled |
| Personal defaults | Enabled, including Fish, Kitty, Git/SSH and browser preferences |
| Sudo and local Polkit | Password required |
| Docker administrator access | Sudo required |
| Desktop automatic login | Enabled only after complete root encryption is verified |
| Additional local-network firewall ports | Disabled; LocalSend retains its shared ports |
| Machine identity | Preserve the identity already configured by Fedora/Anaconda |

`inventory/group_vars/all.yml` is the common default input.
`image/installation_policy.py` defines the account policy used by both
`scripts/installer-defaults` and the packaged `cybexos-config` helper. The
checkout questionnaire starts from that policy instead of maintaining its own
booleans. It uses the ISO's boot-time encryption verifier, including every
Btrfs member. Unverifiable or mixed encrypted/plaintext storage disables
automatic login. Real installs run this read-only probe with administrator
access; `./install --check` never elevates or installs dependencies and cannot
enable autologin if its access is insufficient to verify encryption.

Saved choices take precedence. Re-running or updating an installation must not
silently turn on personal defaults or passwordless access for an older account.
`--reconfigure` offers the saved boolean choices as its defaults. An explicit
identity change in the checkout questionnaire still applies that choice.
Neither parity nor a release update authorizes repartitioning an existing
Fedora installation or resetting user settings to match a clean account.
Fastfetch, Voxtype, Oh My Posh, MIME associations, and npm configuration are
seeded only when absent, as on the ISO. Managed Fish and Kitty fragments remain
updateable independently of those personal files.

The checkout path installs onto existing Fedora and retains source-release
updates and uninstall; the ISO uses Anaconda for disk/account creation and RPM
delivery for desktop updates and repair. These mechanisms are distinct from the
installed policy. Comparing a development checkout with an older ISO is not a
parity test: both artifacts must come from the same revision, with completed
hardware setup and equivalent application choices.

## Required checks for changes and releases

Run `./tests/run` and `python3 -B image/check-source`. Both required CI jobs run
`image/test_installation_parity.py`. It executes the real checkout questionnaire
and non-interactive dry run, then compares the complete generated configuration
with ISO provisioning and target finalization for encrypted and plaintext
installations. It also checks saved opt-outs, existing personal files, mixed or
unverifiable Btrfs, and the policy module shipped in the repair payload.

The existing image package, desktop payload, installed policy and user parity
tests cover package selections and shared task/template sources. Extend those
checks whenever a package, setting, service or hardware path changes; a new
feature cannot be added to only one installer's schema.

Release changes must pass the source and image gates from the same Git revision
and the existing generic Fedora and ISO installation/upgrade qualification
gates. Build and qualify a new ISO/RPM when its payload changes. Retain the
revision and artifact digests in the qualification evidence. Fixture checks
compare the installation contract; they are not evidence that fresh physical
or VM installations have been performed. Do not use an older ISO qualification
as evidence for a newer checkout.
5 changes: 5 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ repository.
1. Review `release-manifest.json`, `VERSION`, the Fedora release,
configuration schema, minimum updater version, and all dependency pins.
2. Run `./tests/run` and `./tests/fedora-vm-convergence` locally when practical.
Run `python3 -B image/check-source` as well. Both source suites enforce the
[ISO-leading installation contract](installation-parity.md); changes to
applications, defaults or policy must cover both installation paths. Build
and qualify ISO/RPM artifacts from the same release revision rather than
reusing an older image as parity evidence.
The source gate includes an N to N+1 ownership test that advances vendor
runtime while requiring every user customization sentinel to remain
byte-identical.
Expand Down
3 changes: 3 additions & 0 deletions image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ version references.
Both deployment paths consume [the shared desktop contract](../assets/desktop-contract.json),
wallpaper collection, existing Cybex Plymouth artwork, helpers, firewall zone
and sysctl policy. Bluetooth visibility matches the workstation default.
The ISO is authoritative for both paths' installation defaults; the checkout
installer consumes the same [installation policy](installation_policy.py).
The [parity contract](../docs/installation-parity.md) describes its checks.
The image contains the repository desktop and default applications; it does
not export personal plugins (including the Omarchy plugin), credentials,
monitor overrides or private launchers from the build machine. The private
Expand Down
40 changes: 40 additions & 0 deletions image/installation_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Fresh-install policy shared by the ISO and the checkout installer.

The inventory supplies application and security defaults. Machine identity
and verified disk encryption are inputs, never properties of the build host.
Existing saved choices are applied separately and must not be reset here.
"""
import yaml


BOOLEANS = ('manage_system_identity', 'manage_personal_dotfiles', 'cleanup_legacy_xps_artifacts',
'passwordless_wheel', 'passwordless_local_polkit', 'docker_sudoless', 'desktop_autologin',
'start_optional_hardware_services', 'allow_insecure_sccache_transport', 'xps_2026_camera_enabled')
FEATURES = ('developer_tools', 'connected_widgets', 'proprietary_apps', 'tailscale', 'docker', 'podman',
'steam', 'private_hooks', 'apple_display', 'source_builds', 'local_network_services')


def defaults(inventory, *, fresh_account=True, encrypted=False):
values = yaml.safe_load(inventory.read_text())
if not isinstance(values, dict) or not isinstance(values.get('features'), dict):
raise ValueError('Installation defaults must contain the feature contract')
features = values['features']
if set(features) != set(FEATURES):
raise ValueError('Installation feature keys must match both installer schemas')
for key in FEATURES:
if type(features.get(key)) is not bool:
raise ValueError(f'features.{key} must have a boolean installation default')
for key in BOOLEANS:
# The inventory retains the legacy gdm_autologin template for direct
# Ansible callers; fresh installers use verified encryption instead.
if key != 'desktop_autologin' and type(values.get(key)) is not bool:
raise ValueError(f'{key} must have a boolean installation default')
result = {key: values.get(key) is True for key in BOOLEANS}
# Match the ISO: installed identity remains owned by Fedora/Anaconda;
# a fresh account receives personal defaults and encrypted-root autologin.
# Repairing an older installation must never opt it into either choice.
result['manage_system_identity'] = False
result['manage_personal_dotfiles'] = fresh_account and values.get('manage_personal_dotfiles') is True
result['desktop_autologin'] = fresh_account and encrypted is True
result['features'] = {key: features[key] for key in FEATURES}
return result
1 change: 1 addition & 0 deletions image/package
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def main():
prepare_provision(ROOT, payload)
copy("image/library/cybexos_managed_file.py", "usr/share/cybexos/lib/managed_files.py")
copy("image/release_metadata.py", "usr/share/cybexos/lib/release_metadata.py")
copy("image/installation_policy.py", "usr/share/cybexos/lib/installation_policy.py")
copy("roles/apps/files/cybexos-repository-policy", "usr/libexec/cybexos-repository-policy", True)
# The workstation's font family mappings; roles/apps/tasks/fonts.yml
# installs the same file on checkout deployments.
Expand Down
1 change: 1 addition & 0 deletions image/repair-installed
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def prepare(payload):
shutil.copyfile(ROOT / source, target)
target.chmod(0o755 if executable else 0o644)

copy('image/installation_policy.py', 'usr/share/cybexos/lib/installation_policy.py')
for relative in ('usr/libexec/cybexos-configure-installed', 'usr/libexec/cybexos-config',
'usr/bin/cybex', 'usr/bin/hyprland-quickshell',
'usr/lib/systemd/user/hyprland-session.target',
Expand Down
30 changes: 10 additions & 20 deletions image/rootfs/usr/libexec/cybexos-config
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ import tempfile

import yaml

# Source fixtures must exercise this checkout, even on a machine with an older
# packaged policy installed. The installed helper reads its RPM-owned library.
source_image = Path(__file__).resolve().parent.parent.parent.parent
sys.path.insert(0, str(source_image) if (source_image / 'installation_policy.py').is_file()
else '/usr/share/cybexos/lib')
from installation_policy import BOOLEANS, FEATURES, defaults as installation_defaults # noqa: E402


CONFIG = Path('/etc/cybexos/config.yml')
INVENTORY = Path('/usr/share/cybexos/provision/inventory/group_vars/all.yml')
APPLY = '/usr/libexec/cybexos-configure-installed'
# Key order and quoting follow ./install's generated file.
STRINGS = ('primary_user', 'primary_group', 'primary_home', 'machine_hostname', 'machine_timezone',
'machine_locale', 'regional_locale', 'machine_keyboard_layout', 'machine_keyboard_variant')
BOOLEANS = ('manage_system_identity', 'manage_personal_dotfiles', 'cleanup_legacy_xps_artifacts',
'passwordless_wheel', 'passwordless_local_polkit', 'docker_sudoless', 'desktop_autologin',
'start_optional_hardware_services', 'allow_insecure_sccache_transport', 'xps_2026_camera_enabled')
FEATURES = ('developer_tools', 'connected_widgets', 'proprietary_apps', 'tailscale', 'docker', 'podman',
'steam', 'private_hooks', 'apple_display', 'source_builds', 'local_network_services')
PATTERNS = {
'machine_hostname': r'[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}',
'machine_timezone': r'[A-Za-z0-9_+-]+(?:/[A-Za-z0-9_+-]+)*',
Expand Down Expand Up @@ -125,24 +127,12 @@ def detect(root=Path('/'), entries=None, group=group_name, inventory=INVENTORY,
account = candidates[0]
if account is None:
return None
defaults = yaml.safe_load(inventory.read_text()) or {}
features = defaults.get('features') or {}
values = {'config_schema_version': 1, 'primary_user': account.pw_name,
'primary_group': group(account.pw_gid), 'primary_home': account.pw_dir, **identity(root)}
# Unset, templated or non-boolean defaults fall back to the stricter choice.
values.update({key: defaults.get(key) is True for key in BOOLEANS})
# A live ISO cannot inherit the build host's sudo choice. The guided
# installer applies a confirmed opt-in after provisioning; Advanced has
# no such prompt and keeps the password-required default.
if fresh_account:
values['passwordless_wheel'] = False
# Anaconda and the OS settings own identity after an ISO installation; a
# later checkout run must not reset them to this installation-time record.
values['manage_system_identity'] = False
# Only a fresh installation account opts in without being asked.
values['manage_personal_dotfiles'] = fresh_account
values.update(installation_defaults(inventory, fresh_account=fresh_account))
# Only the target helper's verified decision (or a saved login policy)
# can enable automatic login; the build host is never an input.
values['desktop_autologin'] = login.get('user') == account.pw_name and login.get('autologin') is True
values['features'] = {key: features.get(key) is True for key in FEATURES}
return values


Expand Down
Loading
Loading