NAH 2.0.16
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_host.h
Go to the documentation of this file.
1
9#ifndef NAH_HOST_H
10#define NAH_HOST_H
11
12#ifdef __cplusplus
13
14#include "nah_core.h"
15#include "nah_json.h"
16#include "nah_fs.h"
17#include "nah_exec.h"
18#include "nah_semver.h"
19
20#include <string>
21#include <vector>
22#include <memory>
23#include <optional>
24#include <functional>
25#include <algorithm>
26
27namespace nah {
28namespace host {
29
30// Portable getenv that avoids MSVC warnings
31namespace detail {
32inline std::string safe_getenv(const char* name) {
33#ifdef _WIN32
34 char* buf = nullptr;
35 size_t sz = 0;
36 if (_dupenv_s(&buf, &sz, name) == 0 && buf != nullptr) {
37 std::string result(buf);
38 free(buf);
39 return result;
40 }
41 return "";
42#else
43 const char* val = std::getenv(name);
44 return val ? val : "";
45#endif
46}
47} // namespace detail
48
49// ============================================================================
50// App Info
51// ============================================================================
52
53struct AppInfo {
54 std::string id;
55 std::string version;
56 std::string instance_id;
57 std::string install_root;
58 std::string record_path;
59 std::string metadata_json;
60};
61
62// ============================================================================
63// NAH Host Class
64// ============================================================================
65
84class NahHost {
85public:
91 static std::unique_ptr<NahHost> create(const std::string& root_path = "");
92
107 static std::unique_ptr<NahHost> discover(const std::vector<std::string>& search_paths);
108
116 static bool isValidRoot(const std::string& path);
117
121 const std::string& root() const { return root_; }
122
126 std::vector<AppInfo> listApplications() const;
127
134 std::optional<AppInfo> findApplication(const std::string& id,
135 const std::string& version = "") const;
136
140 nah::core::HostEnvironment getHostEnvironment() const;
141
149 nah::core::CompositionResult getLaunchContract(
150 const std::string& app_id,
151 const std::string& version = "",
152 bool enable_trace = false) const;
153
161 nah::core::CompositionResult getLaunchContract(
162 const std::string& app_id,
163 const std::string& version,
164 const nah::core::CompositionOptions& options) const;
165
174 int executeApplication(
175 const std::string& app_id,
176 const std::string& version = "",
177 const std::vector<std::string>& args = {},
178 std::function<void(const std::string&)> output_handler = nullptr) const;
179
187 int executeContract(
188 const nah::core::LaunchContract& contract,
189 const std::vector<std::string>& args = {},
190 std::function<void(const std::string&)> output_handler = nullptr) const;
191
195 bool isApplicationInstalled(const std::string& app_id,
196 const std::string& version = "") const;
197
201 nah::core::RuntimeInventory getInventory() const;
202
207 std::string validateRoot() const;
208
209 // ========================================================================
210 // Component API
211 // ========================================================================
212
219 nah::core::CompositionResult composeComponentLaunch(
220 const std::string& uri,
221 const std::string& referrer_uri = "") const;
222
231 int launchComponent(
232 const std::string& uri,
233 const std::string& referrer_uri = "",
234 const std::vector<std::string>& args = {},
235 std::function<void(const std::string&)> output_handler = nullptr) const;
236
242 bool canHandleComponentUri(const std::string& uri) const;
243
248 std::vector<std::pair<std::string, nah::core::ComponentDecl>> listAllComponents() const;
249
250private:
251 explicit NahHost(std::string root) : root_(std::move(root)) {}
252
253 // Load install record for an app
254 std::optional<nah::core::InstallRecord> loadInstallRecord(const std::string& path) const;
255
256 // Load app manifest (JSON)
257 std::optional<nah::core::AppDeclaration> loadAppManifest(const std::string& app_dir) const;
258
259 std::string extractMetadataJson(const std::string& app_dir) const;
260
261 std::string root_;
262};
263
264// ============================================================================
265// Convenience Functions
266// ============================================================================
267
274inline int quickExecute(const std::string& app_id, const std::string& nah_root = "") {
275 auto host = NahHost::create(nah_root);
276 return host->executeApplication(app_id);
277}
278
284inline std::vector<std::string> listInstalledApps(const std::string& nah_root = "") {
285 auto host = NahHost::create(nah_root);
286 auto apps = host->listApplications();
287 std::vector<std::string> results;
288 for (const auto& app : apps) {
289 results.push_back(app.id + "@" + app.version);
290 }
291 return results;
292}
293
294// ============================================================================
295// Implementation
296// ============================================================================
297
298#ifdef NAH_HOST_IMPLEMENTATION
299
300inline std::unique_ptr<NahHost> NahHost::create(const std::string& root_path) {
301 std::string resolved_root = root_path;
302
303 if (resolved_root.empty()) {
304 std::string env_root = detail::safe_getenv("NAH_ROOT");
305 if (!env_root.empty()) {
306 resolved_root = env_root;
307 } else {
308 resolved_root = "/nah";
309 }
310 }
311
312 return std::unique_ptr<NahHost>(new NahHost(resolved_root));
313}
314
315inline std::vector<AppInfo> NahHost::listApplications() const {
316 std::vector<AppInfo> apps;
317 std::string apps_dir = root_ + "/registry/apps";
318
319 if (!nah::fs::exists(apps_dir)) {
320 return apps;
321 }
322
323 auto files = nah::fs::list_directory(apps_dir);
324 for (const auto& entry : files) {
325 if (entry.size() > 5 && entry.substr(entry.size() - 5) == ".json") {
326 auto record = loadInstallRecord(entry);
327 if (record) {
328 AppInfo info;
329 info.id = record->app.id;
330 info.version = record->app.version;
331 info.instance_id = record->install.instance_id;
332 info.install_root = record->paths.install_root;
333 info.record_path = entry;
334 info.metadata_json = extractMetadataJson(record->paths.install_root);
335 apps.push_back(info);
336 }
337 }
338 }
339
340 return apps;
341}
342
343inline std::optional<AppInfo> NahHost::findApplication(const std::string& id,
344 const std::string& version) const {
345 auto apps = listApplications();
346
347 std::vector<AppInfo> matches;
348 for (const auto& app : apps) {
349 if (app.id == id) {
350 if (version.empty() || app.version == version) {
351 matches.push_back(app);
352 }
353 }
354 }
355
356 if (matches.empty()) {
357 return std::nullopt;
358 }
359
360 // If multiple versions, sort by semver and return the highest
361 if (matches.size() > 1 && version.empty()) {
362 std::sort(matches.begin(), matches.end(), [](const AppInfo& a, const AppInfo& b) {
363 auto va = nah::semver::parse_version(a.version);
364 auto vb = nah::semver::parse_version(b.version);
365 if (va && vb) {
366 return *va > *vb; // Descending order (highest first)
367 }
368 // Fallback to string comparison if parsing fails
369 return a.version > b.version;
370 });
371 }
372 return matches[0];
373}
374
375inline nah::core::HostEnvironment NahHost::getHostEnvironment() const {
376 std::string host_json_path = root_ + "/host/host.json";
377 auto content = nah::fs::read_file(host_json_path);
378 if (!content) {
379 // Return empty environment
380 return nah::core::HostEnvironment{};
381 }
382
383 auto result = nah::json::parse_host_environment(*content, host_json_path);
384 if (result.ok) {
385 return result.value;
386 }
387
388 // Return empty environment on parse failure
389 return nah::core::HostEnvironment{};
390}
391
392inline nah::core::CompositionResult NahHost::getLaunchContract(
393 const std::string& app_id,
394 const std::string& version,
395 bool enable_trace) const {
396
397 // Find the application
398 auto app_info = findApplication(app_id, version);
399 if (!app_info) {
400 nah::core::CompositionResult result;
401 result.ok = false;
402 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
403 result.critical_error_context = "Application not found: " + app_id;
404 return result;
405 }
406
407 // Load install record
408 auto record = loadInstallRecord(app_info->record_path);
409 if (!record) {
410 nah::core::CompositionResult result;
411 result.ok = false;
412 result.critical_error = nah::core::CriticalError::INSTALL_RECORD_INVALID;
413 result.critical_error_context = "Failed to load install record";
414 return result;
415 }
416
417 // Load app manifest
418 auto app_decl = loadAppManifest(app_info->install_root);
419 if (!app_decl) {
420 nah::core::CompositionResult result;
421 result.ok = false;
422 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
423 result.critical_error_context = "Failed to load app manifest";
424 return result;
425 }
426
427 // Load host environment
428 auto host_env = getHostEnvironment();
429
430 // Get inventory
431 auto inventory = getInventory();
432
433 // Compose
434 nah::core::CompositionOptions opts;
435 opts.enable_trace = enable_trace;
436
437 return nah::core::nah_compose(*app_decl, host_env, *record, inventory, opts);
438}
439
440inline nah::core::CompositionResult NahHost::getLaunchContract(
441 const std::string& app_id,
442 const std::string& version,
443 const nah::core::CompositionOptions& options) const {
444
445 // Find the application
446 auto app_info = findApplication(app_id, version);
447 if (!app_info) {
448 nah::core::CompositionResult result;
449 result.ok = false;
450 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
451 result.critical_error_context = "Application not found: " + app_id;
452 return result;
453 }
454
455 // Load install record
456 auto record = loadInstallRecord(app_info->record_path);
457 if (!record) {
458 nah::core::CompositionResult result;
459 result.ok = false;
460 result.critical_error = nah::core::CriticalError::INSTALL_RECORD_INVALID;
461 result.critical_error_context = "Failed to load install record";
462 return result;
463 }
464
465 // Load app manifest
466 auto app_decl = loadAppManifest(app_info->install_root);
467 if (!app_decl) {
468 nah::core::CompositionResult result;
469 result.ok = false;
470 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
471 result.critical_error_context = "Failed to load app manifest";
472 return result;
473 }
474
475 // Load host environment
476 auto host_env = getHostEnvironment();
477
478 // Get inventory
479 auto inventory = getInventory();
480
481 // Use provided options (including loader_override)
482 return nah::core::nah_compose(*app_decl, host_env, *record, inventory, options);
483}
484
485inline int NahHost::executeApplication(
486 const std::string& app_id,
487 const std::string& version,
488 const std::vector<std::string>& args,
489 std::function<void(const std::string&)> output_handler) const {
490
491 auto result = getLaunchContract(app_id, version);
492 if (!result.ok) {
493 if (output_handler) {
494 output_handler("Error: " + result.critical_error_context);
495 }
496 return 1;
497 }
498
499 return executeContract(result.contract, args, output_handler);
500}
501
502inline int NahHost::executeContract(
503 const nah::core::LaunchContract& contract,
504 const std::vector<std::string>& /* args */,
505 std::function<void(const std::string&)> output_handler) const {
506
507 // Use nah::exec::execute which takes the contract directly
508 // Note: args parameter reserved for future use (appending to contract arguments)
509 auto exec_result = nah::exec::execute(contract);
510
511 if (!exec_result.ok) {
512 if (output_handler) {
513 output_handler("Execution error: " + exec_result.error);
514 }
515 return 1;
516 }
517
518 return exec_result.exit_code;
519}
520
521inline bool NahHost::isApplicationInstalled(const std::string& app_id,
522 const std::string& version) const {
523 return findApplication(app_id, version).has_value();
524}
525
526inline nah::core::RuntimeInventory NahHost::getInventory() const {
527 nah::core::RuntimeInventory inventory;
528 std::string naks_dir = root_ + "/registry/naks";
529
530 if (!nah::fs::exists(naks_dir)) {
531 return inventory;
532 }
533
534 auto files = nah::fs::list_directory(naks_dir);
535 for (const auto& entry : files) {
536 // list_directory returns full paths, so use entry directly
537 if (entry.size() > 5 && entry.substr(entry.size() - 5) == ".json") {
538 // NAH v2.0: Registry files ARE the runtime descriptors
539 // Parse the registry file directly as a RuntimeDescriptor
540 auto runtime_content = nah::fs::read_file(entry);
541 if (runtime_content) {
542 // Extract record_ref from filename
543 std::string basename = entry;
544 size_t last_slash = entry.rfind('/');
545 if (last_slash != std::string::npos) {
546 basename = entry.substr(last_slash + 1);
547 }
548 std::string record_ref = basename;
549
550 auto result = nah::json::parse_runtime_descriptor(*runtime_content, entry);
551 if (result.ok) {
552 result.value.source_path = entry;
553
554 // Resolve relative paths to absolute (for sandbox/portability support)
555 if (!result.value.paths.root.empty() && !nah::fs::is_absolute_path(result.value.paths.root)) {
556 result.value.paths.root = nah::fs::absolute_path(nah::fs::join_paths(root_, result.value.paths.root));
557 }
558
559 // Resolve relative lib_dirs
560 for (auto& lib_dir : result.value.paths.lib_dirs) {
561 if (!lib_dir.empty() && !nah::fs::is_absolute_path(lib_dir)) {
562 lib_dir = nah::fs::absolute_path(nah::fs::join_paths(result.value.paths.root, lib_dir));
563 }
564 }
565
566 // Resolve relative loader exec_paths
567 for (auto& [name, loader] : result.value.loaders) {
568 if (!loader.exec_path.empty() && !nah::fs::is_absolute_path(loader.exec_path)) {
569 loader.exec_path = nah::fs::absolute_path(nah::fs::join_paths(result.value.paths.root, loader.exec_path));
570 }
571 }
572
573 inventory.runtimes[record_ref] = result.value;
574 }
575 }
576 }
577 }
578
579 return inventory;
580}
581
582inline std::string NahHost::validateRoot() const {
583 if (!nah::fs::exists(root_)) {
584 return "NAH root does not exist: " + root_;
585 }
586
587 // Check required directories
588 std::vector<std::string> required_dirs = {
589 "/registry/apps",
590 "/host"
591 };
592
593 for (const auto& dir : required_dirs) {
594 if (!nah::fs::exists(root_ + dir)) {
595 return "Missing required directory: " + root_ + dir;
596 }
597 }
598
599 return ""; // Valid
600}
601
602inline bool NahHost::isValidRoot(const std::string& path) {
603 if (path.empty() || !nah::fs::exists(path)) {
604 return false;
605 }
606
607 // Check required directories that make up a valid NAH root
608 std::vector<std::string> required_dirs = {
609 "/registry/apps",
610 "/host"
611 };
612
613 for (const auto& dir : required_dirs) {
614 std::string full_path = path + dir;
615 if (!nah::fs::exists(full_path)) {
616 return false;
617 }
618 }
619
620 return true;
621}
622
623inline std::unique_ptr<NahHost> NahHost::discover(const std::vector<std::string>& search_paths) {
624 for (const auto& path : search_paths) {
625 // Skip empty paths (e.g., from getenv returning nullptr)
626 if (path.empty()) {
627 continue;
628 }
629
630 // Check if this path is a valid NAH root
631 if (isValidRoot(path)) {
632 return std::unique_ptr<NahHost>(new NahHost(path));
633 }
634 }
635
636 // No valid root found
637 return nullptr;
638}
639
640inline std::optional<nah::core::InstallRecord> NahHost::loadInstallRecord(const std::string& path) const {
641 auto content = nah::fs::read_file(path);
642 if (!content) {
643 return std::nullopt;
644 }
645
646 auto result = nah::json::parse_install_record(*content);
647 if (result.ok) {
648 // Ensure absolute paths (portable check for both Unix and Windows)
649 if (!result.value.paths.install_root.empty() && !nah::fs::is_absolute_path(result.value.paths.install_root)) {
650 result.value.paths.install_root = nah::fs::absolute_path(nah::fs::join_paths(root_, result.value.paths.install_root));
651 }
652 return result.value;
653 }
654
655 return std::nullopt;
656}
657
658inline std::optional<nah::core::AppDeclaration> NahHost::loadAppManifest(const std::string& app_dir) const {
659 auto json_content = nah::fs::read_file(app_dir + "/nap.json");
660 if (json_content) {
661 auto result = nah::json::parse_app_declaration(*json_content);
662 if (result.ok) {
663 return result.value;
664 }
665 }
666
667 return std::nullopt;
668}
669
670inline std::string NahHost::extractMetadataJson(const std::string& app_dir) const {
671 auto json_content = nah::fs::read_file(app_dir + "/nap.json");
672 if (!json_content) {
673 return "{}";
674 }
675
676 try {
677 auto j = nah::json::json::parse(*json_content);
678
679 if (j.contains("app") && j["app"].is_object()) {
680 j = j["app"];
681 }
682
683 if (j.contains("metadata") && j["metadata"].is_object()) {
684 return j["metadata"].dump();
685 }
686 } catch (...) {
687 }
688
689 return "{}";
690}
691
692// ============================================================================
693// Component Implementation
694// ============================================================================
695
696// Helper: Match URI against pattern
697inline bool matches_uri_pattern(const std::string& pattern, const std::string& uri) {
698 auto pattern_parsed = nah::core::parse_component_uri(pattern);
699 auto uri_parsed = nah::core::parse_component_uri(uri);
700
701 if (!pattern_parsed.valid || !uri_parsed.valid) {
702 return false;
703 }
704
705 // App IDs must match
706 if (pattern_parsed.app_id != uri_parsed.app_id) {
707 return false;
708 }
709
710 // Check if pattern ends with wildcard
711 if (pattern_parsed.component_path.size() >= 2 &&
712 pattern_parsed.component_path.substr(pattern_parsed.component_path.size() - 2) == "/*") {
713 // Prefix match
714 std::string prefix = pattern_parsed.component_path.substr(
715 0, pattern_parsed.component_path.size() - 2
716 );
717 return uri_parsed.component_path.substr(0, prefix.size()) == prefix;
718 } else {
719 // Exact match
720 return pattern_parsed.component_path == uri_parsed.component_path;
721 }
722}
723
724inline nah::core::CompositionResult NahHost::composeComponentLaunch(
725 const std::string& uri,
726 const std::string& referrer_uri) const {
727
728 // 1. Parse URI
729 auto parsed = nah::core::parse_component_uri(uri);
730 if (!parsed.valid) {
731 nah::core::CompositionResult result;
732 result.ok = false;
733 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
734 result.critical_error_context = "Invalid component URI: " + uri;
735 return result;
736 }
737
738 // 2. Find application
739 auto app_info = findApplication(parsed.app_id);
740 if (!app_info) {
741 nah::core::CompositionResult result;
742 result.ok = false;
743 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
744 result.critical_error_context = "Application not found: " + parsed.app_id;
745 return result;
746 }
747
748 // 3. Load app manifest to get components
749 auto app_decl = loadAppManifest(app_info->install_root);
750 if (!app_decl) {
751 nah::core::CompositionResult result;
752 result.ok = false;
753 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
754 result.critical_error_context = "Failed to load app manifest";
755 return result;
756 }
757
758 // 4. Match component by URI pattern
759 nah::core::ComponentDecl* matched_component = nullptr;
760 for (auto& comp : app_decl->components) {
761 if (matches_uri_pattern(comp.uri_pattern, uri)) {
762 matched_component = &comp;
763 break; // First match wins
764 }
765 }
766
767 if (!matched_component) {
768 nah::core::CompositionResult result;
769 result.ok = false;
770 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
771 result.critical_error_context = "No component matches URI: " + uri;
772 return result;
773 }
774
775 // 5. Create modified app declaration with component entrypoint
776 // We reuse nah_compose by creating a temporary AppDeclaration
777 // with the component's entrypoint
778 nah::core::AppDeclaration component_app = *app_decl;
779 component_app.entrypoint_path = matched_component->entrypoint;
780
781 // Override loader if component specifies one
782 if (!matched_component->loader.empty()) {
783 component_app.nak_loader = matched_component->loader;
784 }
785
786 // Merge component-specific environment
787 for (const auto& [key, value] : matched_component->environment) {
788 // Convert to KEY=value format
789 component_app.env_vars.push_back(key + "=" + value.value);
790 }
791
792 // Merge component-specific permissions
793 component_app.permissions_filesystem.insert(
794 component_app.permissions_filesystem.end(),
795 matched_component->permissions_filesystem.begin(),
796 matched_component->permissions_filesystem.end()
797 );
798 component_app.permissions_network.insert(
799 component_app.permissions_network.end(),
800 matched_component->permissions_network.begin(),
801 matched_component->permissions_network.end()
802 );
803
804 // 6. Get install record (need for nah_compose)
805 auto install_record = loadInstallRecord(app_info->record_path);
806 if (!install_record) {
807 nah::core::CompositionResult result;
808 result.ok = false;
809 result.critical_error = nah::core::CriticalError::INSTALL_RECORD_INVALID;
810 result.critical_error_context = "Failed to load install record";
811 return result;
812 }
813
814 // Override pinned loader if component specifies one
815 if (!matched_component->loader.empty()) {
816 install_record->nak.loader = matched_component->loader;
817 }
818
819 // 7. Get host environment and inventory
820 auto host_env = getHostEnvironment();
821 auto inventory = getInventory();
822
823 // 8. Compose using the standard nah_compose function
824 nah::core::CompositionOptions comp_opts;
825 auto result = nah::core::nah_compose(component_app, host_env, *install_record, inventory, comp_opts);
826
827 if (!result.ok) {
828 return result;
829 }
830
831 // 9. Inject component-specific environment variables
832 result.contract.environment["NAH_COMPONENT_ID"] = matched_component->id;
833 result.contract.environment["NAH_COMPONENT_URI"] = uri;
834 result.contract.environment["NAH_COMPONENT_PATH"] = parsed.component_path;
835
836 if (!parsed.query.empty()) {
837 result.contract.environment["NAH_COMPONENT_QUERY"] = parsed.query;
838 }
839 if (!parsed.fragment.empty()) {
840 result.contract.environment["NAH_COMPONENT_FRAGMENT"] = parsed.fragment;
841 }
842 if (!referrer_uri.empty()) {
843 result.contract.environment["NAH_COMPONENT_REFERRER"] = referrer_uri;
844 }
845
846 return result;
847}
848
849inline int NahHost::launchComponent(
850 const std::string& uri,
851 const std::string& referrer_uri,
852 const std::vector<std::string>& args,
853 std::function<void(const std::string&)> output_handler) const {
854
855 auto result = composeComponentLaunch(uri, referrer_uri);
856 if (!result.ok) {
857 if (output_handler) {
858 output_handler("Error: " + result.critical_error_context);
859 }
860 return 1;
861 }
862
863 return executeContract(result.contract, args, output_handler);
864}
865
866inline bool NahHost::canHandleComponentUri(const std::string& uri) const {
867 auto parsed = nah::core::parse_component_uri(uri);
868 if (!parsed.valid) {
869 return false;
870 }
871
872 auto app_info = findApplication(parsed.app_id);
873 if (!app_info) {
874 return false;
875 }
876
877 auto app_decl = loadAppManifest(app_info->install_root);
878 if (!app_decl || app_decl->components.empty()) {
879 return false;
880 }
881
882 // Check if any component matches
883 for (const auto& comp : app_decl->components) {
884 if (matches_uri_pattern(comp.uri_pattern, uri)) {
885 return true;
886 }
887 }
888
889 return false;
890}
891
892inline std::vector<std::pair<std::string, nah::core::ComponentDecl>>
893NahHost::listAllComponents() const {
894 std::vector<std::pair<std::string, nah::core::ComponentDecl>> result;
895
896 auto apps = listApplications();
897 for (const auto& app : apps) {
898 auto manifest = loadAppManifest(app.install_root);
899 if (manifest) {
900 for (const auto& comp : manifest->components) {
901 result.push_back({app.id, comp});
902 }
903 }
904 }
905
906 return result;
907}
908
909#endif // NAH_HOST_IMPLEMENTATION
910
911} // namespace host
912} // namespace nah
913
914#endif // __cplusplus
915
916#endif // NAH_HOST_H