1use std::ffi::{OsStr, OsString};
11use std::fmt;
12
13const REGISTERED_NAMES: &str = include_str!("environment_registry_names.txt");
14const UNKNOWN_CODE: &str = "HARN-ENV-001";
15const INVALID_VALUE_CODE: &str = "HARN-ENV-002";
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum EnvironmentConsumer {
20 Runtime,
21 Cli,
22 BuildTooling,
23 TestHarness,
24 EmbedderExtension,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum EnvironmentValueShape {
33 OwnerValidated,
34 Boolean,
35 UnsignedInteger,
36 NonNegativeNumber,
37 UnitInterval,
38 Enumerated(&'static [&'static str]),
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum EnvironmentSensitivity {
44 Public,
45 Credential,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct EnvironmentVariableSpec {
50 pub name: String,
51 pub consumer: EnvironmentConsumer,
52 pub value_shape: EnvironmentValueShape,
53 pub sensitivity: EnvironmentSensitivity,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum EnvironmentDiagnosticKind {
58 UnknownName { suggestion: Option<String> },
59 InvalidValue { expected: EnvironmentValueShape },
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct EnvironmentDiagnostic {
66 pub code: &'static str,
67 pub key: String,
68 pub kind: EnvironmentDiagnosticKind,
69}
70
71impl fmt::Display for EnvironmentDiagnostic {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match &self.kind {
74 EnvironmentDiagnosticKind::UnknownName { suggestion } => {
75 write!(
76 formatter,
77 "{}: unknown Harn environment variable `{}`.",
78 self.code, self.key
79 )?;
80 if let Some(suggestion) = suggestion {
81 write!(formatter, " Did you mean `{suggestion}`?")?;
82 }
83 formatter.write_str(" Use `HARN_EXT_<NAME>` for settings owned by a calling tool.")
84 }
85 EnvironmentDiagnosticKind::InvalidValue { expected } => write!(
86 formatter,
87 "{}: environment variable `{}` must be {}",
88 self.code,
89 self.key,
90 expected.description()
91 ),
92 }
93 }
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct EnvironmentValidationError {
98 diagnostics: Vec<EnvironmentDiagnostic>,
99}
100
101impl EnvironmentValidationError {
102 pub fn diagnostics(&self) -> &[EnvironmentDiagnostic] {
103 &self.diagnostics
104 }
105}
106
107impl fmt::Display for EnvironmentValidationError {
108 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109 for (index, diagnostic) in self.diagnostics.iter().enumerate() {
110 if index > 0 {
111 formatter.write_str("\n")?;
112 }
113 diagnostic.fmt(formatter)?;
114 }
115 Ok(())
116 }
117}
118
119impl std::error::Error for EnvironmentValidationError {}
120
121impl EnvironmentValueShape {
122 fn description(self) -> String {
123 match self {
124 Self::OwnerValidated => "valid for its owning subsystem".to_string(),
125 Self::Boolean => {
126 "a boolean (`true`, `false`, `yes`, `no`, `on`, `off`, `1`, or `0`)".to_string()
127 }
128 Self::UnsignedInteger => "an unsigned integer".to_string(),
129 Self::NonNegativeNumber => "a non-negative number".to_string(),
130 Self::UnitInterval => "a number between 0 and 1".to_string(),
131 Self::Enumerated(values) => format!(
132 "one of {}",
133 values
134 .iter()
135 .map(|value| format!("`{value}`"))
136 .collect::<Vec<_>>()
137 .join(", ")
138 ),
139 }
140 }
141
142 fn accepts(self, value: &OsStr) -> bool {
143 let Some(value) = value.to_str() else {
144 return matches!(self, Self::OwnerValidated);
145 };
146 match self {
147 Self::OwnerValidated => true,
148 Self::Boolean => matches!(
149 value.trim().to_ascii_lowercase().as_str(),
150 "1" | "0" | "true" | "false" | "yes" | "no" | "on" | "off"
151 ),
152 Self::UnsignedInteger => value.trim().parse::<u64>().is_ok(),
153 Self::NonNegativeNumber => value
154 .trim()
155 .parse::<f64>()
156 .is_ok_and(|parsed| parsed.is_finite() && parsed >= 0.0),
157 Self::UnitInterval => value
158 .trim()
159 .parse::<f64>()
160 .is_ok_and(|parsed| parsed.is_finite() && (0.0..=1.0).contains(&parsed)),
161 Self::Enumerated(values) => {
162 let value = value.trim().to_ascii_lowercase();
163 values.contains(&value.as_str())
164 }
165 }
166 }
167}
168
169pub fn variable_spec(name: &str) -> Option<EnvironmentVariableSpec> {
171 if registered_names().binary_search(&name).is_ok() {
172 return Some(spec_for_registered_name(name));
173 }
174 if is_extension_name(name) {
175 return Some(EnvironmentVariableSpec {
176 name: name.to_string(),
177 consumer: EnvironmentConsumer::EmbedderExtension,
178 value_shape: EnvironmentValueShape::OwnerValidated,
179 sensitivity: sensitivity_for(name),
180 });
181 }
182 if is_structured_runtime_name(name) {
183 return Some(spec_for_registered_name(name));
184 }
185 None
186}
187
188pub fn validate_startup_environment() -> Result<(), EnvironmentValidationError> {
191 validate_environment(std::env::vars_os())
192}
193
194pub fn validate_environment<I, K, V>(vars: I) -> Result<(), EnvironmentValidationError>
197where
198 I: IntoIterator<Item = (K, V)>,
199 K: Into<OsString>,
200 V: Into<OsString>,
201{
202 let mut diagnostics = Vec::new();
203 for (key, value) in vars {
204 let key = key.into();
205 let Some(key) = key.to_str() else {
206 continue;
207 };
208 if !key.starts_with("HARN_") {
209 continue;
210 }
211 let Some(spec) = variable_spec(key) else {
212 diagnostics.push(EnvironmentDiagnostic {
213 code: UNKNOWN_CODE,
214 key: key.to_string(),
215 kind: EnvironmentDiagnosticKind::UnknownName {
216 suggestion: nearest_registered_name(key),
217 },
218 });
219 continue;
220 };
221 let value = value.into();
222 if !spec.value_shape.accepts(&value) {
223 diagnostics.push(EnvironmentDiagnostic {
224 code: INVALID_VALUE_CODE,
225 key: key.to_string(),
226 kind: EnvironmentDiagnosticKind::InvalidValue {
227 expected: spec.value_shape,
228 },
229 });
230 }
231 }
232 diagnostics.sort_by(|left, right| left.key.cmp(&right.key));
233 if diagnostics.is_empty() {
234 Ok(())
235 } else {
236 Err(EnvironmentValidationError { diagnostics })
237 }
238}
239
240fn registered_names() -> &'static [&'static str] {
241 static NAMES: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
242 NAMES
243 .get_or_init(|| {
244 REGISTERED_NAMES
245 .lines()
246 .filter(|name| !name.is_empty())
247 .collect()
248 })
249 .as_slice()
250}
251
252fn spec_for_registered_name(name: &str) -> EnvironmentVariableSpec {
253 EnvironmentVariableSpec {
254 name: name.to_string(),
255 consumer: consumer_for(name),
256 value_shape: value_shape_for(name),
257 sensitivity: sensitivity_for(name),
258 }
259}
260
261fn consumer_for(name: &str) -> EnvironmentConsumer {
262 if name.starts_with("HARN_TEST_") || name.contains("_TEST_") || name.starts_with("HARN_E2E_") {
263 EnvironmentConsumer::TestHarness
264 } else if [
265 "HARN_BUILD_",
266 "HARN_CARGO_",
267 "HARN_CHECK_",
268 "HARN_CI_",
269 "HARN_CODEGEN_",
270 "HARN_DEV_",
271 "HARN_RELEASE_",
272 ]
273 .iter()
274 .any(|prefix| name.starts_with(prefix))
275 {
276 EnvironmentConsumer::BuildTooling
277 } else if name.starts_with("HARN_CLI_")
278 || name.starts_with("HARN_DOCTOR_")
279 || name.starts_with("HARN_INIT_")
280 {
281 EnvironmentConsumer::Cli
282 } else {
283 EnvironmentConsumer::Runtime
284 }
285}
286
287fn value_shape_for(name: &str) -> EnvironmentValueShape {
288 match name {
289 "HARN_LLM_TIMEOUT"
290 | "HARN_LLM_IDLE_TIMEOUT"
291 | "HARN_LLM_FIRST_TOKEN_TIMEOUT"
292 | "HARN_RETENTION_DAYS"
293 | "HARN_EVENT_LOG_QUEUE_DEPTH" => EnvironmentValueShape::UnsignedInteger,
294 "HARN_OTEL_SAMPLE_RATIO" => EnvironmentValueShape::UnitInterval,
295 "HARN_ALLOW_TOOLCHAIN_MISMATCH"
296 | "HARN_BYTECODE_CACHE"
297 | "HARN_DISPATCH_GENERATION_DEBUG"
298 | "HARN_FLIGHT_RECORDER"
299 | harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV
300 | "HARN_LLM_STREAM"
301 | "HARN_REPLAY_ENABLED"
302 | "HARN_REQUIRE_SIGNED_SKILLS"
303 | "HARN_TRACE"
304 | "HARN_VERBOSE_CONFIG" => EnvironmentValueShape::Boolean,
305 crate::vm::subtask::PLACEMENT_ENV => {
306 EnvironmentValueShape::Enumerated(crate::vm::subtask::PLACEMENT_VALUES)
307 }
308 _ => EnvironmentValueShape::OwnerValidated,
309 }
310}
311
312fn sensitivity_for(name: &str) -> EnvironmentSensitivity {
313 if [
314 "TOKEN",
315 "SECRET",
316 "PASSWORD",
317 "API_KEY",
318 "OAUTH_KEY",
319 "HEADERS",
320 "PRIVATE_KEY",
321 ]
322 .iter()
323 .any(|fragment| name.contains(fragment))
324 {
325 EnvironmentSensitivity::Credential
326 } else {
327 EnvironmentSensitivity::Public
328 }
329}
330
331fn is_extension_name(name: &str) -> bool {
335 name.strip_prefix("HARN_EXT_")
336 .is_some_and(is_upper_identifier)
337}
338
339fn is_structured_runtime_name(name: &str) -> bool {
343 is_secret_name(name)
344 || is_rate_limit_name(name)
345 || is_model_role_name(name)
346 || is_agent_model_option_name(name)
347}
348
349fn is_secret_name(name: &str) -> bool {
350 name.strip_prefix("HARN_SECRET_")
351 .is_some_and(is_upper_identifier)
352}
353
354fn is_rate_limit_name(name: &str) -> bool {
355 let Some(suffix) = name.strip_prefix("HARN_RATE_LIMIT_") else {
356 return false;
357 };
358 let Some((provider, field)) = suffix.rsplit_once('_') else {
359 return false;
360 };
361 is_upper_identifier(provider) && matches!(field, "QUEUE" | "RPM" | "TPM" | "CONCURRENCY")
362}
363
364fn is_model_role_name(name: &str) -> bool {
365 let suffix = name
366 .strip_prefix("HARN_LLM_ROLE_")
367 .or_else(|| name.strip_prefix("HARN_LLM_"));
368 let Some(suffix) = suffix else {
369 return false;
370 };
371 ["_MODEL", "_PROVIDER", "_ROUTE_POLICY"]
372 .iter()
373 .find_map(|ending| suffix.strip_suffix(ending))
374 .is_some_and(is_upper_identifier)
375}
376
377fn is_agent_model_option_name(name: &str) -> bool {
381 const SUFFIXES: &[&str] = &[
382 "_EFFORT",
383 "_MODEL",
384 "_MODEL_ROLE",
385 "_PROVIDER",
386 "_REASONING_TASK",
387 "_TOOL_FORMAT",
388 ];
389 let Some(prefix) = SUFFIXES.iter().find_map(|suffix| name.strip_suffix(suffix)) else {
390 return false;
391 };
392 let Some(prefix) = prefix.strip_prefix("HARN_") else {
393 return false;
394 };
395 let role = prefix
396 .strip_prefix("AGENT_")
397 .or_else(|| prefix.strip_prefix("LLM_"))
398 .unwrap_or(prefix);
399 matches!(prefix, "AGENT" | "LLM") || is_upper_identifier(role)
400}
401
402fn is_upper_identifier(value: &str) -> bool {
403 !value.is_empty()
404 && value
405 .bytes()
406 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
407 && !value.starts_with('_')
408 && !value.ends_with('_')
409}
410
411fn nearest_registered_name(name: &str) -> Option<String> {
412 registered_names()
413 .iter()
414 .copied()
415 .filter(|candidate| !candidate.ends_with('_'))
416 .map(|candidate| (strsim::levenshtein(name, candidate), candidate))
417 .min_by_key(|(distance, candidate)| (*distance, *candidate))
418 .filter(|(distance, _)| *distance <= 3)
419 .map(|(_, candidate)| candidate.to_string())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn typo_is_typed_and_suggests_registered_name() {
428 let error = validate_environment([("HARN_LLM_TIMOUT", "30")]).unwrap_err();
429 assert_eq!(
430 error.diagnostics(),
431 &[EnvironmentDiagnostic {
432 code: UNKNOWN_CODE,
433 key: "HARN_LLM_TIMOUT".to_string(),
434 kind: EnvironmentDiagnosticKind::UnknownName {
435 suggestion: Some("HARN_LLM_TIMEOUT".to_string()),
436 },
437 }]
438 );
439 }
440
441 #[test]
442 fn known_and_structured_extension_names_are_accepted() {
443 validate_environment([
444 ("HARN_LLM_TIMEOUT", "30"),
445 ("HARN_EXT_ACME_MODE", "custom"),
446 ("HARN_LLM_ROLE_REVIEW_MODEL", "reviewer"),
447 ("HARN_SECRET_ACME_TOKEN", "credential"),
448 ])
449 .unwrap();
450 }
451
452 #[test]
453 fn malformed_extension_name_is_not_a_prefix_bypass() {
454 let error = validate_environment([("HARN_EXT_", "anything")]).unwrap_err();
455 assert!(matches!(
456 error.diagnostics()[0].kind,
457 EnvironmentDiagnosticKind::UnknownName { .. }
458 ));
459 }
460
461 #[test]
469 fn dynamic_agent_role_options_follow_a_closed_structural_grammar() {
470 for name in [
471 "HARN_AGENT_MODEL",
472 "HARN_LLM_TOOL_FORMAT",
473 "HARN_AGENT_REVIEW_PROVIDER",
474 "HARN_LLM_PLANNER_REASONING_TASK",
475 "HARN_RELEASE_EFFORT",
476 ] {
477 assert!(variable_spec(name).is_some(), "{name}");
478 }
479 for name in [
480 "HARN_AGENT_REVIEW_UNKNOWN",
481 "HARN_LLM_TIMOUT",
482 "HARN_RELEASE_",
483 ] {
484 assert!(variable_spec(name).is_none(), "{name}");
485 }
486 }
487
488 #[test]
489 fn credential_metadata_covers_non_api_oauth_keys() {
490 assert_eq!(
491 variable_spec("HARN_OAUTH_KEY").unwrap().sensitivity,
492 EnvironmentSensitivity::Credential
493 );
494 }
495
496 #[test]
497 fn invalid_known_value_is_rejected_at_startup() {
498 let error = validate_environment([("HARN_LLM_TIMEOUT", "soon")]).unwrap_err();
499 assert_eq!(
500 error.diagnostics()[0].kind,
501 EnvironmentDiagnosticKind::InvalidValue {
502 expected: EnvironmentValueShape::UnsignedInteger
503 }
504 );
505 }
506
507 #[test]
508 fn subtask_placement_uses_the_runtime_owned_closed_vocabulary() {
509 let spec = variable_spec(crate::vm::subtask::PLACEMENT_ENV).unwrap();
510 assert_eq!(
511 spec.value_shape,
512 EnvironmentValueShape::Enumerated(crate::vm::subtask::PLACEMENT_VALUES)
513 );
514 validate_environment([(crate::vm::subtask::PLACEMENT_ENV, "worker")]).unwrap();
515 validate_environment([(crate::vm::subtask::PLACEMENT_ENV, "CURRENT_THREAD")]).unwrap();
516
517 let error =
518 validate_environment([(crate::vm::subtask::PLACEMENT_ENV, "workers")]).unwrap_err();
519 assert_eq!(
520 error.diagnostics()[0].kind,
521 EnvironmentDiagnosticKind::InvalidValue {
522 expected: EnvironmentValueShape::Enumerated(crate::vm::subtask::PLACEMENT_VALUES)
523 }
524 );
525 assert!(error.to_string().contains("`worker`, `current_thread`"));
526 }
527
528 #[test]
529 fn diagnostics_cannot_render_values_even_for_credentials() {
530 let secret = "must-never-appear";
531 let error = validate_environment([("HARN_CLOUD_API_KEZ", secret)]).unwrap_err();
532 let rendered = error.to_string();
533 assert!(rendered.contains("HARN_CLOUD_API_KEZ"));
534 assert!(!rendered.contains(secret));
535 }
536
537 #[test]
541 fn retired_limit_names_are_unknown_at_startup() {
542 for name in [
543 concat!("HARN", "_BUDGET_USD"),
544 concat!("HARN", "_TOKEN_BUDGET"),
545 concat!("HARN", "_MAX_CONCURRENCY"),
546 concat!("HARN", "_NETWORK_MODE"),
547 concat!("HARN", "_FILESYSTEM_MODE"),
548 concat!("HARN", "_SANDBOX_MODE"),
549 ] {
550 let error = validate_environment([(name, "1")]).expect_err(name);
551 assert!(
552 error.to_string().contains(name),
553 "startup must name {name}, got {error}"
554 );
555 assert!(
556 variable_spec(name).is_none(),
557 "{name} must stay unregistered"
558 );
559 }
560 }
561
562 #[test]
563 fn registry_is_sorted_unique_and_contains_metadata() {
564 let names = registered_names();
565 assert!(
566 names.windows(2).all(|pair| pair[0] < pair[1]),
567 "environment registry must remain sorted and unique"
568 );
569 let timeout = variable_spec("HARN_LLM_TIMEOUT").unwrap();
570 assert_eq!(timeout.consumer, EnvironmentConsumer::Runtime);
571 assert_eq!(timeout.value_shape, EnvironmentValueShape::UnsignedInteger);
572 let token = variable_spec("HARN_PACKAGE_REGISTRY_TOKEN").unwrap();
573 assert_eq!(token.sensitivity, EnvironmentSensitivity::Credential);
574 let host_providers = variable_spec(crate::llm_config::HOST_PROVIDERS_CONFIG_ENV).unwrap();
575 assert_eq!(host_providers.consumer, EnvironmentConsumer::Runtime);
576 assert_eq!(
577 host_providers.value_shape,
578 EnvironmentValueShape::OwnerValidated
579 );
580 }
581
582 #[test]
583 fn embedded_runtime_bootstrap_accepts_registered_process_environment() {
584 crate::initialize_runtime().expect("registered process environment");
585 }
586
587 #[test]
588 fn every_compiled_harn_name_is_registered_or_structurally_owned() {
589 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
590 .ancestors()
591 .nth(2)
592 .expect("harn-vm lives below workspace/crates");
593 let protocol_symbols = protocol_artifact_symbol_names(workspace_root);
594 let crates_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
595 .parent()
596 .expect("harn-vm lives below crates");
597 let mut missing = std::collections::BTreeSet::new();
598 for entry in walkdir::WalkDir::new(crates_dir)
599 .into_iter()
600 .filter_map(Result::ok)
601 .filter(|entry| {
602 entry.file_type().is_file()
603 && entry.path().extension().and_then(OsStr::to_str) == Some("rs")
604 && entry
605 .path()
606 .components()
607 .any(|component| component.as_os_str() == "src")
608 && !is_protocol_artifact_projection(entry.path())
609 && entry.file_name() != "environment_registry.rs"
610 })
611 {
612 let source = std::fs::read_to_string(entry.path()).expect("read Rust source");
613 for token in harn_name_tokens(&source) {
614 if variable_spec(token).is_none()
615 && !protocol_symbols.contains(token)
616 && !matches!(token, "HARN_LLM_" | "HARN_LLM_ROLE_" | "HARN_SECRET_")
617 {
618 missing.insert(format!("{}: {token}", entry.path().display()));
619 }
620 }
621 }
622 assert!(
623 missing.is_empty(),
624 "compiled HARN_* names missing from environment_registry_names.txt:\n{}",
625 missing.into_iter().collect::<Vec<_>>().join("\n")
626 );
627 }
628
629 #[test]
630 fn every_harn_script_owned_name_is_registered_or_structurally_owned() {
631 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
632 .ancestors()
633 .nth(2)
634 .expect("harn-vm lives below workspace/crates");
635 let source_roots = [
636 "benchmarks",
637 "conformance",
638 "crates",
639 "evals",
640 "examples",
641 "experiments",
642 "perf",
643 "personas",
644 "scripts",
645 "tests",
646 ];
647 let protocol_symbols = protocol_artifact_symbol_names(workspace_root);
648 let mut missing = std::collections::BTreeSet::new();
649 for source_root in source_roots {
650 let source_root = workspace_root.join(source_root);
651 if !source_root.exists() {
652 continue;
653 }
654 for entry in walkdir::WalkDir::new(source_root)
655 .into_iter()
656 .filter_entry(|entry| !is_pruned_reference_directory(entry))
657 .filter_map(Result::ok)
658 .filter(|entry| {
659 entry.file_type().is_file()
660 && entry.path().extension().and_then(OsStr::to_str) == Some("harn")
661 })
662 {
663 let source = std::fs::read_to_string(entry.path()).expect("read Harn source");
664 for token in harn_name_tokens(&source) {
665 if variable_spec(token).is_none()
671 && !protocol_symbols.contains(token)
672 && !matches!(
673 token,
674 "HARN_AGENT"
675 | "HARN_AGENT_"
676 | "HARN_BOOTSTRAP_"
677 | "HARN_LLM"
678 | "HARN_LLM_"
679 | "HARN_PLANNER"
680 | "HARN_RELEASE"
681 )
682 {
683 missing.insert(format!("{}: {token}", entry.path().display()));
684 }
685 }
686 }
687 }
688 assert!(
689 missing.is_empty(),
690 "Harn-script HARN_* names missing from environment_registry_names.txt:\n{}",
691 missing.into_iter().collect::<Vec<_>>().join("\n")
692 );
693 }
694
695 fn workflow_assigned_names(source: &str) -> std::collections::BTreeSet<String> {
703 let mut names = std::collections::BTreeSet::new();
704 for line in source.lines() {
705 let trimmed = line.trim_start();
706 let candidate = trimmed.strip_prefix("export ").unwrap_or(trimmed);
707 let Some(rest) = candidate.strip_prefix("HARN_") else {
708 continue;
709 };
710 let mut name = String::from("HARN_");
711 let mut terminator = None;
712 for character in rest.chars() {
713 if character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_'
714 {
715 name.push(character);
716 } else {
717 terminator = Some(character);
718 break;
719 }
720 }
721 if name == "HARN_" {
722 continue;
723 }
724 if !matches!(terminator, Some(':') | Some('=')) {
728 continue;
729 }
730 names.insert(name);
731 }
732 names
733 }
734
735 #[test]
736 fn workflow_name_scan_reads_assignments_and_ignores_references() {
737 let source = concat!(
738 " env:\n",
739 " HARN_BUMP_BRANCH: main\n",
740 " run: |\n",
741 " export HARN_BUMP_REFRESH=\"$HARN_BUMP_REFRESH_COMMAND\"\n",
742 " echo \"$HARN_BUMP_TOKEN\"\n",
743 " # HARN_BUMP_NOT_A_KEY: prose\n",
744 );
745 let names = workflow_assigned_names(source);
746 assert!(names.contains("HARN_BUMP_BRANCH"), "reads a YAML env key");
747 assert!(names.contains("HARN_BUMP_REFRESH"), "reads a shell export");
748 assert!(
749 !names.contains("HARN_BUMP_REFRESH_COMMAND"),
750 "a reference on a right-hand side is not an assignment"
751 );
752 assert!(
753 !names.contains("HARN_BUMP_TOKEN"),
754 "an echoed reference is not an assignment"
755 );
756 assert!(
757 !names.contains("HARN_BUMP_NOT_A_KEY"),
758 "a comment is not an assignment"
759 );
760 }
761
762 #[test]
767 fn every_workflow_assigned_name_is_registered() {
768 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
769 .ancestors()
770 .nth(2)
771 .expect("harn-vm lives below workspace/crates");
772 let workflows = workspace_root.join(".github/workflows");
773 assert!(
774 workflows.is_dir(),
775 "no workflow directory at {}; this gate would pass by seeing nothing",
776 workflows.display()
777 );
778 let mut scanned = 0usize;
779 let mut missing = std::collections::BTreeSet::new();
780 for entry in walkdir::WalkDir::new(&workflows)
781 .into_iter()
782 .filter_map(Result::ok)
783 .filter(|entry| {
784 entry.file_type().is_file()
785 && matches!(
786 entry.path().extension().and_then(OsStr::to_str),
787 Some("yml") | Some("yaml")
788 )
789 })
790 {
791 let source = std::fs::read_to_string(entry.path()).expect("read workflow");
792 scanned += 1;
793 for name in workflow_assigned_names(&source) {
794 if variable_spec(&name).is_none() {
795 missing.insert(format!("{}: {name}", entry.path().display()));
796 }
797 }
798 }
799 assert!(
800 scanned > 0,
801 "scanned no workflow files; the gate cannot see anything"
802 );
803 assert!(
804 missing.is_empty(),
805 "workflow-assigned HARN_* names missing from \
806 environment_registry_names.txt:\n{}",
807 missing.into_iter().collect::<Vec<_>>().join("\n")
808 );
809 }
810
811 const UNREAD_NAME_ALLOWLIST: &[(&str, &str)] = &[];
817
818 fn is_pruned_reference_directory(entry: &walkdir::DirEntry) -> bool {
823 if !entry.file_type().is_dir() {
824 return false;
825 }
826 let Some(name) = entry.file_name().to_str() else {
827 return true;
828 };
829 name.starts_with(".target")
830 || matches!(
831 name,
832 ".build"
833 | ".burin"
834 | ".git"
835 | ".harn"
836 | ".harn-runs"
837 | ".harn-toolchain-cache"
838 | ".venv"
839 | ".worktrees"
840 | "__pycache__"
841 | "changelog"
842 | "dist"
843 | "node_modules"
844 | "pkg"
845 | "target"
846 )
847 }
848
849 #[test]
850 fn generated_harn_state_is_not_an_environment_contract_owner() {
851 let root = tempfile::tempdir().expect("temporary workspace");
852 let state = root.path().join(".harn");
853 std::fs::create_dir(&state).expect("create generated Harn state");
854 let entry = walkdir::WalkDir::new(root.path())
855 .into_iter()
856 .filter_map(Result::ok)
857 .find(|entry| entry.path() == state)
858 .expect("walk generated Harn state");
859
860 assert!(is_pruned_reference_directory(&entry));
861 }
862
863 fn is_protocol_artifact_projection(path: &std::path::Path) -> bool {
867 path.components().any(|component| {
868 matches!(
869 component.as_os_str().to_str(),
870 Some("dump_protocol_artifacts" | "protocol-artifacts")
871 )
872 })
873 }
874
875 fn is_reference_source(path: &std::path::Path) -> bool {
879 if path.extension().and_then(OsStr::to_str) == Some("md")
880 || is_protocol_artifact_projection(path)
881 {
882 return false;
883 }
884 !matches!(
885 path.file_name().and_then(OsStr::to_str),
886 Some("environment_registry.rs" | "environment_registry_names.txt")
887 )
888 }
889
890 fn protocol_artifact_symbol_names(
894 workspace_root: &std::path::Path,
895 ) -> std::collections::BTreeSet<String> {
896 let path = workspace_root.join("spec/protocol-artifacts/harn-protocol.rs");
897 let source = std::fs::read_to_string(&path)
898 .unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
899 bounded_harn_tokens(&source).map(str::to_string).collect()
900 }
901
902 #[test]
903 fn protocol_artifact_names_are_not_environment_references() {
904 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
905 .ancestors()
906 .nth(2)
907 .expect("harn-vm lives below workspace/crates");
908 let protocol_symbols = protocol_artifact_symbol_names(workspace_root);
909 let generator = std::path::Path::new(
910 "crates/harn-cli/src/commands/dump_protocol_artifacts/typescript.rs",
911 );
912 let projection = std::path::Path::new("spec/protocol-artifacts/harn-protocol.ts");
913 let runtime = std::path::Path::new("crates/harn-vm/src/llm/call.rs");
914
915 assert!(is_protocol_artifact_projection(generator));
916 assert!(is_protocol_artifact_projection(projection));
917 assert!(!is_reference_source(generator));
918 assert!(!is_reference_source(projection));
919 assert!(is_reference_source(runtime));
920 assert!(variable_spec("HARN_AGENT_EVENT_KINDS").is_none());
921 assert!(variable_spec("HARN_WORKER_STATUSES").is_none());
922 assert!(protocol_symbols.contains("HARN_AGENT_EVENT_KINDS"));
923 assert!(!protocol_symbols.contains("HARN_ACP_VERBOSE"));
924 }
925
926 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
927 enum EnvironmentReferenceSyntax {
928 StringLiteral,
929 ShellLike,
930 }
931
932 fn environment_reference_syntax(path: &std::path::Path) -> EnvironmentReferenceSyntax {
933 let file_name = path.file_name().and_then(OsStr::to_str).unwrap_or_default();
934 let extension = path.extension().and_then(OsStr::to_str).unwrap_or_default();
935 if matches!(file_name, "Makefile" | "GNUmakefile")
936 || file_name.starts_with(".env")
937 || matches!(
938 extension,
939 "bash" | "bat" | "cmd" | "fish" | "mk" | "ps1" | "sh" | "yaml" | "yml" | "zsh"
940 )
941 {
942 EnvironmentReferenceSyntax::ShellLike
943 } else {
944 EnvironmentReferenceSyntax::StringLiteral
945 }
946 }
947
948 fn environment_reference_tokens<'a>(path: &std::path::Path, source: &'a str) -> Vec<&'a str> {
949 match environment_reference_syntax(path) {
950 EnvironmentReferenceSyntax::StringLiteral => harn_name_tokens(source).collect(),
951 EnvironmentReferenceSyntax::ShellLike => bounded_harn_tokens(source).collect(),
952 }
953 }
954
955 #[expect(
959 clippy::string_slice,
960 reason = "start/end bound an ASCII HARN_* token found by match_indices"
961 )]
962 fn bounded_harn_tokens(source: &str) -> impl Iterator<Item = &str> {
963 let bytes = source.as_bytes();
964 source.match_indices("HARN_").filter_map(move |(start, _)| {
965 if start > 0 {
966 let previous = bytes[start - 1];
967 if previous.is_ascii_alphanumeric() || previous == b'_' {
968 return None;
969 }
970 }
971 let mut end = start + "HARN_".len();
972 while end < bytes.len()
973 && (bytes[end].is_ascii_uppercase()
974 || bytes[end].is_ascii_digit()
975 || bytes[end] == b'_')
976 {
977 end += 1;
978 }
979 Some(&source[start..end])
980 })
981 }
982
983 #[test]
988 fn every_registered_name_has_a_non_projection_source_reference() {
989 let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
990 .ancestors()
991 .nth(2)
992 .expect("harn-vm lives below workspace/crates");
993 let mut referenced = std::collections::BTreeSet::new();
994 let protocol_symbols = protocol_artifact_symbol_names(workspace_root);
995 for entry in walkdir::WalkDir::new(workspace_root)
996 .into_iter()
997 .filter_entry(|entry| !is_pruned_reference_directory(entry))
998 .filter_map(Result::ok)
999 .filter(|entry| entry.file_type().is_file() && is_reference_source(entry.path()))
1000 {
1001 let Ok(source) = std::fs::read_to_string(entry.path()) else {
1002 continue;
1003 };
1004 for token in environment_reference_tokens(entry.path(), &source) {
1005 if !protocol_symbols.contains(token) {
1006 referenced.insert(token.to_string());
1007 }
1008 }
1009 }
1010
1011 let allowed: std::collections::BTreeSet<&str> = UNREAD_NAME_ALLOWLIST
1012 .iter()
1013 .map(|(name, _)| *name)
1014 .collect();
1015 let unregistered_allowlist = allowed
1016 .iter()
1017 .copied()
1018 .filter(|name| registered_names().binary_search(name).is_err())
1019 .collect::<Vec<_>>();
1020 assert!(
1021 unregistered_allowlist.is_empty(),
1022 "allowlisted names are not in environment_registry_names.txt:\n{}",
1023 unregistered_allowlist.join("\n")
1024 );
1025 let owned_allowlist = allowed
1026 .iter()
1027 .copied()
1028 .filter(|name| referenced.contains(*name))
1029 .collect::<Vec<_>>();
1030 assert!(
1031 owned_allowlist.is_empty(),
1032 "allowlisted names now have source owners; drop them from UNREAD_NAME_ALLOWLIST:\n{}",
1033 owned_allowlist.join("\n")
1034 );
1035
1036 let unread = registered_names()
1037 .iter()
1038 .copied()
1039 .filter(|name| !referenced.contains(*name) && !allowed.contains(name))
1040 .collect::<Vec<_>>();
1041 assert!(
1042 unread.is_empty(),
1043 "registered names have no source owner; delete them from \
1044 environment_registry_names.txt or allowlist them with a reason:\n{}",
1045 unread.join("\n")
1046 );
1047 }
1048
1049 #[test]
1050 fn environment_reference_scan_dispatches_by_source_syntax() {
1051 let compiled = concat!(
1052 "const HARN_AGENT_EVENT_KINDS: &[&str] = &[];\n",
1053 "const ENV: &str = \"HARN_REAL_ENVIRONMENT_KNOB\";\n",
1054 );
1055 assert_eq!(
1056 environment_reference_tokens(std::path::Path::new("runtime.rs"), compiled),
1057 vec!["HARN_REAL_ENVIRONMENT_KNOB"]
1058 );
1059 assert_eq!(
1060 environment_reference_tokens(
1061 std::path::Path::new("bench.sh"),
1062 "cache=${HARN_BENCH_CACHE_DIR:-target}\n",
1063 ),
1064 vec!["HARN_BENCH_CACHE_DIR"]
1065 );
1066 assert_eq!(
1067 environment_reference_tokens(
1068 std::path::Path::new("Makefile"),
1069 "HARN_BIN_ASSIGN = harn_bin\n",
1070 ),
1071 vec!["HARN_BIN_ASSIGN"]
1072 );
1073 }
1074
1075 #[expect(
1076 clippy::string_slice,
1077 reason = "start/end bound an ASCII HARN_* token found by match_indices"
1078 )]
1079 fn harn_name_tokens(source: &str) -> impl Iterator<Item = &str> {
1080 source.match_indices("\"HARN_").filter_map(|(quote, _)| {
1081 let start = quote + 1;
1082 let bytes = source.as_bytes();
1083 let mut end = start + "HARN_".len();
1084 while end < bytes.len()
1085 && (bytes[end].is_ascii_uppercase()
1086 || bytes[end].is_ascii_digit()
1087 || bytes[end] == b'_')
1088 {
1089 end += 1;
1090 }
1091 (end > start + "HARN_".len()).then(|| &source[start..end])
1092 })
1093 }
1094}