26inline std::string safe_getenv(
const char* name) {
30 if (_dupenv_s(&buf, &sz, name) == 0 && buf !=
nullptr) {
31 std::string result(buf);
37 const char* val = std::getenv(name);
38 return val ? val :
"";
42inline std::optional<std::filesystem::path> canonical_boundary_path(
const std::string& path) {
44 auto absolute = std::filesystem::absolute(std::filesystem::path(path), ec);
45 if (ec)
return std::nullopt;
46 auto canonical = std::filesystem::weakly_canonical(absolute, ec);
47 if (ec)
return std::nullopt;
48 return canonical.lexically_normal();
51inline std::optional<std::string> resolve_within(
const std::string& root,
52 const std::string& candidate) {
53 if (root.empty() || candidate.empty())
return std::nullopt;
54 const auto normalized_root = canonical_boundary_path(root);
55 const auto normalized_candidate = canonical_boundary_path(candidate);
56 if (!normalized_root || !normalized_candidate)
return std::nullopt;
58 auto root_it = normalized_root->begin();
59 auto candidate_it = normalized_candidate->begin();
60 for (; root_it != normalized_root->end(); ++root_it, ++candidate_it) {
61 if (candidate_it == normalized_candidate->end() || *root_it != *candidate_it) {
65 return normalized_candidate->string();
76 std::string instance_id;
77 std::string install_root;
78 std::string record_path;
79 std::string metadata_json;
111 static std::unique_ptr<NahHost>
create(
const std::string& root_path =
"");
127 static std::unique_ptr<NahHost> discover(
const std::vector<std::string>& search_paths);
136 static bool isValidRoot(
const std::string& path);
141 const std::string& root()
const {
return root_; }
146 std::vector<AppInfo> listApplications()
const;
154 std::optional<AppInfo> findApplication(
const std::string&
id,
155 const std::string& version =
"")
const;
160 std::optional<nah::core::HostEnvironment> getHostEnvironment()
const;
169 nah::core::CompositionResult getLaunchContract(
170 const std::string& app_id,
171 const std::string& version =
"",
172 bool enable_trace =
false)
const;
181 nah::core::CompositionResult getLaunchContract(
182 const std::string& app_id,
183 const std::string& version,
184 const nah::core::CompositionOptions& options)
const;
193 int executeApplication(
194 const std::string& app_id,
195 const std::string& version =
"",
196 const std::vector<std::string>& args = {})
const;
205 const nah::core::LaunchContract& contract,
206 const std::vector<std::string>& args = {})
const;
211 bool isApplicationInstalled(
const std::string& app_id,
212 const std::string& version =
"")
const;
217 nah::core::RuntimeInventory getInventory()
const;
223 std::string validateRoot()
const;
226 explicit NahHost(std::string root) : root_(std::move(root)) {}
229 std::optional<nah::core::InstallRecord> loadInstallRecord(
const std::string& path)
const;
232 std::optional<nah::core::AppDeclaration> loadAppManifest(
const std::string& app_dir)
const;
234 std::string extractMetadataJson(
const std::string& app_dir)
const;
243inline std::unique_ptr<NahHost> NahHost::create(
const std::string& root_path) {
244 std::string resolved_root = root_path;
246 if (resolved_root.empty()) {
247 std::string env_root = detail::safe_getenv(
"NAH_ROOT");
248 if (!env_root.empty()) {
249 resolved_root = env_root;
251 std::string home = detail::safe_getenv(
"HOME");
252 if (home.empty()) home = detail::safe_getenv(
"USERPROFILE");
253 resolved_root = home.empty() ?
".nah" : home +
"/.nah";
257 return std::unique_ptr<NahHost>(
new NahHost(resolved_root));
260inline std::vector<AppInfo> NahHost::listApplications()
const {
261 std::vector<AppInfo> apps;
262 std::string apps_dir = root_ +
"/registry/apps";
264 if (!nah::fs::exists(apps_dir)) {
268 auto files = nah::fs::list_directory(apps_dir);
269 for (
const auto& entry : files) {
270 if (entry.size() > 5 && entry.substr(entry.size() - 5) ==
".json") {
271 auto record = loadInstallRecord(entry);
274 info.id = record->app.id;
275 info.version = record->app.version;
276 info.instance_id = record->install.instance_id;
277 info.install_root = record->paths.install_root;
278 info.record_path = entry;
279 info.metadata_json = extractMetadataJson(record->paths.install_root);
280 apps.push_back(info);
288inline std::optional<AppInfo> NahHost::findApplication(
const std::string&
id,
289 const std::string& version)
const {
290 auto apps = listApplications();
292 std::vector<AppInfo> matches;
293 for (
const auto& app : apps) {
295 if (version.empty() || app.version == version) {
296 matches.push_back(app);
301 if (matches.empty()) {
306 if (matches.size() > 1 && version.empty()) {
307 std::sort(matches.begin(), matches.end(), [](
const AppInfo& a,
const AppInfo& b) {
308 auto va = nah::semver::parse_version(a.version);
309 auto vb = nah::semver::parse_version(b.version);
314 return a.version > b.version;
320inline std::optional<nah::core::HostEnvironment> NahHost::getHostEnvironment()
const {
321 std::string host_json_path = root_ +
"/host/host.json";
322 auto content = nah::fs::read_file(host_json_path);
325 return nah::core::HostEnvironment{};
328 auto result = nah::json::parse_host_environment(*content, host_json_path);
336inline nah::core::CompositionResult NahHost::getLaunchContract(
337 const std::string& app_id,
338 const std::string& version,
339 bool enable_trace)
const {
340 nah::core::CompositionOptions opts;
341 opts.enable_trace = enable_trace;
342 return getLaunchContract(app_id, version, opts);
345inline nah::core::CompositionResult NahHost::getLaunchContract(
346 const std::string& app_id,
347 const std::string& version,
348 const nah::core::CompositionOptions& options)
const {
351 auto app_info = findApplication(app_id, version);
353 nah::core::CompositionResult result;
355 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
356 result.critical_error_context =
"Application not found: " + app_id;
361 auto record = loadInstallRecord(app_info->record_path);
363 nah::core::CompositionResult result;
365 result.critical_error = nah::core::CriticalError::INSTALL_RECORD_INVALID;
366 result.critical_error_context =
"Failed to load install record";
371 auto app_decl = loadAppManifest(app_info->install_root);
373 nah::core::CompositionResult result;
375 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
376 result.critical_error_context =
"Failed to load app manifest";
381 auto host_env = getHostEnvironment();
383 nah::core::CompositionResult result;
384 result.critical_error = nah::core::CriticalError::HOST_CONFIG_INVALID;
385 result.critical_error_context =
"Failed to load host configuration";
390 auto inventory = getInventory();
392 auto result = nah::core::nah_compose(*app_decl, *host_env, *record, inventory, options);
393 if (!result.ok)
return result;
395 const std::string binary_boundary = result.contract.nak.id.empty()
396 ? result.contract.app.root : result.contract.nak.root;
397 const auto binary = detail::resolve_within(binary_boundary, result.contract.execution.binary);
398 if (!binary || !nah::fs::is_file(*binary)) {
400 result.critical_error = nah::core::CriticalError::ENTRYPOINT_NOT_FOUND;
401 result.critical_error_context =
"Execution binary not found: " + result.contract.execution.binary;
404 result.contract.execution.binary = *binary;
406 const auto cwd_in_app = detail::resolve_within(result.contract.app.root, result.contract.execution.cwd);
407 std::optional<std::string> cwd_in_nak;
408 if (!result.contract.nak.root.empty()) {
409 cwd_in_nak = detail::resolve_within(result.contract.nak.root, result.contract.execution.cwd);
411 const auto cwd = cwd_in_app ? cwd_in_app : cwd_in_nak;
412 if (!cwd || !nah::fs::is_directory(*cwd)) {
414 result.critical_error = nah::core::CriticalError::PATH_TRAVERSAL;
415 result.critical_error_context =
"Working directory is outside the installed app and runtime";
418 result.contract.execution.cwd = *cwd;
422inline int NahHost::executeApplication(
423 const std::string& app_id,
424 const std::string& version,
425 const std::vector<std::string>& args)
const {
427 auto result = getLaunchContract(app_id, version);
428 if (!result.ok)
return 1;
430 return executeContract(result.contract, args);
433inline int NahHost::executeContract(
434 const nah::core::LaunchContract& contract,
435 const std::vector<std::string>& args)
const {
437 auto effective_contract = contract;
438 effective_contract.execution.arguments.insert(
439 effective_contract.execution.arguments.end(), args.begin(), args.end());
440 auto exec_result = nah::exec::execute(effective_contract);
442 if (!exec_result.ok)
return 1;
444 return exec_result.exit_code;
447inline bool NahHost::isApplicationInstalled(
const std::string& app_id,
448 const std::string& version)
const {
449 return findApplication(app_id, version).has_value();
452inline nah::core::RuntimeInventory NahHost::getInventory()
const {
453 nah::core::RuntimeInventory inventory;
454 std::string naks_dir = root_ +
"/registry/naks";
456 if (!nah::fs::exists(naks_dir)) {
460 auto files = nah::fs::list_directory(naks_dir);
461 for (
const auto& entry : files) {
463 if (entry.size() > 5 && entry.substr(entry.size() - 5) ==
".json") {
466 auto runtime_content = nah::fs::read_file(entry);
467 if (runtime_content) {
469 std::string basename = entry;
470 size_t last_slash = entry.rfind(
'/');
471 if (last_slash != std::string::npos) {
472 basename = entry.substr(last_slash + 1);
474 std::string record_ref = basename;
476 auto result = nah::json::parse_runtime_descriptor(*runtime_content, entry);
478 result.value.source_path = entry;
481 if (!result.value.paths.root.empty() && !nah::fs::is_absolute_path(result.value.paths.root)) {
482 result.value.paths.root = nah::fs::absolute_path(nah::fs::join_paths(root_, result.value.paths.root));
484 const auto runtime_root = detail::resolve_within(root_ +
"/naks", result.value.paths.root);
488 result.value.paths.root = *runtime_root;
490 if (result.value.paths.resource_root.empty()) {
491 result.value.paths.resource_root = result.value.paths.root;
492 }
else if (!nah::fs::is_absolute_path(result.value.paths.resource_root)) {
493 result.value.paths.resource_root = nah::fs::absolute_path(
494 nah::fs::join_paths(result.value.paths.root, result.value.paths.resource_root));
496 const auto resource_root = detail::resolve_within(
497 result.value.paths.root, result.value.paths.resource_root);
498 if (!resource_root) {
501 result.value.paths.resource_root = *resource_root;
504 for (
auto& lib_dir : result.value.paths.lib_dirs) {
505 if (!lib_dir.empty() && !nah::fs::is_absolute_path(lib_dir)) {
506 lib_dir = nah::fs::absolute_path(nah::fs::join_paths(result.value.paths.root, lib_dir));
508 const auto resolved = detail::resolve_within(result.value.paths.root, lib_dir);
517 for (
auto& [name, loader] : result.value.loaders) {
518 if (!loader.exec_path.empty() && !nah::fs::is_absolute_path(loader.exec_path)) {
519 loader.exec_path = nah::fs::absolute_path(nah::fs::join_paths(result.value.paths.root, loader.exec_path));
521 const auto resolved = detail::resolve_within(result.value.paths.root, loader.exec_path);
523 loader.exec_path.clear();
525 loader.exec_path = *resolved;
529 inventory.runtimes[record_ref] = result.value;
538inline std::string NahHost::validateRoot()
const {
539 if (!nah::fs::is_directory(root_)) {
540 return "NAH root does not exist: " + root_;
544 const std::vector<std::string> required_dirs = {
545 "/apps",
"/naks",
"/host",
"/registry/apps",
"/registry/naks",
"/staging"
548 for (
const auto& dir : required_dirs) {
549 if (!nah::fs::is_directory(root_ + dir)) {
550 return "Missing required directory: " + root_ + dir;
557inline bool NahHost::isValidRoot(
const std::string& path) {
558 return !path.empty() && NahHost(path).validateRoot().empty();
561inline std::unique_ptr<NahHost> NahHost::discover(
const std::vector<std::string>& search_paths) {
562 for (
const auto& path : search_paths) {
569 if (isValidRoot(path)) {
570 return std::unique_ptr<NahHost>(
new NahHost(path));
578inline std::optional<nah::core::InstallRecord> NahHost::loadInstallRecord(
const std::string& path)
const {
579 auto content = nah::fs::read_file(path);
584 auto result = nah::json::parse_install_record(*content);
587 if (!result.value.paths.install_root.empty() && !nah::fs::is_absolute_path(result.value.paths.install_root)) {
588 result.value.paths.install_root = nah::fs::absolute_path(nah::fs::join_paths(root_, result.value.paths.install_root));
590 const auto install_root = detail::resolve_within(root_ +
"/apps", result.value.paths.install_root);
594 result.value.paths.install_root = *install_root;
601inline std::optional<nah::core::AppDeclaration> NahHost::loadAppManifest(
const std::string& app_dir)
const {
602 auto json_content = nah::fs::read_file(app_dir +
"/nap.json");
604 auto result = nah::json::parse_app_declaration(*json_content);
613inline std::string NahHost::extractMetadataJson(
const std::string& app_dir)
const {
614 auto json_content = nah::fs::read_file(app_dir +
"/nap.json");
620 auto j = nah::json::json::parse(*json_content);
622 if (j.contains(
"app") && j[
"app"].is_object()) {
626 if (j.contains(
"metadata") && j[
"metadata"].is_object()) {
627 return j[
"metadata"].dump();
Result create(const fs::path &source, const fs::path &output)