git.delta.rocks / jrsonnet / refs/commits / faec7071817b

difftreelog

refactor drop unnecessary async/await

vnlkvprsYaroslav Bolyukin2026-01-22parent: #33f3601.patch.diff
in: trunk

15 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
@@ -30,9 +30,9 @@
 
 async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {
 	info!("building");
-	let host = config.host(&hostname).await?;
+	let host = config.host(&hostname)?;
 	// let action = Action::from(self.subcommand.clone());
-	let nixos = host.nixos_config().await?;
+	let nixos = host.nixos_config()?;
 	let drv = nix_go!(nixos.system.build[{ build_attr }]);
 	let out_output = spawn_blocking(move || drv.build("out"))
 		.await
@@ -59,7 +59,7 @@
 
 impl BuildSystems {
 	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
-		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+		let hosts = opts.filter_skipped(config.list_hosts()?)?;
 		let set = LocalSet::new();
 		let build_attr = self.build_attr.clone();
 		for host in hosts {
@@ -95,20 +95,20 @@
 
 impl Deploy {
 	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
-		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+		let hosts = opts.filter_skipped(config.list_hosts()?)?;
 		let set = LocalSet::new();
 		for host in hosts.into_iter() {
 			let config = config.clone();
 			let span = info_span!("deploy", host = field::display(&host.name));
 			let hostname = host.name.clone();
 			let opts = opts.clone();
-			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
+			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind")? {
 				host.set_deploy_kind(deploy_kind);
 			};
-			if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
+			if let Some(destination) = opts.action_attr::<String>(&host, "dest")? {
 				host.set_session_destination(destination);
 			};
-			if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+			if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store")? {
 				host.set_legacy_ssh_store(legacy);
 			};
 
@@ -153,7 +153,7 @@
 						self.action,
 						&host,
 						remote_path,
-						match opts.action_attr(&host, "specialisation").await {
+						match opts.action_attr(&host, "specialisation") {
 							Ok(v) => v,
 							_ => {
 								error!("unreachable? failed to get specialization");
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -35,7 +35,7 @@
 		let mut data = Vec::new();
 		match self.cmd {
 			InfoCmd::ListHosts { ref tagged } => {
-				'host: for host in config.list_hosts().await? {
+				'host: for host in config.list_hosts()? {
 					if !tagged.is_empty() {
 						let config = &config.config_field;
 						let host_name = &host.name;
@@ -59,7 +59,7 @@
 					"at leas one of --external or --internal must be set"
 				);
 				let mut out = <BTreeSet<String>>::new();
-				let host = config.system_config(&host).await?;
+				let host = config.system_config(&host)?;
 				if external {
 					let data: Vec<String> = nix_go_json!(host.network.externalIps);
 					out.extend(data);
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -75,7 +75,7 @@
 
 impl RollbackSingle {
 	pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
-		let host = config.host(&self.machine).await?;
+		let host = config.host(&self.machine)?;
 		match &self.action {
 			RollbackAction::ListTargets => {
 				let generations = list_all_generations(&host, config).await;
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeSet, HashSet},3	io::{Read, Write, stdin, stdout},4	path::PathBuf,5};67use anyhow::{Context, Result, bail, ensure};8use clap::Parser;9use fleet_base::{host::Config, opts::FleetOpts};10use fleet_shared::SecretData;11use tabled::Tabled;12use tokio::fs::read;13use tracing::{info, info_span, warn};1415#[derive(Parser)]16pub enum Secret {17	AddManager,18	/// Force load host keys for all defined hosts19	ForceKeys,20	/// Read secret from remote host, requires sudo on one of the owning hosts21	Read {22		/// Secret name to read23		name: String,2425		/// Distribution with what machine to read26		/// If not shared between multiple - defaults to single owner27		#[clap(short = 'm', long)]28		machine: Option<String>,2930		/// Which private secret part to read31		#[clap(short = 'p', long, default_value = "secret")]32		part: String,3334		/// Which host should we use to decrypt, in case if reencryption is required, without35		/// regeneration36		#[clap(long)]37		prefer_identities: Vec<String>,38	},39	Regenerate {40		/// Which host should we use to decrypt, in case if reencryption is required, without41		/// regeneration42		#[clap(long)]43		prefer_identities: Vec<String>,44		/// Only regenerate shared secrets45		#[clap(long)]46		skip_hosts: bool,47	},48	List {},49	Edit {50		name: String,51		#[clap(short = 'm', long)]52		machine: String,5354		#[clap(long)]55		add: bool,5657		/// Which private secret part to read58		#[clap(short = 'p', long, default_value = "secret")]59		part: String,60	},61}6263/*64#[allow(clippy::too_many_arguments)]65#[tracing::instrument(skip(config, secret, definition, prefer_identities))]66async fn maybe_regenerate_shared_secret(67	secret_name: &str,68	config: &Config,69	mut secret: FleetSecretDistribution,70	definition: SharedSecretDefinition,71	prefer_identities: &[String],72	expectations: &Expectations,73) -> Result<FleetSecretDistribution> {74	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);75	let value = definition.definition_value();7677	let (should_reencrypt, reason) = match reason {78		Some(RegenerationReason::OwnersAdded(_)) => {79			// Secret always needs to be reencrypted for new owners to be able to read it80			(81				true,82				if nix_go_json!(value.regenerateOnOwnerAdded) {83					reason84				} else {85					None86				},87			)88		}89		Some(RegenerationReason::OwnersRemoved(_)) => {90			// No need to reencrypt, we can just leave stanzas in place.91			if nix_go_json!(value.regenerateOnOwnerRemoved) {92				(true, reason)93			} else {94				(false, None)95			}96		}97		Some(_) => (true, reason),98		None => (false, None),99	};100101	if let Some(reason) = reason {102		info!("secret needs to be regenerated: {reason}");103		let generated = generate_shared(config, secret_name, definition, expectations).await?;104		Ok(generated)105	} else if should_reencrypt {106		info!("secret needs to be reencrypted");107		let identity_holder = if !prefer_identities.is_empty() {108			prefer_identities109				.iter()110				.find(|i| secret.owners.iter().any(|s| s == *i))111		} else {112			secret.owners.first()113		};114		let Some(identity_holder) = identity_holder else {115			bail!("no available holder found");116		};117118		for (part_name, part) in secret.secret.parts.iter_mut() {119			let _span = info_span!("part reencryption", part_name);120			if !part.raw.encrypted {121				continue;122			}123			let host = config.host(identity_holder).await?;124			let encrypted = host125				.reencrypt(126					part.raw.clone(),127					expectations.owners.iter().cloned().collect(),128				)129				.await?;130			part.raw = encrypted;131		}132		secret.owners = expectations.owners.clone();133		Ok(secret)134	} else {135		Ok(secret)136	}137}138*/139140/*141async fn generate_pure(142	_config: &Config,143	_display_name: &str,144	_secret: Value,145	_default_generator: Value,146	_expectations: &Expectations,147) -> Result<FleetSecretData> {148	bail!("pure generators are broken for now")149}150async fn generate_impure(151	config: &Config,152	_display_name: &str,153	secret: Value,154	default_generator: Value,155	expectations: &Expectations,156) -> Result<FleetSecretData> {157	let generator = nix_go!(secret.generator);158	let on: Option<String> = nix_go_json!(default_generator.impureOn);159160	let nixpkgs = &config.nixpkgs;161162	let host = if let Some(on) = &on {163		config.host(on).await?164	} else {165		config.local_host()166	};167	let on_pkgs = host.pkgs().await?;168	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);169170	let mut recipients = Vec::new();171	for owner in &expectations.owners {172		let key = config.key(owner).await?;173		recipients.push(key);174	}175	let generators = nix_go!(mk_secret_generators(Obj { recipients }));176	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;177178	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));179180	let generator = nix_go!(call_package(generator)(Obj {}));181182	let generator = spawn_blocking(move || generator.build("out"))183		.await184		.expect("nix build shouldn't fail")?;185	let generator = host.remote_derivation(&generator).await?;186187	let out_parent = host.mktemp_dir().await?;188	let out = format!("{out_parent}/out");189190	let mut r#gen = host.cmd(generator).await?;191	r#gen.env("out", &out);192	if on.is_none() {193		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.194		let project_path: String = config195			.directory196			.clone()197			.into_os_string()198			.into_string()199			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;200		r#gen.env("FLEET_PROJECT", project_path);201	}202	r#gen.run().await.context("impure generator")?;203204	{205		let marker = host.read_file_text(format!("{out}/marker")).await?;206		ensure!(marker == "SUCCESS", "generation not succeeded");207	}208209	let mut parts = BTreeMap::new();210	for part in host.read_dir(&out).await? {211		if part == "created_at" || part == "expires_at" || part == "marker" {212			continue;213		}214		let contents: SecretData = host215			.read_file_text(format!("{out}/{part}"))216			.await?217			.parse()218			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;219		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });220	}221222	let created_at = host.read_file_value(format!("{out}/created_at")).await?;223	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();224225	let new_data = FleetSecretData {226		created_at,227		expires_at,228		parts,229		generation_data: expectations.generation_data.clone(),230	};231232	if let Some(reason) = secret_needs_regeneration(&new_data, &expectations.owners, expectations) {233		bail!("newly generated secret needs to be regenerated: {reason}")234	}235236	Ok(new_data)237}238239async fn generate(240	config: &Config,241	display_name: &str,242	secret: Value,243	expectations: &Expectations,244) -> Result<FleetSecretData> {245	let generator = nix_go!(secret.generator);246	// Can't properly check on nix module system level247	{248		let gen_ty = generator.type_of();249		if matches!(gen_ty, NixType::Null) {250			bail!("secret has no generator defined, can't automatically generate it.");251		}252		if matches!(gen_ty, NixType::Attrs) {253			if !generator.has_field("__functor")? {254				bail!("generator should be functor, got {gen_ty:?}");255			}256		} else if matches!(gen_ty, NixType::Function) {257			bail!("generator should be functor, got {gen_ty:?}");258		}259	}260	let nixpkgs = &config.nixpkgs;261	let default_pkgs = &config.default_pkgs;262	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);263	// Generators provide additional information in passthru, to access264	// passthru we should call generator, but information about where this generator is supposed to build265	// is located in passthru... Thus evaluating generator on host.266	//267	// Maybe it is also possible to do some magic with __functor?268	//269	// I don't want to make modules always responsible for additional secret data anyway,270	// so it should be in derivation, and not in the secret data itself.271	let generators = nix_go!(default_mk_secret_generators(Obj {272		recipients: <Vec<String>>::new(),273	}));274	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;275276	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));277	let default_generator = nix_go!(call_package(generator)(Obj {}));278279	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);280281	match kind {282		GeneratorKind::Impure => {283			generate_impure(284				config,285				display_name,286				secret,287				default_generator,288				expectations,289			)290			.await291		}292		GeneratorKind::Pure => {293			generate_pure(294				config,295				display_name,296				secret,297				default_generator,298				expectations,299			)300			.await301		}302	}303}304*/305/*306async fn generate_shared(307	config: &Config,308	display_name: &str,309	secret: SharedSecretDefinition,310	expectations: &Expectations,311) -> Result<FleetSecretDistribution> {312	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);313	Ok(FleetSecretDistribution {314		managed: Some(true),315		secret: generate(316			config,317			display_name,318			secret.definition_value(),319			expectations,320		)321		.await?,322		owners: expectations.owners.clone(),323	})324}*/325326async fn parse_public(327	public: Option<String>,328	public_file: Option<PathBuf>,329) -> Result<Option<SecretData>> {330	Ok(match (public, public_file) {331		(Some(v), None) => Some(SecretData {332			data: v.into(),333			encrypted: false,334		}),335		(None, Some(v)) => Some(SecretData {336			data: read(v).await?,337			encrypted: false,338		}),339		(Some(_), Some(_)) => {340			bail!("only public or public_file should be set")341		}342		(None, None) => None,343	})344}345346async fn parse_secret() -> Result<Option<Vec<u8>>> {347	let mut input = vec![];348	stdin().read_to_end(&mut input)?;349	if input.is_empty() {350		Ok(None)351	} else {352		Ok(Some(input))353	}354}355356fn parse_machines(357	initial: BTreeSet<String>,358	machines: Option<Vec<String>>,359	mut add_machines: Vec<String>,360	mut remove_machines: Vec<String>,361) -> Result<BTreeSet<String>> {362	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {363		bail!("no operation");364	}365366	let initial_machines = initial.clone();367	let mut target_machines = initial;368	info!("Currently encrypted for {initial_machines:?}");369370	if let Some(machines) = machines {371		ensure!(372			add_machines.is_empty() && remove_machines.is_empty(),373			"can't combine --machines and --add-machines/--remove-machines"374		);375		let target = initial_machines.iter().collect::<HashSet<_>>();376		let source = machines.iter().collect::<HashSet<_>>();377		for removed in target.difference(&source) {378			remove_machines.push((*removed).clone());379		}380		for added in source.difference(&target) {381			add_machines.push((*added).clone());382		}383	}384385	for machine in &remove_machines {386		if !target_machines.remove(machine) {387			warn!("secret is not enabled for {machine}");388		}389	}390	for machine in &add_machines {391		if !target_machines.insert(machine.to_owned()) {392			warn!("secret is already added to {machine}");393		}394	}395	if !remove_machines.is_empty() {396		// TODO: maybe force secret regeneration?397		// Not that useful without revokation.398		warn!(399			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"400		);401	}402	Ok(target_machines)403}404impl Secret {405	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {406		match self {407			Secret::AddManager => {408				todo!("part of fleet-pusher")409			}410			Secret::ForceKeys => {411				for host in config.list_hosts()? {412					if opts.should_skip(&host)? {413						continue;414					}415					config.key(&host.name).await?;416				}417			}418			Secret::Read {419				name,420				machine,421				part: part_name,422				mut prefer_identities,423			} => {424				let Some(secret) = config.shared_secret(&name) else {425					bail!("secret doesn't exists");426				};427428				let dist = if secret.len() == 1 {429					&secret[0]430				} else if let Some(machine) = machine {431					let dist = secret.get(&machine);432					let Some(dist) = dist else {433						bail!("machine {machine} has no distribution of secret {name}");434					};435					prefer_identities.push(machine);436					dist437				} else {438					bail!(439						"secret {name} has shares, but no --machine specified for specifing which do you need"440					)441				};442443				let Some(part) = dist.secret.parts.get(&part_name) else {444					bail!("no part {part_name} in secret {name}");445				};446				let data = if part.raw.encrypted {447					let identity_holder = if !prefer_identities.is_empty() {448						prefer_identities449							.iter()450							.find(|i| dist.owners.iter().any(|s| s == *i))451					} else {452						dist.owners.first()453					};454					let Some(identity_holder) = identity_holder else {455						bail!("no available holder found");456					};457					let host = config.host(identity_holder)?;458					host.decrypt(part.raw.clone()).await?459				} else {460					part.raw.data.clone()461				};462				stdout().write_all(&data)?;463			}464			Secret::Regenerate {465				prefer_identities,466				skip_hosts,467			} => {468				/*469								info!("checking for secrets to regenerate");470								let expected_shared_set = config471									.list_configured_shared()472									.await?473									.into_iter()474									.collect::<HashSet<_>>();475								let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();476								{477									// Generate missing shared478									let _span = info_span!("shared").entered();479									for missing in expected_shared_set.difference(&stored_shared_set) {480										let definition = config.shared_secret_definition(missing)?;481										if !definition.is_managed()? {482											info!("skipping unmanaged secret: {missing}");483											continue;484										}485										let expectations = definition486											.expectations()487											.with_context(|| format!("expectations for shared {missing:?}"))?;488										info!("generating secret: {missing}");489										let shared = generate_shared(config, missing, definition, &expectations)490											.in_current_span()491											.await?;492										config.replace_shared(missing.to_string(), shared)493									}494								}495								if !skip_hosts {496									for host in config.list_hosts().await? {497										if opts.should_skip(&host).await? {498											continue;499										}500501										let _span = info_span!("host", host = host.name).entered();502										let expected_set = host503											.list_defined_secrets()?504											.into_iter()505											.collect::<HashSet<_>>();506										let stored_set = config507											.list_secrets_for_owner(&host.name)508											.into_iter()509											.collect::<HashSet<_>>();510										for missing_secret in expected_set.difference(&stored_set) {511											let secret = host.secret_definition(missing_secret)?;512											if secret.is_shared()? {513												continue;514											}515											info!("generating missing secret: {missing_secret}");516											let expectations = secret.expectations().with_context(|| {517												format!("expectations for {missing_secret:?} of {:?}", host.name)518											})?;519											let generated = match generate(520												config,521												missing_secret,522												secret.definition_value()?,523												&expectations,524											)525											.in_current_span()526											.await527											{528												Ok(v) => v,529												Err(e) => {530													error!("{e:?}");531													continue;532												}533											};534											config.insert_secret(host.name, missing_secret.to_string(), generated)535										}536										for known_secret in stored_set.intersection(&expected_set) {537											let secret = host.secret_definition(known_secret)?;538											if secret.is_shared()? {539												continue;540											}541											info!("updating secret: {known_secret}");542											let data = config.host_secret(&host.name, known_secret)?;543											let expectations = secret.expectations()?;544											if let Some(regen_reason) = data.needs_regeneration(&expectations) {545												info!("needs regeneration: {regen_reason}");546												let generated = match generate(547													config,548													known_secret,549													secret.definition_value()?,550													&expectations,551												)552												.in_current_span()553												.await554												{555													Ok(v) => v,556													Err(e) => {557														error!("{e:?}");558														continue;559													}560												};561												config.insert_secret(562													&host.name,563													known_secret.to_string(),564													FleetLegacyHostSecret {565														managed: Some(true),566														secret: generated,567													},568												)569											}570										}571										for removed_secret in stored_set.difference(&expected_set) {572											let definition = host.secret_definition(removed_secret)?;573											if definition.is_shared()? {574												continue;575											}576											info!("removing secret: {removed_secret}");577											config.remove_secret(&host.name, removed_secret);578										}579									}580								}581								for known_secret in stored_shared_set.intersection(&expected_shared_set) {582									info!("updating shared secret: {known_secret}");583									let data = config.shared_secret(known_secret)?.expect("exists");584585									let definition = config.shared_secret_definition(known_secret)?;586									let expectations = definition.expectations()?;587									config.replace_shared(588										known_secret.to_owned(),589										maybe_regenerate_shared_secret(590											known_secret,591											config,592											data,593											definition,594											&prefer_identities,595											&expectations,596										)597										.await?,598									);599								}600								for removed_secret in stored_shared_set.difference(&expected_shared_set) {601									info!("removing shared secret: {removed_secret}");602									config.remove_shared(removed_secret);603								}604				*/605				todo!()606			}607			Secret::List {} => {608				let _span = info_span!("loading secrets").entered();609				let configured = config.list_configured_shared()?;610				#[derive(Tabled)]611				struct SecretDisplay {612					#[tabled(rename = "Name")]613					name: String,614					#[tabled(rename = "Owners")]615					owners: String,616				}617				// let mut table = vec![];618				for name in configured.iter().cloned() {619					let config = config.clone();620					let data = config.shared_secret(&name).expect("exists");621					/*622										let definition = config.shared_secret_definition(&name)?;623										let expectations = definition.expectations()?;624										let owners = data625											.owners()626											.map(|o| {627												if expectations.owners.contains(o) {628													o.green().to_string()629												} else {630													o.red().to_string()631												}632											})633											.collect::<Vec<_>>();634										table.push(SecretDisplay {635											owners: owners.join(", "),636											name,637										})638					*/639				}640				// info!("loaded\n{}", Table::new(table).to_string())641			}642			Secret::Edit {643				name,644				machine,645				part,646				add,647			} => {648				let secret = config649					.host_secret(&machine, &name)650					.context("secret not found")?;651				if let Some(data) = secret.secret.parts.get(&part) {652					let host = config.host(&machine)?;653					let secret = host.decrypt(data.raw.clone()).await?;654					String::from_utf8(secret).context("secret is not utf8")?655				} else if add {656					String::new()657				} else {658					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");659				};660			}661		}662		Ok(())663	}664}665666/*667async fn edit_temp_file(668	builder: tempfile::Builder<'_, '_>,669	r: Vec<u8>,670	header: &str,671	comment: &str,672) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {673	if !stdin().is_tty() {674		// TODO: Also try to open /dev/tty directly?675		bail!("stdin is not tty, can't open editor");676	}677678	use std::fmt::Write;679	let mut file = builder.tempfile()?;680681	let mut full_header = String::new();682	let mut had = false;683	for line in header.trim_end().lines() {684		had = true;685		writeln!(&mut full_header, "{comment}{line}")?;686	}687	if had {688		writeln!(&mut full_header, "{}", comment.trim_end())?;689	}690	writeln!(691		&mut full_header,692		"{comment}Do not touch this header! It will be removed automatically"693	)?;694695	file.write_all(full_header.as_bytes())?;696	file.write_all(&r)?;697698	let abs_path = file.into_temp_path();699	let editor = std::env::var_os("VISUAL")700		.or_else(|| std::env::var_os("EDITOR"))701		.unwrap_or_else(|| "vi".into());702	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())703		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;704	let editor_args = editor_args705		.into_iter()706		.map(|v| {707			// Only ASCII subsequences are replaced708			unsafe { OsString::from_encoded_bytes_unchecked(v) }709		})710		.collect_vec();711	let Some((editor, args)) = editor_args.split_first() else {712		bail!("EDITOR env var has no command");713	};714	let mut command = Command::new(editor);715	command.args(args);716717	let path_arg = abs_path.canonicalize()?;718719	// TODO: Save full state, using tcget/_getmode/_setmode720	let was_raw = terminal::is_raw_mode_enabled()?;721	terminal::enable_raw_mode()?;722723	let status = command.arg(path_arg).status().await;724725	if !was_raw {726		terminal::disable_raw_mode()?;727	}728729	let success = match status {730		Ok(s) => s.success(),731		Err(e) if e.kind() == io::ErrorKind::NotFound => {732			bail!("editor not found")733		}734		Err(e) => bail!("editor spawn error: {e}"),735	};736737	let mut file = std::fs::read(&abs_path).context("read editor output")?;738	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {739		todo!();740	};741	todo!();742743	// Ok((success, abs_path))744}745*/
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -216,13 +216,10 @@
 		.map(|a| extra_args::parse_os(&a))
 		.transpose()?
 		.unwrap_or_default();
-	let config = opts
-		.fleet_opts
-		.build(
-			nix_args,
-			matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
-		)
-		.await?;
+	let config = opts.fleet_opts.build(
+		nix_args,
+		matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
+	)?;
 
 	match run_command(&config, opts.fleet_opts, opts.command).await {
 		Ok(()) => {
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -421,3 +421,14 @@
 		}
 	}
 }
+
+#[derive(Debug)]
+pub struct Expectations {
+	pub owners: BTreeSet<String>,
+	pub generation_data: serde_json::Value,
+	pub parts: BTreeMap<String, GeneratorPart>,
+}
+#[derive(Deserialize, Debug, Clone)]
+pub struct GeneratorPart {
+	pub encrypted: bool,
+}
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -471,10 +471,13 @@
 		cmd.run().await
 	}
 }
+
+struct HostSecretDefinition(Value);
+
 impl ConfigHost {
 	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,
 	// assuming getting tags always returns the same value.
-	pub async fn tags(&self) -> Result<Vec<String>> {
+	pub fn tags(&self) -> Result<Vec<String>> {
 		if let Some(v) = self.groups.get() {
 			return Ok(v.clone());
 		}
@@ -487,7 +490,7 @@
 
 		Ok(tags)
 	}
-	pub async fn nixos_config(&self) -> Result<Value> {
+	pub fn nixos_config(&self) -> Result<Value> {
 		if let Some(v) = self.nixos_config.get() {
 			return Ok(v.clone());
 		}
@@ -495,7 +498,7 @@
 			bail!("local host has no nixos_config");
 		};
 		let nixos_config = nix_go!(host_config.nixos.config);
-		assert_warn("nixos config evaluation", &nixos_config).await?;
+		assert_warn("nixos config evaluation", &nixos_config)?;
 
 		let _ = self.nixos_config.set(nixos_config.clone());
 
@@ -522,7 +525,7 @@
 	}
 
 	/// Packages for this host, resolved with nixpkgs overlays
-	pub async fn pkgs(&self) -> Result<Value> {
+	pub fn pkgs(&self) -> Result<Value> {
 		if let Some(value) = &self.pkgs_override {
 			return Ok(value.clone());
 		}
@@ -534,17 +537,29 @@
 	}
 }
 
+pub struct SharedSecretDefinition(Value);
+impl SharedSecretDefinition {
+	pub fn expected_owners(&self) -> Result<BTreeSet<String>> {
+		let secret = &self.0;
+		Ok(nix_go_json!(secret.expectedOwners))
+	}
+	pub fn generator(&self) -> Result<Value> {
+		let secret = &self.0;
+		Ok(nix_go!(secret.generator))
+	}
+}
+
 impl Config {
-	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {
+	pub fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {
 		let config = &self.config_field;
 		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);
 		Ok(tagged)
 	}
-	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {
+	pub fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {
 		let mut out = BTreeSet::new();
 		for owner in owners {
 			if let Some(tag) = owner.strip_prefix('@') {
-				let hosts = self.tagged_hostnames(tag).await?;
+				let hosts = self.tagged_hostnames(tag)?;
 				out.extend(hosts);
 			} else {
 				out.insert(owner);
@@ -574,7 +589,7 @@
 		}
 	}
 
-	pub async fn host(&self, name: &str) -> Result<ConfigHost> {
+	pub fn host(&self, name: &str) -> Result<ConfigHost> {
 		let config = &self.config_field;
 		let host_config = nix_go!(config.hosts[{ name }]);
 
@@ -595,23 +610,23 @@
 			legacy_ssh_store: OnceCell::new(),
 		})
 	}
-	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
+	pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
 		let config = &self.config_field;
 		let names = nix_go!(config.hosts).list_fields()?;
 		let mut out = vec![];
 		for name in names {
-			out.push(self.host(&name).await?);
+			out.push(self.host(&name)?);
 		}
 		Ok(out)
 	}
 	// TODO: Replace usages with .host().nixos_config
-	pub async fn system_config(&self, host: &str) -> Result<Value> {
+	pub fn system_config(&self, host: &str) -> Result<Value> {
 		let fleet_field = &self.config_field;
 		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))
 	}
 
 	/// Shared secrets configured in fleet.nix or in flake
-	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
+	pub fn list_configured_shared(&self) -> Result<Vec<String>> {
 		let config_field = &self.config_field;
 		nix_go!(config_field.sharedSecrets).list_fields()
 	}
@@ -659,6 +674,17 @@
 		data.secrets.get(secret).cloned()
 	}
 
+	pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {
+		let config = &self.config_field;
+		let shared_secrets = nix_go!(config.secrets);
+		if !shared_secrets.has_field(secret)? {
+			return Ok(None);
+		}
+		Ok(Some(SharedSecretDefinition(nix_go!(
+			shared_secrets[secret]
+		))))
+	}
+
 	// TODO: Should this be something modifiable from other processes?
 	// E.g terraform provider might want to update FleetData (e.g secrets),
 	// and current implementation assumes only one process holds current fleet.nix
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -12,10 +12,10 @@
 	pub fn cached_key(&self, host: &str) -> Option<String> {
 		let data = self.data();
 		let key = data.hosts.get(host).map(|h| &h.encryption_key);
-		if let Some(key) = key {
-			if key.is_empty() {
-				return None;
-			}
+		if let Some(key) = key
+			&& key.is_empty()
+		{
+			return None;
 		}
 		key.cloned()
 	}
@@ -30,7 +30,7 @@
 			Ok(key)
 		} else {
 			warn!("Loading key for {}", host);
-			let host = self.host(host).await?;
+			let host = self.host(host)?;
 			let mut cmd = host.cmd("cat").await?;
 			cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub");
 			let key = cmd.run_string().await?;
@@ -47,7 +47,7 @@
 	}
 
 	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {
-		let hosts = self.expand_owner_set(hosts).await?;
+		let hosts = self.expand_owner_set(hosts)?;
 		futures::stream::iter(hosts.iter())
 			.then(|m| self.recipient(m.as_ref()))
 			.try_collect::<Vec<_>>()
@@ -57,12 +57,7 @@
 	#[allow(dead_code)]
 	pub async fn orphaned_data(&self) -> Result<Vec<String>> {
 		let mut out = Vec::new();
-		let host_names = self
-			.list_hosts()
-			.await?
-			.into_iter()
-			.map(|h| h.name)
-			.collect_vec();
+		let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
 		for hostname in self
 			.data()
 			.hosts
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -104,20 +104,20 @@
 }
 
 impl FleetOpts {
-	pub async fn filter_skipped(
+	pub fn filter_skipped(
 		&self,
 		hosts: impl IntoIterator<Item = ConfigHost>,
 	) -> Result<Vec<ConfigHost>> {
 		let mut out = Vec::new();
 		for host in hosts {
-			if self.should_skip(&host).await? {
+			if self.should_skip(&host)? {
 				continue;
 			}
 			out.push(host);
 		}
 		Ok(out)
 	}
-	pub async fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
+	pub fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
 		if self.skip.iter().any(|h| h as &str == host.name) {
 			return Ok(true);
 		}
@@ -137,7 +137,7 @@
 			}
 		}
 		if have_group_matches {
-			let host_tags = host.tags().await?;
+			let host_tags = host.tags()?;
 			for item in self.only.iter() {
 				match item {
 					HostItem::Tag { name, .. } if host_tags.contains(name) => {
@@ -149,15 +149,15 @@
 		}
 		Ok(true)
 	}
-	pub async fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
+	pub fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
 	where
 		T::Err: Sync,
 		anyhow::Error: From<T::Err>,
 	{
-		let str = self.action_attr_str(host, attr).await?;
+		let str = self.action_attr_str(host, attr)?;
 		Ok(str.map(|v| T::from_str(&v)).transpose()?)
 	}
-	pub async fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
+	pub fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
 		if self.only.is_empty() {
 			return Ok(None);
 		}
@@ -176,7 +176,7 @@
 			}
 		}
 		if have_group_matches {
-			let host_tags = host.tags().await?;
+			let host_tags = host.tags()?;
 			for item in self.only.iter() {
 				match item {
 					HostItem::Tag { name, attrs }
@@ -195,7 +195,7 @@
 	}
 
 	// TODO: Config should be detached from opts.
-	pub async fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
+	pub fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
 		let cwd = current_dir()?;
 		let mut directory = cwd.clone();
 		let mut fleet_data_path = directory.join("fleet.nix");
@@ -248,7 +248,6 @@
 
 		if assert {
 			assert_warn("fleet config evaluation", &config_field)
-				.await
 				.context("failed to verify assertions")?;
 		}
 
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,38 +1,168 @@
-use std::cell::OnceCell;
-use std::collections::{BTreeMap, HashMap};
-use std::sync::{Arc, Mutex, OnceLock};
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::sync::OnceLock;
 
-use anyhow::{Context, bail};
+use anyhow::{Context, bail, ensure};
+use fleet_shared::SecretData;
 use itertools::Itertools;
 use nix_eval::{NativeFn, Value, nix_go, nix_go_json};
 use serde::Deserialize;
 use tracing::{info, warn};
 
-use crate::fleetdata::{FleetData, FleetSecrets};
-use crate::host::Config;
+use crate::fleetdata::{
+	Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
+};
+use crate::host::{Config, ConfigHost};
+use crate::secret::{RegenerationReason, secret_needs_regeneration};
+use anyhow::{Result, anyhow};
 
 #[derive(thiserror::Error, Debug)]
 enum Error {}
 
-struct Parts {
-	encrypted: Vec<String>,
-	public: Vec<String>,
+pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum GeneratorKind {
+	Impure,
+	Pure,
 }
 
-trait SecretsBackend {
-	fn has_shared(&self, name: &str);
-	fn has_host(&self, host: &str, name: &str);
-	fn shared_parts(&self, name: &str) -> Parts;
-	fn host_parts(&self, host: &str, name: &str) -> Parts;
+pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {
+	info!("get pkgs");
+	let pkgs = host_on.pkgs()?;
+	let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
+	let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
+	Ok(pkgs.clone().attrs_update(generators)?)
+}
+pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {
+	let host_on = config.local_host();
+	get_pkgs_and_generators(&host_on, vec![])
 }
+pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result<Value> {
+	ensure!(
+		package.is_function(),
+		"package should be a function to be called with callPackage"
+	);
+	// No need to use nixpkgs.buildUsing, as only nixpkgs-lib is used.
+	let nixpkgs = &config.nixpkgs;
+	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs));
+	Ok(nix_go!(call_package(package)(Obj {})))
+}
+
+pub fn get_default_generator_drv(config: &Config, generator: &Value) -> Result<Value> {
+	let default_pkgs_and_generators = get_default_pkgs_and_generators(config)?;
+	let default_generator_drv = call_package(config, &default_pkgs_and_generators, generator)
+		.context("failed to initialize generator to get metadata")?;
+
+	Ok(default_generator_drv)
+}
+
+pub async fn generate(
+	config: &Config,
+	expectations: Expectations,
+	generator: &Value,
+	default_generator_drv: &Value,
+) -> Result<FleetSecretDistribution> {
+	let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind);
+
+	match kind {
+		GeneratorKind::Impure => {
+			let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);
 
-struct FsSecretsBackend {}
+			let host_on = if let Some(on) = &impure_on {
+				config
+					.host(on)
+					.context("failed to get secret generation target host")?
+			} else {
+				config.local_host()
+			};
+			let pkgs_and_generators =
+				get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())
+					.context("failed to get pkgs for target host")?;
+			let generator = call_package(config, &pkgs_and_generators, generator)
+				.context("failed to evaluate generator for target host")?;
 
-pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+			let generator = generator
+				.build("out")
+				.context("failed to build generator for target host")?;
 
-#[derive(Deserialize, Debug)]
-struct GeneratorPart {
-	encrypted: bool,
+			let generator = host_on
+				.remote_derivation(&generator)
+				.await
+				.context("failed to copy generator to target host")?;
+
+			// TODO: Remove destdir after everything is done
+			let out_parent = host_on
+				.mktemp_dir()
+				.await
+				.context("failed to prepare generator output dir on target host")?;
+			let out = format!("{out_parent}/out");
+			let mut generator_cmd = host_on.cmd(generator).await?;
+			generator_cmd.env("out", &out);
+			if impure_on.is_none() {
+				let project_path: String = config
+					.directory
+					.clone()
+					.into_os_string()
+					.into_string()
+					.map_err(|e| anyhow!("fleet project path is not utf-8: {e:?}"))?;
+				generator_cmd.env("FLEET_PROJECT", project_path);
+			};
+			generator_cmd
+				.run()
+				.await
+				.context("failed to run impure generator")?;
+
+			{
+				let marker = host_on.read_file_text(format!("{out}/marker")).await?;
+				ensure!(
+					marker == "SUCCESS",
+					"impure generator ended prematurely, secret generation failed"
+				);
+			}
+
+			let mut parts = BTreeMap::new();
+			for part in host_on.read_dir(&out).await? {
+				if part == "created_at" || part == "expires_at" || part == "marker" {
+					continue;
+				}
+				let contents: SecretData = host_on
+					.read_file_text(format!("{out}/{part}"))
+					.await?
+					.parse()
+					.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;
+				parts.insert(part.to_owned(), FleetSecretPart { raw: contents });
+			}
+
+			let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;
+			let expires_at = host_on
+				.read_file_value(format!("{out}/expires_at"))
+				.await
+				.ok();
+
+			let new_data = FleetSecretData {
+				created_at,
+				expires_at,
+				parts,
+				generation_data: expectations.generation_data.clone(),
+			};
+
+			let new_data = FleetSecretDistribution {
+				secret: new_data,
+				owners: expectations.owners.clone(),
+				_deprecated_managed: true,
+			};
+
+			if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {
+				bail!("newly generated secret needs to be regenerated: {reason}")
+			}
+
+			Ok(new_data)
+		}
+		GeneratorKind::Pure => {
+			bail!("pure generators are disabled for now")
+		}
+	}
 }
 
 pub fn init_primops() {
@@ -52,52 +182,61 @@
 				.get()
 				.expect("primops data should be set on init");
 
-			info!("get pkgs");
-			let nixpkgs = &config.nixpkgs;
-			let default_pkgs = &config.default_pkgs;
-			let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
-			let generators = nix_go!(default_mk_secret_generators(Obj {
-				recipients: <Vec<String>>::new(),
-			}));
-			let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;
+			let shared_def = config.secret_definition(&secret).context("failed to get shared secret definition")?;
 
-			info!("call package");
-			let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
-			let default_generator = call_package
-				.call(generator.clone())
-				.context("calling callPackage with generator")?
-				.call(Value::new_attrs(HashMap::new()))
-				.context("providing extra callPackage args")?;
+			let (shared, generator, expected_owners) = if generator.is_string() {
+				assert_eq!(generator.to_string()?, "shared", "asserted by nixos type system");
+				let Some(shared_def) = shared_def else {
+					bail!("secret {secret} is defined on host {host} as shared, but there is no shared secret with same name defined at fleetConfiguration.secrets.{secret}.generator")
+				};
+				let expected_owners = shared_def.expected_owners()?;
+
+				ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
 
-			info!("get parts");
-			let mut parts: BTreeMap<String, GeneratorPart> = nix_go_json!(default_generator.parts);
-			info!("got parts: {parts:?}");
+				(true, shared_def.generator()?, expected_owners)
+			} else {
+				if shared_def.is_some() {
+					bail!("hosts can only have their own generators for non-shared secrets, either set host secret generator to \"shared\", or remove shared secret generator at fleetConfiguration.secrets.{secret}.generator")
+				}
 
-			let Some(existing) = config
-				.host_secret(&host, &secret) else {
-				bail!("missing secret {secret} for host {host}; secret needs regeneration")
+				(false, generator.clone(), BTreeSet::from_iter([host.clone()]))
 			};
 
-			info!("got existing: {existing:?}");
+			let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;
+			let expectations = Expectations {
+				parts: nix_go_json!(default_generator_drv.parts),
+				generation_data: nix_go_json!(default_generator_drv.generationData),
+				owners: expected_owners,
+			};
+
+			let reason: RegenerationReason = 'regenerate: {
+				let Some(existing) = config
+					.host_secret(&host, &secret) else {
+					break 'regenerate RegenerationReason::Missing;
+				};
+				if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {
+					break 'regenerate reason;
+				}
 
-			let mut out = HashMap::new();
+				let mut parts = expectations.parts.clone();
 
-			for (part_name, part) in &existing.secret.parts {
-				let Some(definition) = parts.remove(part_name) else {
-					warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
-					continue;
-				};
-				if definition.encrypted != part.raw.encrypted {
-					bail!("secret {secret} part {part_name} is supposed to be {}, but it is {}; secret needs regeneration", if definition.encrypted {"encrypted"} else {"unencrypted"}, if part.raw.encrypted {"encrypted"} else {"unencrypted"});
+				let mut out = HashMap::new();
+				for (part_name, part) in &existing.secret.parts {
+					let Some(definition) = parts.remove(part_name) else {
+						warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
+						continue;
+					};
+					assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");
+					out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
 				}
-				out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
-			}
-			if !parts.is_empty(){
-				let defs = parts.keys().collect_vec();
-				bail!("secret parts are defined, but not stored: {defs:?}, secret needs regeneration")
-			}
+				assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");
 
-			Ok(Value::new_attrs(out))
+				return Ok(Value::new_attrs(out))
+			};
+
+			todo!()
+
+
 		},
 	)
 	.register();
modifiedcrates/fleet-base/src/secret.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,16 +1,8 @@
-use std::collections::BTreeSet;
+use std::collections::{BTreeMap, BTreeSet};
 
 use chrono::{DateTime, Utc};
 
-use crate::fleetdata::FleetSecretData;
-
-#[derive(Debug)]
-pub struct Expectations {
-	pub owners: BTreeSet<String>,
-	pub generation_data: serde_json::Value,
-	pub public_parts: BTreeSet<String>,
-	pub private_parts: BTreeSet<String>,
-}
+use crate::fleetdata::{Expectations, FleetSecretData, FleetSecretDistribution, GeneratorPart};
 
 #[derive(thiserror::Error, Debug)]
 pub enum RegenerationReason {
@@ -34,56 +26,62 @@
 	ExpectedPublic(String),
 	#[error("secret is expired at {0}")]
 	Expired(DateTime<Utc>),
+
+	#[error("secret is not generated for this host")]
+	Missing,
 }
 
 pub fn secret_needs_regeneration(
-	secret: &FleetSecretData,
-	owners: &BTreeSet<String>,
+	secret: &FleetSecretDistribution,
 	expectations: &Expectations,
 ) -> Option<RegenerationReason> {
-	if !owners.is_empty() {
-		let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();
-		if !added.is_empty() {
-			return Some(RegenerationReason::OwnersAdded(added));
-		}
+	let added: BTreeSet<String> = expectations
+		.owners
+		.difference(&secret.owners)
+		.cloned()
+		.collect();
+	if !added.is_empty() {
+		return Some(RegenerationReason::OwnersAdded(added));
+	}
 
-		let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();
-		if !removed.is_empty() {
-			return Some(RegenerationReason::OwnersRemoved(removed));
-		}
+	let removed: BTreeSet<String> = secret
+		.owners
+		.difference(&expectations.owners)
+		.cloned()
+		.collect();
+	if !removed.is_empty() {
+		return Some(RegenerationReason::OwnersRemoved(removed));
 	}
 
-	if secret.generation_data != expectations.generation_data {
+	if secret.secret.generation_data != expectations.generation_data {
 		return Some(RegenerationReason::GenerationData {
 			expected: expectations.generation_data.clone(),
-			found: secret.generation_data.clone(),
+			found: secret.secret.generation_data.clone(),
 		});
 	}
 
-	if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {
-		let expected: BTreeSet<String> = expectations
-			.public_parts
-			.union(&expectations.private_parts)
-			.cloned()
-			.collect();
-		let found: BTreeSet<String> = secret.parts.keys().cloned().collect();
+	let expected: BTreeSet<String> = expectations.parts.keys().cloned().collect();
+	let found: BTreeSet<String> = secret.secret.parts.keys().cloned().collect();
 
-		if found != expected {
-			return Some(RegenerationReason::PartList { expected, found });
-		}
+	if found != expected {
+		return Some(RegenerationReason::PartList { expected, found });
+	}
 
-		for (name, value) in secret.parts.iter() {
-			if value.raw.encrypted {
-				if !expectations.private_parts.contains(name) {
-					return Some(RegenerationReason::ExpectedPrivate(name.clone()));
-				}
-			} else if !expectations.public_parts.contains(name) {
-				return Some(RegenerationReason::ExpectedPublic(name.clone()));
+	for (name, value) in secret.secret.parts.iter() {
+		let expectation = expectations
+			.parts
+			.get(name)
+			.expect("found == expected checked");
+		if value.raw.encrypted {
+			if !expectation.encrypted {
+				return Some(RegenerationReason::ExpectedPrivate(name.clone()));
 			}
+		} else if expectation.encrypted {
+			return Some(RegenerationReason::ExpectedPublic(name.clone()));
 		}
 	}
 
-	if let Some(expiration) = secret.expires_at {
+	if let Some(expiration) = secret.secret.expires_at {
 		// TODO: Leeway?
 		if expiration < Utc::now() {
 			return Some(RegenerationReason::Expired(expiration));
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -731,6 +731,10 @@
 	}
 
 	pub fn has_field(&self, field: &str) -> Result<bool> {
+		if !matches!(self.type_of(), NixType::Attrs) {
+			bail!("invalid type: expected attrs");
+		}
+
 		let f = init_field_name(field);
 		with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })
 	}
@@ -881,6 +885,12 @@
 	pub fn is_null(&self) -> bool {
 		matches!(self.type_of(), NixType::Null)
 	}
+	pub fn is_string(&self) -> bool {
+		matches!(self.type_of(), NixType::String)
+	}
+	pub fn is_attrs(&self) -> bool {
+		matches!(self.type_of(), NixType::Attrs)
+	}
 }
 
 impl From<String> for Value {
modifiedcrates/nix-eval/src/util.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -1,23 +1,15 @@
 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<()> {
+pub fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
 	let before_errors = Instant::now();
 	let errors: Vec<String> = nix_go_json!(val.errors);
-	// let assertions: Vec<Assertion> = nix_go_json!(val.assertions);
-	debug!("errors evaluation took {:?} {errors:?} ", before_errors.elapsed());
+	debug!("errors evaluation took {:?}", before_errors.elapsed());
 	if !errors.is_empty() {
 		bail!(
 			"failed with error{}{}",
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -160,7 +160,7 @@
           mkImpureSecretGenerator,
         }:
         mkImpureSecretGenerator {
-          # TODO: Escape prompt?
+          # TODO: Escape prompt/part (preferrably just use env) to prevent shell injection
           script = ''
             ${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}
           '';
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -89,6 +89,7 @@
                 # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
                 # (Some secrets-encryption-in-git/managed PKI solution is expected)
                 impureOn ? null,
+                generationData ? null,
                 parts,
               }:
               (prev.writeShellScript "impureGenerator.sh" ''
@@ -117,7 +118,7 @@
               '').overrideAttrs
                 (old: {
                   passthru = {
-                    inherit impureOn parts;
+                    inherit impureOn parts generationData;
                     generatorKind = "impure";
                   };
                 });