NAH 3.0.0
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_host.h
Go to the documentation of this file.
1
3#ifndef NAH_HOST_H
4#define NAH_HOST_H
5
6#ifdef __cplusplus
7
8#include "nah_core.h"
9#include "nah_json.h"
10#include "nah_fs.h"
11#include "nah_exec.h"
12#include "nah_semver.h"
13
14#include <string>
15#include <vector>
16#include <memory>
17#include <optional>
18#include <algorithm>
19#include <filesystem>
20
21namespace nah {
22namespace host {
23
24// Portable getenv that avoids MSVC warnings
25namespace detail {
26inline std::string safe_getenv(const char* name) {
27#ifdef _WIN32
28 char* buf = nullptr;
29 size_t sz = 0;
30 if (_dupenv_s(&buf, &sz, name) == 0 && buf != nullptr) {
31 std::string result(buf);
32 free(buf);
33 return result;
34 }
35 return "";
36#else
37 const char* val = std::getenv(name);
38 return val ? val : "";
39#endif
40}
41
42inline std::optional<std::filesystem::path> canonical_boundary_path(const std::string& path) {
43 std::error_code ec;
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();
49}
50
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;
57
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) {
62 return std::nullopt;
63 }
64 }
65 return normalized_candidate->string();
66}
67} // namespace detail
68
69// ============================================================================
70// App Info
71// ============================================================================
72
73struct AppInfo {
74 std::string id;
75 std::string version;
76 std::string instance_id;
77 std::string install_root;
78 std::string record_path;
79 std::string metadata_json;
80};
81
82// ============================================================================
83// NAH Host Class
84// ============================================================================
85
104class NahHost {
105public:
111 static std::unique_ptr<NahHost> create(const std::string& root_path = "");
112
127 static std::unique_ptr<NahHost> discover(const std::vector<std::string>& search_paths);
128
136 static bool isValidRoot(const std::string& path);
137
141 const std::string& root() const { return root_; }
142
146 std::vector<AppInfo> listApplications() const;
147
154 std::optional<AppInfo> findApplication(const std::string& id,
155 const std::string& version = "") const;
156
160 std::optional<nah::core::HostEnvironment> getHostEnvironment() const;
161
169 nah::core::CompositionResult getLaunchContract(
170 const std::string& app_id,
171 const std::string& version = "",
172 bool enable_trace = false) const;
173
181 nah::core::CompositionResult getLaunchContract(
182 const std::string& app_id,
183 const std::string& version,
184 const nah::core::CompositionOptions& options) const;
185
193 int executeApplication(
194 const std::string& app_id,
195 const std::string& version = "",
196 const std::vector<std::string>& args = {}) const;
197
204 int executeContract(
205 const nah::core::LaunchContract& contract,
206 const std::vector<std::string>& args = {}) const;
207
211 bool isApplicationInstalled(const std::string& app_id,
212 const std::string& version = "") const;
213
217 nah::core::RuntimeInventory getInventory() const;
218
223 std::string validateRoot() const;
224
225private:
226 explicit NahHost(std::string root) : root_(std::move(root)) {}
227
228 // Load install record for an app
229 std::optional<nah::core::InstallRecord> loadInstallRecord(const std::string& path) const;
230
231 // Load app manifest (JSON)
232 std::optional<nah::core::AppDeclaration> loadAppManifest(const std::string& app_dir) const;
233
234 std::string extractMetadataJson(const std::string& app_dir) const;
235
236 std::string root_;
237};
238
239// ============================================================================
240// Implementation
241// ============================================================================
242
243inline std::unique_ptr<NahHost> NahHost::create(const std::string& root_path) {
244 std::string resolved_root = root_path;
245
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;
250 } else {
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";
254 }
255 }
256
257 return std::unique_ptr<NahHost>(new NahHost(resolved_root));
258}
259
260inline std::vector<AppInfo> NahHost::listApplications() const {
261 std::vector<AppInfo> apps;
262 std::string apps_dir = root_ + "/registry/apps";
263
264 if (!nah::fs::exists(apps_dir)) {
265 return apps;
266 }
267
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);
272 if (record) {
273 AppInfo info;
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);
281 }
282 }
283 }
284
285 return apps;
286}
287
288inline std::optional<AppInfo> NahHost::findApplication(const std::string& id,
289 const std::string& version) const {
290 auto apps = listApplications();
291
292 std::vector<AppInfo> matches;
293 for (const auto& app : apps) {
294 if (app.id == id) {
295 if (version.empty() || app.version == version) {
296 matches.push_back(app);
297 }
298 }
299 }
300
301 if (matches.empty()) {
302 return std::nullopt;
303 }
304
305 // If multiple versions, sort by semver and return the highest
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);
310 if (va && vb) {
311 return *va > *vb; // Descending order (highest first)
312 }
313 // Fallback to string comparison if parsing fails
314 return a.version > b.version;
315 });
316 }
317 return matches[0];
318}
319
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);
323 if (!content) {
324 // Return empty environment
325 return nah::core::HostEnvironment{};
326 }
327
328 auto result = nah::json::parse_host_environment(*content, host_json_path);
329 if (result.ok) {
330 return result.value;
331 }
332
333 return std::nullopt;
334}
335
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);
343}
344
345inline nah::core::CompositionResult NahHost::getLaunchContract(
346 const std::string& app_id,
347 const std::string& version,
348 const nah::core::CompositionOptions& options) const {
349
350 // Find the application
351 auto app_info = findApplication(app_id, version);
352 if (!app_info) {
353 nah::core::CompositionResult result;
354 result.ok = false;
355 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
356 result.critical_error_context = "Application not found: " + app_id;
357 return result;
358 }
359
360 // Load install record
361 auto record = loadInstallRecord(app_info->record_path);
362 if (!record) {
363 nah::core::CompositionResult result;
364 result.ok = false;
365 result.critical_error = nah::core::CriticalError::INSTALL_RECORD_INVALID;
366 result.critical_error_context = "Failed to load install record";
367 return result;
368 }
369
370 // Load app manifest
371 auto app_decl = loadAppManifest(app_info->install_root);
372 if (!app_decl) {
373 nah::core::CompositionResult result;
374 result.ok = false;
375 result.critical_error = nah::core::CriticalError::MANIFEST_MISSING;
376 result.critical_error_context = "Failed to load app manifest";
377 return result;
378 }
379
380 // Load host environment
381 auto host_env = getHostEnvironment();
382 if (!host_env) {
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";
386 return result;
387 }
388
389 // Get inventory
390 auto inventory = getInventory();
391
392 auto result = nah::core::nah_compose(*app_decl, *host_env, *record, inventory, options);
393 if (!result.ok) return result;
394
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)) {
399 result.ok = false;
400 result.critical_error = nah::core::CriticalError::ENTRYPOINT_NOT_FOUND;
401 result.critical_error_context = "Execution binary not found: " + result.contract.execution.binary;
402 return result;
403 }
404 result.contract.execution.binary = *binary;
405
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);
410 }
411 const auto cwd = cwd_in_app ? cwd_in_app : cwd_in_nak;
412 if (!cwd || !nah::fs::is_directory(*cwd)) {
413 result.ok = false;
414 result.critical_error = nah::core::CriticalError::PATH_TRAVERSAL;
415 result.critical_error_context = "Working directory is outside the installed app and runtime";
416 return result;
417 }
418 result.contract.execution.cwd = *cwd;
419 return result;
420}
421
422inline int NahHost::executeApplication(
423 const std::string& app_id,
424 const std::string& version,
425 const std::vector<std::string>& args) const {
426
427 auto result = getLaunchContract(app_id, version);
428 if (!result.ok) return 1;
429
430 return executeContract(result.contract, args);
431}
432
433inline int NahHost::executeContract(
434 const nah::core::LaunchContract& contract,
435 const std::vector<std::string>& args) const {
436
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);
441
442 if (!exec_result.ok) return 1;
443
444 return exec_result.exit_code;
445}
446
447inline bool NahHost::isApplicationInstalled(const std::string& app_id,
448 const std::string& version) const {
449 return findApplication(app_id, version).has_value();
450}
451
452inline nah::core::RuntimeInventory NahHost::getInventory() const {
453 nah::core::RuntimeInventory inventory;
454 std::string naks_dir = root_ + "/registry/naks";
455
456 if (!nah::fs::exists(naks_dir)) {
457 return inventory;
458 }
459
460 auto files = nah::fs::list_directory(naks_dir);
461 for (const auto& entry : files) {
462 // list_directory returns full paths, so use entry directly
463 if (entry.size() > 5 && entry.substr(entry.size() - 5) == ".json") {
464 // NAH v2.0: Registry files ARE the runtime descriptors
465 // Parse the registry file directly as a RuntimeDescriptor
466 auto runtime_content = nah::fs::read_file(entry);
467 if (runtime_content) {
468 // Extract record_ref from filename
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);
473 }
474 std::string record_ref = basename;
475
476 auto result = nah::json::parse_runtime_descriptor(*runtime_content, entry);
477 if (result.ok) {
478 result.value.source_path = entry;
479
480 // Resolve relative paths to absolute (for sandbox/portability support)
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));
483 }
484 const auto runtime_root = detail::resolve_within(root_ + "/naks", result.value.paths.root);
485 if (!runtime_root) {
486 continue;
487 }
488 result.value.paths.root = *runtime_root;
489
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));
495 }
496 const auto resource_root = detail::resolve_within(
497 result.value.paths.root, result.value.paths.resource_root);
498 if (!resource_root) {
499 continue;
500 }
501 result.value.paths.resource_root = *resource_root;
502
503 // Resolve relative lib_dirs
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));
507 }
508 const auto resolved = detail::resolve_within(result.value.paths.root, lib_dir);
509 if (!resolved) {
510 lib_dir.clear();
511 } else {
512 lib_dir = *resolved;
513 }
514 }
515
516 // Resolve relative loader exec_paths
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));
520 }
521 const auto resolved = detail::resolve_within(result.value.paths.root, loader.exec_path);
522 if (!resolved) {
523 loader.exec_path.clear();
524 } else {
525 loader.exec_path = *resolved;
526 }
527 }
528
529 inventory.runtimes[record_ref] = result.value;
530 }
531 }
532 }
533 }
534
535 return inventory;
536}
537
538inline std::string NahHost::validateRoot() const {
539 if (!nah::fs::is_directory(root_)) {
540 return "NAH root does not exist: " + root_;
541 }
542
543 // Check required directories
544 const std::vector<std::string> required_dirs = {
545 "/apps", "/naks", "/host", "/registry/apps", "/registry/naks", "/staging"
546 };
547
548 for (const auto& dir : required_dirs) {
549 if (!nah::fs::is_directory(root_ + dir)) {
550 return "Missing required directory: " + root_ + dir;
551 }
552 }
553
554 return ""; // Valid
555}
556
557inline bool NahHost::isValidRoot(const std::string& path) {
558 return !path.empty() && NahHost(path).validateRoot().empty();
559}
560
561inline std::unique_ptr<NahHost> NahHost::discover(const std::vector<std::string>& search_paths) {
562 for (const auto& path : search_paths) {
563 // Skip empty paths (e.g., from getenv returning nullptr)
564 if (path.empty()) {
565 continue;
566 }
567
568 // Check if this path is a valid NAH root
569 if (isValidRoot(path)) {
570 return std::unique_ptr<NahHost>(new NahHost(path));
571 }
572 }
573
574 // No valid root found
575 return nullptr;
576}
577
578inline std::optional<nah::core::InstallRecord> NahHost::loadInstallRecord(const std::string& path) const {
579 auto content = nah::fs::read_file(path);
580 if (!content) {
581 return std::nullopt;
582 }
583
584 auto result = nah::json::parse_install_record(*content);
585 if (result.ok) {
586 // Ensure absolute paths (portable check for both Unix and Windows)
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));
589 }
590 const auto install_root = detail::resolve_within(root_ + "/apps", result.value.paths.install_root);
591 if (!install_root) {
592 return std::nullopt;
593 }
594 result.value.paths.install_root = *install_root;
595 return result.value;
596 }
597
598 return std::nullopt;
599}
600
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");
603 if (json_content) {
604 auto result = nah::json::parse_app_declaration(*json_content);
605 if (result.ok) {
606 return result.value;
607 }
608 }
609
610 return std::nullopt;
611}
612
613inline std::string NahHost::extractMetadataJson(const std::string& app_dir) const {
614 auto json_content = nah::fs::read_file(app_dir + "/nap.json");
615 if (!json_content) {
616 return "{}";
617 }
618
619 try {
620 auto j = nah::json::json::parse(*json_content);
621
622 if (j.contains("app") && j["app"].is_object()) {
623 j = j["app"];
624 }
625
626 if (j.contains("metadata") && j["metadata"].is_object()) {
627 return j["metadata"].dump();
628 }
629 } catch (...) {
630 }
631
632 return "{}";
633}
634
635} // namespace host
636} // namespace nah
637
638#endif // __cplusplus
639
640#endif // NAH_HOST_H
Result create(const fs::path &source, const fs::path &output)
Definition nah_archive.h:98