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