git.delta.rocks / jrsonnet / refs/commits / 33f3601e24db

difftreelog

fix shared generator condition

ywyowvrkYaroslav Bolyukin2026-01-08parent: #45c49ea.patch.diff
in: trunk

10 files changed

modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -114,7 +114,8 @@
 
 			set.spawn_local(
 				(async move {
-					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet").await
+					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")
+						.await
 					{
 						Ok(path) => path,
 						Err(e) => {
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	io::{self, Read, Write, stdin, stdout},4	path::PathBuf,5};67use anyhow::{Context, Result, anyhow, bail, ensure};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11	fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},12	host::Config,13	opts::FleetOpts,14	secret::{Expectations, RegenerationReason, secret_needs_regeneration},15};16use fleet_shared::SecretData;17use nix_eval::{NixType, Value, nix_go, nix_go_json};18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::{fs::read, task::spawn_blocking};21use tracing::{Instrument, error, info, info_span, warn};2223#[derive(Parser)]24pub enum Secret {25	AddManager,26	/// Force load host keys for all defined hosts27	ForceKeys,28	/// Read secret from remote host, requires sudo on one of the owning hosts29	Read {30		/// Secret name to read31		name: String,3233		/// Distribution with what machine to read34		/// If not shared between multiple - defaults to single owner35		#[clap(short = 'm', long)]36		machine: Option<String>,3738		/// Which private secret part to read39		#[clap(short = 'p', long, default_value = "secret")]40		part: String,4142		/// Which host should we use to decrypt, in case if reencryption is required, without43		/// regeneration44		#[clap(long)]45		prefer_identities: Vec<String>,46	},47	Regenerate {48		/// Which host should we use to decrypt, in case if reencryption is required, without49		/// regeneration50		#[clap(long)]51		prefer_identities: Vec<String>,52		/// Only regenerate shared secrets53		#[clap(long)]54		skip_hosts: bool,55	},56	List {},57	Edit {58		name: String,59		#[clap(short = 'm', long)]60		machine: String,6162		#[clap(long)]63		add: bool,6465		/// Which private secret part to read66		#[clap(short = 'p', long, default_value = "secret")]67		part: String,68	},69}7071/*72#[allow(clippy::too_many_arguments)]73#[tracing::instrument(skip(config, secret, definition, prefer_identities))]74async fn maybe_regenerate_shared_secret(75	secret_name: &str,76	config: &Config,77	mut secret: FleetSecretDistribution,78	definition: SharedSecretDefinition,79	prefer_identities: &[String],80	expectations: &Expectations,81) -> Result<FleetSecretDistribution> {82	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);83	let value = definition.definition_value();8485	let (should_reencrypt, reason) = match reason {86		Some(RegenerationReason::OwnersAdded(_)) => {87			// Secret always needs to be reencrypted for new owners to be able to read it88			(89				true,90				if nix_go_json!(value.regenerateOnOwnerAdded) {91					reason92				} else {93					None94				},95			)96		}97		Some(RegenerationReason::OwnersRemoved(_)) => {98			// No need to reencrypt, we can just leave stanzas in place.99			if nix_go_json!(value.regenerateOnOwnerRemoved) {100				(true, reason)101			} else {102				(false, None)103			}104		}105		Some(_) => (true, reason),106		None => (false, None),107	};108109	if let Some(reason) = reason {110		info!("secret needs to be regenerated: {reason}");111		let generated = generate_shared(config, secret_name, definition, expectations).await?;112		Ok(generated)113	} else if should_reencrypt {114		info!("secret needs to be reencrypted");115		let identity_holder = if !prefer_identities.is_empty() {116			prefer_identities117				.iter()118				.find(|i| secret.owners.iter().any(|s| s == *i))119		} else {120			secret.owners.first()121		};122		let Some(identity_holder) = identity_holder else {123			bail!("no available holder found");124		};125126		for (part_name, part) in secret.secret.parts.iter_mut() {127			let _span = info_span!("part reencryption", part_name);128			if !part.raw.encrypted {129				continue;130			}131			let host = config.host(identity_holder).await?;132			let encrypted = host133				.reencrypt(134					part.raw.clone(),135					expectations.owners.iter().cloned().collect(),136				)137				.await?;138			part.raw = encrypted;139		}140		secret.owners = expectations.owners.clone();141		Ok(secret)142	} else {143		Ok(secret)144	}145}146*/147148#[derive(Deserialize)]149#[serde(rename_all = "camelCase")]150enum GeneratorKind {151	Impure,152	Pure,153}154155async fn generate_pure(156	_config: &Config,157	_display_name: &str,158	_secret: Value,159	_default_generator: Value,160	_expectations: &Expectations,161) -> Result<FleetSecretData> {162	bail!("pure generators are broken for now")163}164async fn generate_impure(165	config: &Config,166	_display_name: &str,167	secret: Value,168	default_generator: Value,169	expectations: &Expectations,170) -> Result<FleetSecretData> {171	let generator = nix_go!(secret.generator);172	let on: Option<String> = nix_go_json!(default_generator.impureOn);173174	let nixpkgs = &config.nixpkgs;175176	let host = if let Some(on) = &on {177		config.host(on).await?178	} else {179		config.local_host()180	};181	let on_pkgs = host.pkgs().await?;182	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);183184	let mut recipients = Vec::new();185	for owner in &expectations.owners {186		let key = config.key(owner).await?;187		recipients.push(key);188	}189	let generators = nix_go!(mk_secret_generators(Obj { recipients }));190	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;191192	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));193194	let generator = nix_go!(call_package(generator)(Obj {}));195196	let generator = spawn_blocking(move || generator.build("out"))197		.await198		.expect("nix build shouldn't fail")?;199	let generator = host.remote_derivation(&generator).await?;200201	let out_parent = host.mktemp_dir().await?;202	let out = format!("{out_parent}/out");203204	let mut r#gen = host.cmd(generator).await?;205	r#gen.env("out", &out);206	if on.is_none() {207		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.208		let project_path: String = config209			.directory210			.clone()211			.into_os_string()212			.into_string()213			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;214		r#gen.env("FLEET_PROJECT", project_path);215	}216	r#gen.run().await.context("impure generator")?;217218	{219		let marker = host.read_file_text(format!("{out}/marker")).await?;220		ensure!(marker == "SUCCESS", "generation not succeeded");221	}222223	let mut parts = BTreeMap::new();224	for part in host.read_dir(&out).await? {225		if part == "created_at" || part == "expires_at" || part == "marker" {226			continue;227		}228		let contents: SecretData = host229			.read_file_text(format!("{out}/{part}"))230			.await?231			.parse()232			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;233		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });234	}235236	let created_at = host.read_file_value(format!("{out}/created_at")).await?;237	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();238239	let new_data = FleetSecretData {240		created_at,241		expires_at,242		parts,243		generation_data: expectations.generation_data.clone(),244	};245246	if let Some(reason) = secret_needs_regeneration(&new_data, &expectations.owners, expectations) {247		bail!("newly generated secret needs to be regenerated: {reason}")248	}249250	Ok(new_data)251}252253async fn generate(254	config: &Config,255	display_name: &str,256	secret: Value,257	expectations: &Expectations,258) -> Result<FleetSecretData> {259	let generator = nix_go!(secret.generator);260	// Can't properly check on nix module system level261	{262		let gen_ty = generator.type_of();263		if matches!(gen_ty, NixType::Null) {264			bail!("secret has no generator defined, can't automatically generate it.");265		}266		if matches!(gen_ty, NixType::Attrs) {267			if !generator.has_field("__functor")? {268				bail!("generator should be functor, got {gen_ty:?}");269			}270		} else if matches!(gen_ty, NixType::Function) {271			bail!("generator should be functor, got {gen_ty:?}");272		}273	}274	let nixpkgs = &config.nixpkgs;275	let default_pkgs = &config.default_pkgs;276	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);277	// Generators provide additional information in passthru, to access278	// passthru we should call generator, but information about where this generator is supposed to build279	// is located in passthru... Thus evaluating generator on host.280	//281	// Maybe it is also possible to do some magic with __functor?282	//283	// I don't want to make modules always responsible for additional secret data anyway,284	// so it should be in derivation, and not in the secret data itself.285	let generators = nix_go!(default_mk_secret_generators(Obj {286		recipients: <Vec<String>>::new(),287	}));288	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;289290	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));291	let default_generator = nix_go!(call_package(generator)(Obj {}));292293	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);294295	match kind {296		GeneratorKind::Impure => {297			generate_impure(298				config,299				display_name,300				secret,301				default_generator,302				expectations,303			)304			.await305		}306		GeneratorKind::Pure => {307			generate_pure(308				config,309				display_name,310				secret,311				default_generator,312				expectations,313			)314			.await315		}316	}317}318/*319async fn generate_shared(320	config: &Config,321	display_name: &str,322	secret: SharedSecretDefinition,323	expectations: &Expectations,324) -> Result<FleetSecretDistribution> {325	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);326	Ok(FleetSecretDistribution {327		managed: Some(true),328		secret: generate(329			config,330			display_name,331			secret.definition_value(),332			expectations,333		)334		.await?,335		owners: expectations.owners.clone(),336	})337}*/338339async fn parse_public(340	public: Option<String>,341	public_file: Option<PathBuf>,342) -> Result<Option<SecretData>> {343	Ok(match (public, public_file) {344		(Some(v), None) => Some(SecretData {345			data: v.into(),346			encrypted: false,347		}),348		(None, Some(v)) => Some(SecretData {349			data: read(v).await?,350			encrypted: false,351		}),352		(Some(_), Some(_)) => {353			bail!("only public or public_file should be set")354		}355		(None, None) => None,356	})357}358359async fn parse_secret() -> Result<Option<Vec<u8>>> {360	let mut input = vec![];361	stdin().read_to_end(&mut input)?;362	if input.is_empty() {363		Ok(None)364	} else {365		Ok(Some(input))366	}367}368369fn parse_machines(370	initial: BTreeSet<String>,371	machines: Option<Vec<String>>,372	mut add_machines: Vec<String>,373	mut remove_machines: Vec<String>,374) -> Result<BTreeSet<String>> {375	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {376		bail!("no operation");377	}378379	let initial_machines = initial.clone();380	let mut target_machines = initial;381	info!("Currently encrypted for {initial_machines:?}");382383	if let Some(machines) = machines {384		ensure!(385			add_machines.is_empty() && remove_machines.is_empty(),386			"can't combine --machines and --add-machines/--remove-machines"387		);388		let target = initial_machines.iter().collect::<HashSet<_>>();389		let source = machines.iter().collect::<HashSet<_>>();390		for removed in target.difference(&source) {391			remove_machines.push((*removed).clone());392		}393		for added in source.difference(&target) {394			add_machines.push((*added).clone());395		}396	}397398	for machine in &remove_machines {399		if !target_machines.remove(machine) {400			warn!("secret is not enabled for {machine}");401		}402	}403	for machine in &add_machines {404		if !target_machines.insert(machine.to_owned()) {405			warn!("secret is already added to {machine}");406		}407	}408	if !remove_machines.is_empty() {409		// TODO: maybe force secret regeneration?410		// Not that useful without revokation.411		warn!(412			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"413		);414	}415	Ok(target_machines)416}417impl Secret {418	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {419		match self {420			Secret::AddManager => {421				todo!("part of fleet-pusher")422			}423			Secret::ForceKeys => {424				for host in config.list_hosts().await? {425					if opts.should_skip(&host).await? {426						continue;427					}428					config.key(&host.name).await?;429				}430			}431			Secret::Read {432				name,433				machine,434				part: part_name,435				mut prefer_identities,436			} => {437				let Some(secret) = config.shared_secret(&name) else {438					bail!("secret doesn't exists");439				};440441				let dist = if secret.len() == 1 {442					&secret[0]443				} else if let Some(machine) = machine {444					let dist = secret.get(&machine);445					let Some(dist) = dist else {446						bail!("machine {machine} has no distribution of secret {name}");447					};448					prefer_identities.push(machine);449					dist450				} else {451					bail!(452						"secret {name} has shares, but no --machine specified for specifing which do you need"453					)454				};455456				let Some(part) = dist.secret.parts.get(&part_name) else {457					bail!("no part {part_name} in secret {name}");458				};459				let data = if part.raw.encrypted {460					let identity_holder = if !prefer_identities.is_empty() {461						prefer_identities462							.iter()463							.find(|i| dist.owners.iter().any(|s| s == *i))464					} else {465						dist.owners.first()466					};467					let Some(identity_holder) = identity_holder else {468						bail!("no available holder found");469					};470					let host = config.host(identity_holder).await?;471					host.decrypt(part.raw.clone()).await?472				} else {473					part.raw.data.clone()474				};475				stdout().write_all(&data)?;476			}477			Secret::Regenerate {478				prefer_identities,479				skip_hosts,480			} => {481				/*482								info!("checking for secrets to regenerate");483								let expected_shared_set = config484									.list_configured_shared()485									.await?486									.into_iter()487									.collect::<HashSet<_>>();488								let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();489								{490									// Generate missing shared491									let _span = info_span!("shared").entered();492									for missing in expected_shared_set.difference(&stored_shared_set) {493										let definition = config.shared_secret_definition(missing)?;494										if !definition.is_managed()? {495											info!("skipping unmanaged secret: {missing}");496											continue;497										}498										let expectations = definition499											.expectations()500											.with_context(|| format!("expectations for shared {missing:?}"))?;501										info!("generating secret: {missing}");502										let shared = generate_shared(config, missing, definition, &expectations)503											.in_current_span()504											.await?;505										config.replace_shared(missing.to_string(), shared)506									}507								}508								if !skip_hosts {509									for host in config.list_hosts().await? {510										if opts.should_skip(&host).await? {511											continue;512										}513514										let _span = info_span!("host", host = host.name).entered();515										let expected_set = host516											.list_defined_secrets()?517											.into_iter()518											.collect::<HashSet<_>>();519										let stored_set = config520											.list_secrets_for_owner(&host.name)521											.into_iter()522											.collect::<HashSet<_>>();523										for missing_secret in expected_set.difference(&stored_set) {524											let secret = host.secret_definition(missing_secret)?;525											if secret.is_shared()? {526												continue;527											}528											info!("generating missing secret: {missing_secret}");529											let expectations = secret.expectations().with_context(|| {530												format!("expectations for {missing_secret:?} of {:?}", host.name)531											})?;532											let generated = match generate(533												config,534												missing_secret,535												secret.definition_value()?,536												&expectations,537											)538											.in_current_span()539											.await540											{541												Ok(v) => v,542												Err(e) => {543													error!("{e:?}");544													continue;545												}546											};547											config.insert_secret(host.name, missing_secret.to_string(), generated)548										}549										for known_secret in stored_set.intersection(&expected_set) {550											let secret = host.secret_definition(known_secret)?;551											if secret.is_shared()? {552												continue;553											}554											info!("updating secret: {known_secret}");555											let data = config.host_secret(&host.name, known_secret)?;556											let expectations = secret.expectations()?;557											if let Some(regen_reason) = data.needs_regeneration(&expectations) {558												info!("needs regeneration: {regen_reason}");559												let generated = match generate(560													config,561													known_secret,562													secret.definition_value()?,563													&expectations,564												)565												.in_current_span()566												.await567												{568													Ok(v) => v,569													Err(e) => {570														error!("{e:?}");571														continue;572													}573												};574												config.insert_secret(575													&host.name,576													known_secret.to_string(),577													FleetLegacyHostSecret {578														managed: Some(true),579														secret: generated,580													},581												)582											}583										}584										for removed_secret in stored_set.difference(&expected_set) {585											let definition = host.secret_definition(removed_secret)?;586											if definition.is_shared()? {587												continue;588											}589											info!("removing secret: {removed_secret}");590											config.remove_secret(&host.name, removed_secret);591										}592									}593								}594								for known_secret in stored_shared_set.intersection(&expected_shared_set) {595									info!("updating shared secret: {known_secret}");596									let data = config.shared_secret(known_secret)?.expect("exists");597598									let definition = config.shared_secret_definition(known_secret)?;599									let expectations = definition.expectations()?;600									config.replace_shared(601										known_secret.to_owned(),602										maybe_regenerate_shared_secret(603											known_secret,604											config,605											data,606											definition,607											&prefer_identities,608											&expectations,609										)610										.await?,611									);612								}613								for removed_secret in stored_shared_set.difference(&expected_shared_set) {614									info!("removing shared secret: {removed_secret}");615									config.remove_shared(removed_secret);616								}617				*/618				todo!()619			}620			Secret::List {} => {621				let _span = info_span!("loading secrets").entered();622				let configured = config.list_configured_shared().await?;623				#[derive(Tabled)]624				struct SecretDisplay {625					#[tabled(rename = "Name")]626					name: String,627					#[tabled(rename = "Owners")]628					owners: String,629				}630				// let mut table = vec![];631				for name in configured.iter().cloned() {632					let config = config.clone();633					let data = config.shared_secret(&name).expect("exists");634					/*635					let definition = config.shared_secret_definition(&name)?;636					let expectations = definition.expectations()?;637					let owners = data638						.owners()639						.map(|o| {640							if expectations.owners.contains(o) {641								o.green().to_string()642							} else {643								o.red().to_string()644							}645						})646						.collect::<Vec<_>>();647					table.push(SecretDisplay {648						owners: owners.join(", "),649						name,650					})651*/652				}653				// info!("loaded\n{}", Table::new(table).to_string())654			}655			Secret::Edit {656				name,657				machine,658				part,659				add,660			} => {661				let secret = config662					.host_secret(&machine, &name)663					.context("secret not found")?;664				if let Some(data) = secret.secret.parts.get(&part) {665					let host = config.host(&machine).await?;666					let secret = host.decrypt(data.raw.clone()).await?;667					String::from_utf8(secret).context("secret is not utf8")?668				} else if add {669					String::new()670				} else {671					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");672				};673			}674		}675		Ok(())676	}677}678679/*680async fn edit_temp_file(681	builder: tempfile::Builder<'_, '_>,682	r: Vec<u8>,683	header: &str,684	comment: &str,685) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {686	if !stdin().is_tty() {687		// TODO: Also try to open /dev/tty directly?688		bail!("stdin is not tty, can't open editor");689	}690691	use std::fmt::Write;692	let mut file = builder.tempfile()?;693694	let mut full_header = String::new();695	let mut had = false;696	for line in header.trim_end().lines() {697		had = true;698		writeln!(&mut full_header, "{comment}{line}")?;699	}700	if had {701		writeln!(&mut full_header, "{}", comment.trim_end())?;702	}703	writeln!(704		&mut full_header,705		"{comment}Do not touch this header! It will be removed automatically"706	)?;707708	file.write_all(full_header.as_bytes())?;709	file.write_all(&r)?;710711	let abs_path = file.into_temp_path();712	let editor = std::env::var_os("VISUAL")713		.or_else(|| std::env::var_os("EDITOR"))714		.unwrap_or_else(|| "vi".into());715	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())716		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;717	let editor_args = editor_args718		.into_iter()719		.map(|v| {720			// Only ASCII subsequences are replaced721			unsafe { OsString::from_encoded_bytes_unchecked(v) }722		})723		.collect_vec();724	let Some((editor, args)) = editor_args.split_first() else {725		bail!("EDITOR env var has no command");726	};727	let mut command = Command::new(editor);728	command.args(args);729730	let path_arg = abs_path.canonicalize()?;731732	// TODO: Save full state, using tcget/_getmode/_setmode733	let was_raw = terminal::is_raw_mode_enabled()?;734	terminal::enable_raw_mode()?;735736	let status = command.arg(path_arg).status().await;737738	if !was_raw {739		terminal::disable_raw_mode()?;740	}741742	let success = match status {743		Ok(s) => s.success(),744		Err(e) if e.kind() == io::ErrorKind::NotFound => {745			bail!("editor not found")746		}747		Err(e) => bail!("editor spawn error: {e}"),748	};749750	let mut file = std::fs::read(&abs_path).context("read editor output")?;751	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {752		todo!();753	};754	todo!();755756	// Ok((success, abs_path))757}758*/
after · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	io::{self, Read, Write, stdin, stdout},4	path::PathBuf,5};67use anyhow::{Context, Result, anyhow, bail, ensure};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11	fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},12	host::Config,13	opts::FleetOpts,14	secret::{Expectations, RegenerationReason, secret_needs_regeneration},15};16use fleet_shared::SecretData;17use nix_eval::{NixType, Value, nix_go, nix_go_json};18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::{fs::read, task::spawn_blocking};21use tracing::{Instrument, error, info, info_span, warn};2223#[derive(Parser)]24pub enum Secret {25	AddManager,26	/// Force load host keys for all defined hosts27	ForceKeys,28	/// Read secret from remote host, requires sudo on one of the owning hosts29	Read {30		/// Secret name to read31		name: String,3233		/// Distribution with what machine to read34		/// If not shared between multiple - defaults to single owner35		#[clap(short = 'm', long)]36		machine: Option<String>,3738		/// Which private secret part to read39		#[clap(short = 'p', long, default_value = "secret")]40		part: String,4142		/// Which host should we use to decrypt, in case if reencryption is required, without43		/// regeneration44		#[clap(long)]45		prefer_identities: Vec<String>,46	},47	Regenerate {48		/// Which host should we use to decrypt, in case if reencryption is required, without49		/// regeneration50		#[clap(long)]51		prefer_identities: Vec<String>,52		/// Only regenerate shared secrets53		#[clap(long)]54		skip_hosts: bool,55	},56	List {},57	Edit {58		name: String,59		#[clap(short = 'm', long)]60		machine: String,6162		#[clap(long)]63		add: bool,6465		/// Which private secret part to read66		#[clap(short = 'p', long, default_value = "secret")]67		part: String,68	},69}7071/*72#[allow(clippy::too_many_arguments)]73#[tracing::instrument(skip(config, secret, definition, prefer_identities))]74async fn maybe_regenerate_shared_secret(75	secret_name: &str,76	config: &Config,77	mut secret: FleetSecretDistribution,78	definition: SharedSecretDefinition,79	prefer_identities: &[String],80	expectations: &Expectations,81) -> Result<FleetSecretDistribution> {82	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);83	let value = definition.definition_value();8485	let (should_reencrypt, reason) = match reason {86		Some(RegenerationReason::OwnersAdded(_)) => {87			// Secret always needs to be reencrypted for new owners to be able to read it88			(89				true,90				if nix_go_json!(value.regenerateOnOwnerAdded) {91					reason92				} else {93					None94				},95			)96		}97		Some(RegenerationReason::OwnersRemoved(_)) => {98			// No need to reencrypt, we can just leave stanzas in place.99			if nix_go_json!(value.regenerateOnOwnerRemoved) {100				(true, reason)101			} else {102				(false, None)103			}104		}105		Some(_) => (true, reason),106		None => (false, None),107	};108109	if let Some(reason) = reason {110		info!("secret needs to be regenerated: {reason}");111		let generated = generate_shared(config, secret_name, definition, expectations).await?;112		Ok(generated)113	} else if should_reencrypt {114		info!("secret needs to be reencrypted");115		let identity_holder = if !prefer_identities.is_empty() {116			prefer_identities117				.iter()118				.find(|i| secret.owners.iter().any(|s| s == *i))119		} else {120			secret.owners.first()121		};122		let Some(identity_holder) = identity_holder else {123			bail!("no available holder found");124		};125126		for (part_name, part) in secret.secret.parts.iter_mut() {127			let _span = info_span!("part reencryption", part_name);128			if !part.raw.encrypted {129				continue;130			}131			let host = config.host(identity_holder).await?;132			let encrypted = host133				.reencrypt(134					part.raw.clone(),135					expectations.owners.iter().cloned().collect(),136				)137				.await?;138			part.raw = encrypted;139		}140		secret.owners = expectations.owners.clone();141		Ok(secret)142	} else {143		Ok(secret)144	}145}146*/147148#[derive(Deserialize)]149#[serde(rename_all = "camelCase")]150enum GeneratorKind {151	Impure,152	Pure,153}154155async fn generate_pure(156	_config: &Config,157	_display_name: &str,158	_secret: Value,159	_default_generator: Value,160	_expectations: &Expectations,161) -> Result<FleetSecretData> {162	bail!("pure generators are broken for now")163}164async fn generate_impure(165	config: &Config,166	_display_name: &str,167	secret: Value,168	default_generator: Value,169	expectations: &Expectations,170) -> Result<FleetSecretData> {171	let generator = nix_go!(secret.generator);172	let on: Option<String> = nix_go_json!(default_generator.impureOn);173174	let nixpkgs = &config.nixpkgs;175176	let host = if let Some(on) = &on {177		config.host(on).await?178	} else {179		config.local_host()180	};181	let on_pkgs = host.pkgs().await?;182	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);183184	let mut recipients = Vec::new();185	for owner in &expectations.owners {186		let key = config.key(owner).await?;187		recipients.push(key);188	}189	let generators = nix_go!(mk_secret_generators(Obj { recipients }));190	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;191192	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));193194	let generator = nix_go!(call_package(generator)(Obj {}));195196	let generator = spawn_blocking(move || generator.build("out"))197		.await198		.expect("nix build shouldn't fail")?;199	let generator = host.remote_derivation(&generator).await?;200201	let out_parent = host.mktemp_dir().await?;202	let out = format!("{out_parent}/out");203204	let mut r#gen = host.cmd(generator).await?;205	r#gen.env("out", &out);206	if on.is_none() {207		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.208		let project_path: String = config209			.directory210			.clone()211			.into_os_string()212			.into_string()213			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;214		r#gen.env("FLEET_PROJECT", project_path);215	}216	r#gen.run().await.context("impure generator")?;217218	{219		let marker = host.read_file_text(format!("{out}/marker")).await?;220		ensure!(marker == "SUCCESS", "generation not succeeded");221	}222223	let mut parts = BTreeMap::new();224	for part in host.read_dir(&out).await? {225		if part == "created_at" || part == "expires_at" || part == "marker" {226			continue;227		}228		let contents: SecretData = host229			.read_file_text(format!("{out}/{part}"))230			.await?231			.parse()232			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;233		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });234	}235236	let created_at = host.read_file_value(format!("{out}/created_at")).await?;237	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();238239	let new_data = FleetSecretData {240		created_at,241		expires_at,242		parts,243		generation_data: expectations.generation_data.clone(),244	};245246	if let Some(reason) = secret_needs_regeneration(&new_data, &expectations.owners, expectations) {247		bail!("newly generated secret needs to be regenerated: {reason}")248	}249250	Ok(new_data)251}252253async fn generate(254	config: &Config,255	display_name: &str,256	secret: Value,257	expectations: &Expectations,258) -> Result<FleetSecretData> {259	let generator = nix_go!(secret.generator);260	// Can't properly check on nix module system level261	{262		let gen_ty = generator.type_of();263		if matches!(gen_ty, NixType::Null) {264			bail!("secret has no generator defined, can't automatically generate it.");265		}266		if matches!(gen_ty, NixType::Attrs) {267			if !generator.has_field("__functor")? {268				bail!("generator should be functor, got {gen_ty:?}");269			}270		} else if matches!(gen_ty, NixType::Function) {271			bail!("generator should be functor, got {gen_ty:?}");272		}273	}274	let nixpkgs = &config.nixpkgs;275	let default_pkgs = &config.default_pkgs;276	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);277	// Generators provide additional information in passthru, to access278	// passthru we should call generator, but information about where this generator is supposed to build279	// is located in passthru... Thus evaluating generator on host.280	//281	// Maybe it is also possible to do some magic with __functor?282	//283	// I don't want to make modules always responsible for additional secret data anyway,284	// so it should be in derivation, and not in the secret data itself.285	let generators = nix_go!(default_mk_secret_generators(Obj {286		recipients: <Vec<String>>::new(),287	}));288	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;289290	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));291	let default_generator = nix_go!(call_package(generator)(Obj {}));292293	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);294295	match kind {296		GeneratorKind::Impure => {297			generate_impure(298				config,299				display_name,300				secret,301				default_generator,302				expectations,303			)304			.await305		}306		GeneratorKind::Pure => {307			generate_pure(308				config,309				display_name,310				secret,311				default_generator,312				expectations,313			)314			.await315		}316	}317}318/*319async fn generate_shared(320	config: &Config,321	display_name: &str,322	secret: SharedSecretDefinition,323	expectations: &Expectations,324) -> Result<FleetSecretDistribution> {325	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);326	Ok(FleetSecretDistribution {327		managed: Some(true),328		secret: generate(329			config,330			display_name,331			secret.definition_value(),332			expectations,333		)334		.await?,335		owners: expectations.owners.clone(),336	})337}*/338339async fn parse_public(340	public: Option<String>,341	public_file: Option<PathBuf>,342) -> Result<Option<SecretData>> {343	Ok(match (public, public_file) {344		(Some(v), None) => Some(SecretData {345			data: v.into(),346			encrypted: false,347		}),348		(None, Some(v)) => Some(SecretData {349			data: read(v).await?,350			encrypted: false,351		}),352		(Some(_), Some(_)) => {353			bail!("only public or public_file should be set")354		}355		(None, None) => None,356	})357}358359async fn parse_secret() -> Result<Option<Vec<u8>>> {360	let mut input = vec![];361	stdin().read_to_end(&mut input)?;362	if input.is_empty() {363		Ok(None)364	} else {365		Ok(Some(input))366	}367}368369fn parse_machines(370	initial: BTreeSet<String>,371	machines: Option<Vec<String>>,372	mut add_machines: Vec<String>,373	mut remove_machines: Vec<String>,374) -> Result<BTreeSet<String>> {375	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {376		bail!("no operation");377	}378379	let initial_machines = initial.clone();380	let mut target_machines = initial;381	info!("Currently encrypted for {initial_machines:?}");382383	if let Some(machines) = machines {384		ensure!(385			add_machines.is_empty() && remove_machines.is_empty(),386			"can't combine --machines and --add-machines/--remove-machines"387		);388		let target = initial_machines.iter().collect::<HashSet<_>>();389		let source = machines.iter().collect::<HashSet<_>>();390		for removed in target.difference(&source) {391			remove_machines.push((*removed).clone());392		}393		for added in source.difference(&target) {394			add_machines.push((*added).clone());395		}396	}397398	for machine in &remove_machines {399		if !target_machines.remove(machine) {400			warn!("secret is not enabled for {machine}");401		}402	}403	for machine in &add_machines {404		if !target_machines.insert(machine.to_owned()) {405			warn!("secret is already added to {machine}");406		}407	}408	if !remove_machines.is_empty() {409		// TODO: maybe force secret regeneration?410		// Not that useful without revokation.411		warn!(412			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"413		);414	}415	Ok(target_machines)416}417impl Secret {418	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {419		match self {420			Secret::AddManager => {421				todo!("part of fleet-pusher")422			}423			Secret::ForceKeys => {424				for host in config.list_hosts().await? {425					if opts.should_skip(&host).await? {426						continue;427					}428					config.key(&host.name).await?;429				}430			}431			Secret::Read {432				name,433				machine,434				part: part_name,435				mut prefer_identities,436			} => {437				let Some(secret) = config.shared_secret(&name) else {438					bail!("secret doesn't exists");439				};440441				let dist = if secret.len() == 1 {442					&secret[0]443				} else if let Some(machine) = machine {444					let dist = secret.get(&machine);445					let Some(dist) = dist else {446						bail!("machine {machine} has no distribution of secret {name}");447					};448					prefer_identities.push(machine);449					dist450				} else {451					bail!(452						"secret {name} has shares, but no --machine specified for specifing which do you need"453					)454				};455456				let Some(part) = dist.secret.parts.get(&part_name) else {457					bail!("no part {part_name} in secret {name}");458				};459				let data = if part.raw.encrypted {460					let identity_holder = if !prefer_identities.is_empty() {461						prefer_identities462							.iter()463							.find(|i| dist.owners.iter().any(|s| s == *i))464					} else {465						dist.owners.first()466					};467					let Some(identity_holder) = identity_holder else {468						bail!("no available holder found");469					};470					let host = config.host(identity_holder).await?;471					host.decrypt(part.raw.clone()).await?472				} else {473					part.raw.data.clone()474				};475				stdout().write_all(&data)?;476			}477			Secret::Regenerate {478				prefer_identities,479				skip_hosts,480			} => {481				/*482								info!("checking for secrets to regenerate");483								let expected_shared_set = config484									.list_configured_shared()485									.await?486									.into_iter()487									.collect::<HashSet<_>>();488								let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();489								{490									// Generate missing shared491									let _span = info_span!("shared").entered();492									for missing in expected_shared_set.difference(&stored_shared_set) {493										let definition = config.shared_secret_definition(missing)?;494										if !definition.is_managed()? {495											info!("skipping unmanaged secret: {missing}");496											continue;497										}498										let expectations = definition499											.expectations()500											.with_context(|| format!("expectations for shared {missing:?}"))?;501										info!("generating secret: {missing}");502										let shared = generate_shared(config, missing, definition, &expectations)503											.in_current_span()504											.await?;505										config.replace_shared(missing.to_string(), shared)506									}507								}508								if !skip_hosts {509									for host in config.list_hosts().await? {510										if opts.should_skip(&host).await? {511											continue;512										}513514										let _span = info_span!("host", host = host.name).entered();515										let expected_set = host516											.list_defined_secrets()?517											.into_iter()518											.collect::<HashSet<_>>();519										let stored_set = config520											.list_secrets_for_owner(&host.name)521											.into_iter()522											.collect::<HashSet<_>>();523										for missing_secret in expected_set.difference(&stored_set) {524											let secret = host.secret_definition(missing_secret)?;525											if secret.is_shared()? {526												continue;527											}528											info!("generating missing secret: {missing_secret}");529											let expectations = secret.expectations().with_context(|| {530												format!("expectations for {missing_secret:?} of {:?}", host.name)531											})?;532											let generated = match generate(533												config,534												missing_secret,535												secret.definition_value()?,536												&expectations,537											)538											.in_current_span()539											.await540											{541												Ok(v) => v,542												Err(e) => {543													error!("{e:?}");544													continue;545												}546											};547											config.insert_secret(host.name, missing_secret.to_string(), generated)548										}549										for known_secret in stored_set.intersection(&expected_set) {550											let secret = host.secret_definition(known_secret)?;551											if secret.is_shared()? {552												continue;553											}554											info!("updating secret: {known_secret}");555											let data = config.host_secret(&host.name, known_secret)?;556											let expectations = secret.expectations()?;557											if let Some(regen_reason) = data.needs_regeneration(&expectations) {558												info!("needs regeneration: {regen_reason}");559												let generated = match generate(560													config,561													known_secret,562													secret.definition_value()?,563													&expectations,564												)565												.in_current_span()566												.await567												{568													Ok(v) => v,569													Err(e) => {570														error!("{e:?}");571														continue;572													}573												};574												config.insert_secret(575													&host.name,576													known_secret.to_string(),577													FleetLegacyHostSecret {578														managed: Some(true),579														secret: generated,580													},581												)582											}583										}584										for removed_secret in stored_set.difference(&expected_set) {585											let definition = host.secret_definition(removed_secret)?;586											if definition.is_shared()? {587												continue;588											}589											info!("removing secret: {removed_secret}");590											config.remove_secret(&host.name, removed_secret);591										}592									}593								}594								for known_secret in stored_shared_set.intersection(&expected_shared_set) {595									info!("updating shared secret: {known_secret}");596									let data = config.shared_secret(known_secret)?.expect("exists");597598									let definition = config.shared_secret_definition(known_secret)?;599									let expectations = definition.expectations()?;600									config.replace_shared(601										known_secret.to_owned(),602										maybe_regenerate_shared_secret(603											known_secret,604											config,605											data,606											definition,607											&prefer_identities,608											&expectations,609										)610										.await?,611									);612								}613								for removed_secret in stored_shared_set.difference(&expected_shared_set) {614									info!("removing shared secret: {removed_secret}");615									config.remove_shared(removed_secret);616								}617				*/618				todo!()619			}620			Secret::List {} => {621				let _span = info_span!("loading secrets").entered();622				let configured = config.list_configured_shared().await?;623				#[derive(Tabled)]624				struct SecretDisplay {625					#[tabled(rename = "Name")]626					name: String,627					#[tabled(rename = "Owners")]628					owners: String,629				}630				// let mut table = vec![];631				for name in configured.iter().cloned() {632					let config = config.clone();633					let data = config.shared_secret(&name).expect("exists");634					/*635										let definition = config.shared_secret_definition(&name)?;636										let expectations = definition.expectations()?;637										let owners = data638											.owners()639											.map(|o| {640												if expectations.owners.contains(o) {641													o.green().to_string()642												} else {643													o.red().to_string()644												}645											})646											.collect::<Vec<_>>();647										table.push(SecretDisplay {648											owners: owners.join(", "),649											name,650										})651					*/652				}653				// info!("loaded\n{}", Table::new(table).to_string())654			}655			Secret::Edit {656				name,657				machine,658				part,659				add,660			} => {661				let secret = config662					.host_secret(&machine, &name)663					.context("secret not found")?;664				if let Some(data) = secret.secret.parts.get(&part) {665					let host = config.host(&machine).await?;666					let secret = host.decrypt(data.raw.clone()).await?;667					String::from_utf8(secret).context("secret is not utf8")?668				} else if add {669					String::new()670				} else {671					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");672				};673			}674		}675		Ok(())676	}677}678679/*680async fn edit_temp_file(681	builder: tempfile::Builder<'_, '_>,682	r: Vec<u8>,683	header: &str,684	comment: &str,685) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {686	if !stdin().is_tty() {687		// TODO: Also try to open /dev/tty directly?688		bail!("stdin is not tty, can't open editor");689	}690691	use std::fmt::Write;692	let mut file = builder.tempfile()?;693694	let mut full_header = String::new();695	let mut had = false;696	for line in header.trim_end().lines() {697		had = true;698		writeln!(&mut full_header, "{comment}{line}")?;699	}700	if had {701		writeln!(&mut full_header, "{}", comment.trim_end())?;702	}703	writeln!(704		&mut full_header,705		"{comment}Do not touch this header! It will be removed automatically"706	)?;707708	file.write_all(full_header.as_bytes())?;709	file.write_all(&r)?;710711	let abs_path = file.into_temp_path();712	let editor = std::env::var_os("VISUAL")713		.or_else(|| std::env::var_os("EDITOR"))714		.unwrap_or_else(|| "vi".into());715	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())716		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;717	let editor_args = editor_args718		.into_iter()719		.map(|v| {720			// Only ASCII subsequences are replaced721			unsafe { OsString::from_encoded_bytes_unchecked(v) }722		})723		.collect_vec();724	let Some((editor, args)) = editor_args.split_first() else {725		bail!("EDITOR env var has no command");726	};727	let mut command = Command::new(editor);728	command.args(args);729730	let path_arg = abs_path.canonicalize()?;731732	// TODO: Save full state, using tcget/_getmode/_setmode733	let was_raw = terminal::is_raw_mode_enabled()?;734	terminal::enable_raw_mode()?;735736	let status = command.arg(path_arg).status().await;737738	if !was_raw {739		terminal::disable_raw_mode()?;740	}741742	let success = match status {743		Ok(s) => s.success(),744		Err(e) if e.kind() == io::ErrorKind::NotFound => {745			bail!("editor not found")746		}747		Err(e) => bail!("editor spawn error: {e}"),748	};749750	let mut file = std::fs::read(&abs_path).context("read editor output")?;751	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {752		todo!();753	};754	todo!();755756	// Ok((success, abs_path))757}758*/
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -153,7 +153,7 @@
 	#[serde(flatten)]
 	pub secret: FleetSecretData,
 
-	#[serde(default, skip_serializing, alias="managed")]
+	#[serde(default, skip_serializing, alias = "managed")]
 	pub _deprecated_managed: bool,
 }
 
modifiedcrates/nix-eval/src/util.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -1,15 +1,23 @@
 use std::time::Instant;
 
 use anyhow::bail;
+use serde::Deserialize;
 use tracing::{debug, warn};
 
 use crate::{Value, nix_go_json};
 
+#[derive(Deserialize, Debug)]
+struct Assertion {
+	assertion: bool,
+	message: String,
+}
+
 #[tracing::instrument(level = "info", skip(val))]
 pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
 	let before_errors = Instant::now();
 	let errors: Vec<String> = nix_go_json!(val.errors);
-	debug!("errors evaluation took {:?}", before_errors.elapsed());
+	// let assertions: Vec<Assertion> = nix_go_json!(val.assertions);
+	debug!("errors evaluation took {:?} {errors:?} ", before_errors.elapsed());
 	if !errors.is_empty() {
 		bail!(
 			"failed with error{}{}",
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -7,10 +7,10 @@
 [dependencies]
 thiserror.workspace = true
 
+itertools = "0.14.0"
 linked-hash-map = "0.5.6"
 peg = "0.8.5"
 ron = "0.11.0"
 serde = { version = "1.0.219", features = ["derive"] }
 serde-transcode = "1.1.1"
 serde_json = "1.0.140"
-itertools = "0.14.0"
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -150,7 +150,10 @@
       );
 
     mkAskPass =
-      { prompt ? "Secret value", part ? "secret" }:
+      {
+        prompt ? "Secret value",
+        part ? "secret",
+      }:
       (
         {
           kdePackages,
modifiedlib/flakePart.nixdiffbeforeafterboth
--- a/lib/flakePart.nix
+++ b/lib/flakePart.nix
@@ -34,7 +34,7 @@
           # to do that, evaluate all the modules with only needed option declared.
           bootstrapEval = lib.evalModules {
             class = "fleet";
-            prefix = ["fleetConfiguration"];
+            prefix = [ "fleetConfiguration" ];
             modules = [
               module
               {
@@ -53,7 +53,7 @@
           bootstrapNixpkgs = bootstrapEval.config.nixpkgs.buildUsing;
           normalEval = bootstrapNixpkgs.lib.evalModules {
             class = "fleet";
-            prefix = ["fleetConfiguration"];
+            prefix = [ "fleetConfiguration" ];
             modules = (import ../modules/module-list.nix) ++ [
               module
               (
modifiedmodules/nixos.nixdiffbeforeafterboth
--- a/modules/nixos.nix
+++ b/modules/nixos.nix
@@ -39,13 +39,23 @@
             in
             config.nixpkgs.buildUsing.lib.evalModules {
               class = "nixos";
-              prefix = ["fleetConfiguration" "hosts" hostArgs.config._module.args.name "nixos"];
+              prefix = [
+                "fleetConfiguration"
+                "hosts"
+                hostArgs.config._module.args.name
+                "nixos"
+              ];
               modules = (import "${modulesPath}/module-list.nix") ++ [
                 (module // { key = "attr<host.nixos>"; })
                 (config.nixos // { key = "attr<fleet.nixos>"; })
               ];
               specialArgs = {
-                inherit fleetLib inputs self modulesPath;
+                inherit
+                  fleetLib
+                  inputs
+                  self
+                  modulesPath
+                  ;
               };
             };
         };
@@ -54,32 +64,34 @@
         };
       };
       config = {
-        nixos = let 
-          inherit (hostArgs.config) system;
-        in {
-          _module.args = {
-            nixosHosts = mapAttrs (_: value: value.nixos_unchecked.config) config.hosts;
-            hosts = config.hosts;
-            host = hostArgs.config;
-            fleetConfiguration = config;
+        nixos =
+          let
+            inherit (hostArgs.config) system;
+          in
+          {
+            _module.args = {
+              nixosHosts = mapAttrs (_: value: value.nixos_unchecked.config) config.hosts;
+              hosts = config.hosts;
+              host = hostArgs.config;
+              fleetConfiguration = config;
 
-            inputs' = mapAttrs (
-              inputName: input:
-              builtins.addErrorContext
-                "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}"
-                (
-                  if input._type or null == "flake" then
-                    _fleetFlakeRootConfig.perInput system input
-                  else
-                    "input is not a flake, perhaps flake = false was added to te input declaration?"
-                )
-            ) inputs;
-            self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (
-              _fleetFlakeRootConfig.perInput system self
-            );
+              inputs' = mapAttrs (
+                inputName: input:
+                builtins.addErrorContext
+                  "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}"
+                  (
+                    if input._type or null == "flake" then
+                      _fleetFlakeRootConfig.perInput system input
+                    else
+                      "input is not a flake, perhaps flake = false was added to te input declaration?"
+                  )
+              ) inputs;
+              self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (
+                _fleetFlakeRootConfig.perInput system self
+              );
+            };
+            nixpkgs.hostPlatform = system;
           };
-          nixpkgs.hostPlatform = system;
-        };
         nixos_unchecked = hostArgs.config.nixos.extendModules {
           modules = [
             {
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -77,7 +77,7 @@
     }:
     let
       secretName = config._module.args.name;
-      literal = l: enum [l];
+      literal = l: enum [ l ];
     in
     {
       options = {
@@ -109,17 +109,16 @@
       config = {
         # C api is broken in regard to thunks
         # https://github.com/NixOS/nix/issues/12800
-        parts = let 
-          hostName = host._module.args.name;
-          generator = config.generator;
-        in builtins.deepSeq [
-          hostName
-          secretName
-          generator
-        ] (builtins.fleetEnsureHostSecret
-          hostName
-          secretName
-          generator);
+        parts =
+          let
+            hostName = host._module.args.name;
+            generator = config.generator;
+          in
+          builtins.deepSeq [
+            hostName
+            secretName
+            generator
+          ] (builtins.fleetEnsureHostSecret hostName secretName generator);
       };
     }
   );
@@ -136,14 +135,16 @@
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
-      apply = mapAttrs (_: secret: secret.parts // {definition = secret;});
+      apply = mapAttrs (_: secret: secret.parts // { definition = secret; });
       description = "Host-local secrets";
     };
     system.secretsData = mkOption {
       type = unspecified;
-      default = mapAttrs (_: s:
-        (removeAttrs s.definition ["generator"]) // {
-          parts = mapAttrs (_: part: removeAttrs part ["data"]) s.definition.parts;
+      default = mapAttrs (
+        _: s:
+        (removeAttrs s.definition [ "generator" ])
+        // {
+          parts = mapAttrs (_: part: removeAttrs part [ "data" ]) s.definition.parts;
         }
       ) config.secrets;
       description = "secrets.json contents";
@@ -152,13 +153,25 @@
   config = {
     environment.systemPackages = [ pkgs.fleet-install-secrets ];
 
-    assertions = mapAttrsToList (name: secret: let
-      hasSharedDefinition = fleetConfiguration.secrets ? name;
-    in {
-      assertion = (secret.definition.generator == "shared") == hasSharedDefinition && hasSharedDefinition -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners);
-      message = if hasSharedDefinition then"secret ${name} has host-specific secret generator, secrets with host-specific generators can not have shared generator in fleet configuration"
-      else "secret ${name} is declared as shared, for shared secret fleet configuration should include shared secret generator, and expectedOwners should contain this host";
-    }) config.secrets;
+    assertions = mapAttrsToList (
+      name: secret:
+      let
+        hasSharedDefinition = fleetConfiguration.secrets ? ${name};
+      in
+      {
+        assertion =
+          (secret.definition.generator == "shared") == hasSharedDefinition
+          && (
+            hasSharedDefinition
+            -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners)
+          );
+        message =
+          if hasSharedDefinition then
+            "secret ${name} has host-specific secret generator, secrets with host-specific generators can not have shared generator in fleet configuration"
+          else
+            "secret ${name} is declared as shared, for shared secret fleet configuration should include shared secret generator, and expectedOwners should contain this host";
+      }
+    ) config.secrets;
 
     systemd.services.fleet-install-secrets = mkIf useSysusers {
       wantedBy = [ "sysinit.target" ];
modifiedmodules/nixos/top-level.nixdiffbeforeafterboth
--- a/modules/nixos/top-level.nix
+++ b/modules/nixos/top-level.nix
@@ -2,6 +2,7 @@
   pkgs,
   config,
   lib,
+  ...
 }:
 let
   inherit (lib.strings) optionalString;