NAH 3.0.0
Native Application Host - Library API Reference
Loading...
Searching...
No Matches
nah_core.h
Go to the documentation of this file.
1/*
2 * NAH Core - Header-Only Library
3 * SPDX-License-Identifier: MIT
4 *
5 * ============================================================================
6 * WHAT IS NAH?
7 * ============================================================================
8 *
9 * NAH answers a simple question: "How should I launch this application?"
10 *
11 * When you install an app that needs Python 3.11, or Node 20, or Lua 5.4,
12 * something has to figure out: which binary to run, what environment variables
13 * to set, which library paths to include, and what permissions are required.
14 *
15 * NAH takes four inputs and produces one output:
16 *
17 * +-------------------+
18 * | AppDeclaration |---+ What the app says it needs
19 * +-------------------+ |
20 * +-------------------+ |
21 * | HostEnvironment |---+--> nah_compose() --> LaunchContract
22 * +-------------------+ | (everything needed to run)
23 * +-------------------+ |
24 * | InstallRecord |---+ Where the app is installed
25 * +-------------------+ |
26 * +-------------------+ |
27 * | RuntimeInventory |---+ Available runtimes (Python, Node, etc.)
28 * +-------------------+
29 *
30 * The result is a LaunchContract: a complete, self-contained specification
31 * that tells you exactly how to run the application.
32 *
33 * ============================================================================
34 * QUICK START
35 * ============================================================================
36 *
37 * 1. Include the header (no linking required):
38 *
39 * #include "nah/nah_core.h"
40 * using namespace nah::core;
41 *
42 * 2. Prepare your inputs:
43 *
44 * AppDeclaration app;
45 * app.id = "com.example.myapp";
46 * app.version = "1.0.0";
47 * app.entrypoint_path = "main.lua";
48 * app.nak_id = "lua";
49 * app.nak_version_req = ">=5.4.0";
50 *
51 * InstallRecord install;
52 * install.install.instance_id = "abc123";
53 * install.paths.install_root = "/apps/myapp";
54 * install.nak.record_ref = "lua@5.4.6.json";
55 *
56 * HostEnvironment host_env; // Empty = no host overrides
57 * RuntimeInventory inventory;
58 * inventory.runtimes["lua@5.4.6.json"] = your_lua_runtime;
59 *
60 * 3. Compose and use the contract:
61 *
62 * CompositionResult result = nah_compose(app, host_env, install, inventory);
63 * if (result.ok) {
64 * // result.contract.execution.binary -> "/runtimes/lua/bin/lua"
65 * // result.contract.execution.arguments -> ["/apps/myapp/main.lua"]
66 * // result.contract.environment -> {"LUA_PATH": "...", ...}
67 * exec(result.contract); // Your execution logic
68 * }
69 *
70 * For file-based usage with JSON parsing, see nah.h which adds nah_json.h,
71 * nah_fs.h, and nah_exec.h on top of this pure core.
72 *
73 * ============================================================================
74 * KEY TYPES
75 * ============================================================================
76 *
77 * Inputs:
78 * AppDeclaration - What the app needs (id, version, entrypoint, runtime)
79 * HostEnvironment - Host-provided environment variables
80 * InstallRecord - Where the app lives and which runtime version to use
81 * RuntimeInventory - Available runtimes on this host
82 *
83 * Output:
84 * LaunchContract - Complete exec specification (binary, args, env, cwd)
85 *
86 * Each type has inline documentation with usage examples.
87 */
88
89#ifndef NAH_CORE_H
90#define NAH_CORE_H
91
92#ifdef __cplusplus
93
94#include <algorithm>
95#include <cstdint>
96#include <optional>
97#include <sstream>
98#include <string>
99#include <unordered_map>
100#include <vector>
101
102namespace nah {
103namespace core {
104
105// ============================================================================
106// VERSION AND CONSTANTS
107// ============================================================================
108
110constexpr const char* NAH_CORE_VERSION = "2.0.0";
111constexpr int NAH_CORE_VERSION_MAJOR = 2;
112constexpr int NAH_CORE_VERSION_MINOR = 0;
113constexpr int NAH_CORE_VERSION_PATCH = 0;
114
116constexpr const char* NAH_CONTRACT_SCHEMA = "nah.launch.contract.v2";
117
119constexpr size_t MAX_EXPANDED_SIZE = 64 * 1024;
120
122constexpr size_t MAX_PLACEHOLDERS = 128;
123
125constexpr size_t MAX_ENV_VARS = 1024;
126
128constexpr size_t MAX_LIBRARY_PATHS = 256;
129
131constexpr size_t MAX_ARGUMENTS = 1024;
132
133// ============================================================================
134// ENVIRONMENT OPERATIONS
135// ============================================================================
136
146enum class EnvOp {
147 Set,
148 Prepend,
149 Append,
150 Unset
151};
152
154inline const char* env_op_to_string(EnvOp op) {
155 switch (op) {
156 case EnvOp::Set: return "set";
157 case EnvOp::Prepend: return "prepend";
158 case EnvOp::Append: return "append";
159 case EnvOp::Unset: return "unset";
160 }
161 return "set";
162}
163
165inline std::optional<EnvOp> parse_env_op(const std::string& s) {
166 if (s == "set") return EnvOp::Set;
167 if (s == "prepend") return EnvOp::Prepend;
168 if (s == "append") return EnvOp::Append;
169 if (s == "unset") return EnvOp::Unset;
170 return std::nullopt;
171}
172
179struct EnvValue {
180 EnvOp op = EnvOp::Set;
181 std::string value;
182 std::string separator = ":";
183
184 EnvValue() = default;
185 EnvValue(const char* v) : op(EnvOp::Set), value(v) {}
186 EnvValue(const std::string& v) : op(EnvOp::Set), value(v) {}
187 EnvValue(EnvOp o, const std::string& v, const std::string& sep = ":")
188 : op(o), value(v), separator(sep) {}
189
190 bool is_simple() const { return op == EnvOp::Set; }
191
192 // Comparison operators
193 bool operator==(const std::string& other) const { return value == other; }
194 bool operator==(const char* other) const { return value == other; }
195 bool operator==(const EnvValue& other) const {
196 return op == other.op && value == other.value && separator == other.separator;
197 }
198 bool operator!=(const EnvValue& other) const { return !(*this == other); }
199};
200
202using EnvMap = std::unordered_map<std::string, EnvValue>;
203
204// ============================================================================
205// TRUST STATE
206// ============================================================================
207
216enum class TrustState {
217 Verified,
218 Unverified,
219 Failed,
220 Unknown
221};
222
223inline const char* trust_state_to_string(TrustState s) {
224 switch (s) {
225 case TrustState::Verified: return "verified";
226 case TrustState::Unverified: return "unverified";
227 case TrustState::Failed: return "failed";
228 case TrustState::Unknown: return "unknown";
229 }
230 return "unknown";
231}
232
233inline std::optional<TrustState> parse_trust_state(const std::string& s) {
234 if (s == "verified") return TrustState::Verified;
235 if (s == "unverified") return TrustState::Unverified;
236 if (s == "failed") return TrustState::Failed;
237 if (s == "unknown") return TrustState::Unknown;
238 return std::nullopt;
239}
240
247struct TrustInfo {
248 TrustState state = TrustState::Unknown;
249 std::string source;
250 std::string evaluated_at;
251 std::string expires_at;
252 std::string inputs_hash;
253 std::unordered_map<std::string, std::string> details;
254};
255
256// ============================================================================
257// WARNING SYSTEM
258// ============================================================================
259
265enum class Warning {
266 trust_state_unknown,
267 trust_state_unverified,
268 trust_state_failed,
269 trust_state_stale,
270};
271
272inline const char* warning_to_string(Warning w) {
273 switch (w) {
274 case Warning::trust_state_unknown: return "trust_state_unknown";
275 case Warning::trust_state_unverified: return "trust_state_unverified";
276 case Warning::trust_state_failed: return "trust_state_failed";
277 case Warning::trust_state_stale: return "trust_state_stale";
278 }
279 return "unknown";
280}
281
285struct WarningObject {
286 std::string key;
287 std::unordered_map<std::string, std::string> fields;
288
289 bool operator==(const WarningObject& other) const {
290 return key == other.key && fields == other.fields;
291 }
292};
293
294// ============================================================================
295// CRITICAL ERRORS
296// ============================================================================
297
304enum class CriticalError {
305 MANIFEST_MISSING,
306 ENTRYPOINT_NOT_FOUND,
307 PATH_TRAVERSAL,
308 EXPANSION_FAILED,
309 HOST_CONFIG_INVALID,
310 INSTALL_RECORD_INVALID,
311 NAK_NOT_FOUND,
312 NAK_LOADER_INVALID,
313};
314
315inline const char* critical_error_to_string(CriticalError e) {
316 switch (e) {
317 case CriticalError::MANIFEST_MISSING: return "MANIFEST_MISSING";
318 case CriticalError::ENTRYPOINT_NOT_FOUND: return "ENTRYPOINT_NOT_FOUND";
319 case CriticalError::PATH_TRAVERSAL: return "PATH_TRAVERSAL";
320 case CriticalError::EXPANSION_FAILED: return "EXPANSION_FAILED";
321 case CriticalError::HOST_CONFIG_INVALID: return "HOST_CONFIG_INVALID";
322 case CriticalError::INSTALL_RECORD_INVALID: return "INSTALL_RECORD_INVALID";
323 case CriticalError::NAK_NOT_FOUND: return "NAK_NOT_FOUND";
324 case CriticalError::NAK_LOADER_INVALID: return "NAK_LOADER_INVALID";
325 }
326 return "UNKNOWN";
327}
328
329inline std::optional<CriticalError> parse_critical_error(const std::string& s) {
330 if (s == "MANIFEST_MISSING") return CriticalError::MANIFEST_MISSING;
331 if (s == "ENTRYPOINT_NOT_FOUND") return CriticalError::ENTRYPOINT_NOT_FOUND;
332 if (s == "PATH_TRAVERSAL") return CriticalError::PATH_TRAVERSAL;
333 if (s == "EXPANSION_FAILED") return CriticalError::EXPANSION_FAILED;
334 if (s == "HOST_CONFIG_INVALID") return CriticalError::HOST_CONFIG_INVALID;
335 if (s == "INSTALL_RECORD_INVALID") return CriticalError::INSTALL_RECORD_INVALID;
336 if (s == "NAK_NOT_FOUND") return CriticalError::NAK_NOT_FOUND;
337 if (s == "NAK_LOADER_INVALID") return CriticalError::NAK_LOADER_INVALID;
338 return std::nullopt;
339}
340
341// ============================================================================
342// TRACE SYSTEM
343// ============================================================================
344
348namespace trace_source {
349 constexpr const char* HOST = "host";
350 constexpr const char* NAK_RECORD = "nak_record";
351 constexpr const char* MANIFEST = "manifest";
352 constexpr const char* INSTALL_RECORD = "install_record";
353 constexpr const char* NAH_STANDARD = "nah_standard";
354}
355
361struct TraceContribution {
362 std::string value;
363 std::string source_kind;
364 std::string source_path;
365 int precedence_rank = 0;
366 EnvOp operation = EnvOp::Set;
367 bool accepted = false;
368};
369
375struct TraceEntry {
376 std::string value;
377 std::string source_kind;
378 std::string source_path;
379 int precedence_rank = 0;
380 std::vector<TraceContribution> history;
381};
382
386struct CompositionTrace {
387 std::unordered_map<std::string, TraceEntry> environment;
388 std::unordered_map<std::string, TraceEntry> library_paths;
389 std::unordered_map<std::string, TraceEntry> arguments;
390 std::vector<std::string> decisions;
391};
392
393// ============================================================================
394// APP DECLARATION
395// ============================================================================
396
397// An asset the app exposes for other apps or the host to use.
398struct AssetExportDecl {
399 std::string id;
400 std::string path;
401 std::string type;
402};
403
404// What the app declares it needs to run.
405//
406// This is typically parsed from nap.json
407// but can be constructed directly. All paths are relative to where the app
408// will be installed.
409//
410// Example - a Lua app:
411//
412// AppDeclaration app;
413// app.id = "com.example.game";
414// app.version = "2.1.0";
415// app.entrypoint_path = "main.lua";
416// app.nak_id = "lua";
417// app.nak_version_req = ">=5.4.0";
418// app.lib_dirs = {"lib", "vendor"};
419// app.environment["GAME_DATA"] = "./data";
420//
421// Example - a standalone binary (no runtime):
422//
423// AppDeclaration app;
424// app.id = "org.tool.converter";
425// app.version = "1.0.0";
426// app.entrypoint_path = "bin/converter";
427// // nak_id left empty - no runtime needed
428//
429struct AppDeclaration {
430 // Required: App identity
431 std::string id;
432 std::string version;
433
434 // Required: What to run
435 std::string entrypoint_path;
436
437 // Optional: Runtime requirements (leave nak_id empty for standalone binaries)
438 std::string nak_id;
439 std::string nak_version_req;
440 std::string nak_loader;
441
442 // Optional: Arguments passed after the entrypoint
443 std::vector<std::string> entrypoint_args;
444
445 // Optional: Environment operations.
446 EnvMap environment;
447
448 // Optional: Library search paths (relative to app root)
449 std::vector<std::string> lib_dirs;
450
451 // Optional: Asset directories and exports
452 std::vector<std::string> asset_dirs;
453 std::vector<AssetExportDecl> asset_exports;
454
455 // Optional: Permission requests
456 std::vector<std::string> permissions_filesystem;
457 std::vector<std::string> permissions_network;
458
459 // Optional: Metadata (informational only, does not affect composition)
460 std::string description;
461 std::string author;
462 std::string license;
463 std::string homepage;
464
465};
466
467// ============================================================================
468// HOST ENVIRONMENT
469// ============================================================================
470
471// Host configuration loaded from host.json.
472//
473// This replaces the old "profiles" concept with a single host configuration.
474// It contains environment variables and library paths.
475//
476// Example:
477//
478// HostEnvironment host_env;
479// host_env.vars["NAH_ENV"] = EnvValue(EnvOp::Set, "production");
480// host_env.vars["LOG_LEVEL"] = EnvValue(EnvOp::Set, "warn");
481// host_env.paths.library_prepend = {"/opt/libs"};
482//
483// Host environment takes precedence over app-declared environment variables.
484//
485struct HostEnvironment {
486 EnvMap vars;
487
488 struct {
489 std::vector<std::string> library_prepend;
490 std::vector<std::string> library_append;
491 } paths;
492
493 std::string source_path;
494};
495
496// ============================================================================
497// LOADER CONFIGURATION
498// ============================================================================
499
500// How a runtime executes app entrypoints.
501//
502// For example, Lua's loader might be:
503// exec_path: "/runtimes/lua/bin/lua"
504// args_template: ["{NAH_APP_ENTRY}"]
505//
506// The args_template supports {VAR} placeholders that are expanded from the
507// environment before execution.
508struct LoaderConfig {
509 std::string exec_path;
510 std::vector<std::string> args_template;
511};
512
513// ============================================================================
514// RUNTIME DESCRIPTOR
515// ============================================================================
516
517// Describes an installed runtime (NAK - Native Application Kit).
518//
519// A NAK is a runtime like Lua, Node, or Python that apps can depend on.
520// The RuntimeDescriptor tells NAH where the runtime is installed and how
521// to use it.
522//
523// Example - Lua 5.4.6:
524//
525// RuntimeDescriptor lua;
526// lua.nak.id = "lua";
527// lua.nak.version = "5.4.6";
528// lua.paths.root = "/runtimes/lua/5.4.6";
529// lua.paths.lib_dirs = {"/runtimes/lua/5.4.6/lib"};
530// lua.environment["LUA_PATH"] = EnvValue(EnvOp::Prepend, "./?.lua", ";");
531// lua.loaders["default"] = {"/runtimes/lua/5.4.6/bin/lua", {"{NAH_APP_ENTRY}"}};
532//
533// NAKs without loaders (libs-only) just provide libraries and environment.
534//
535struct RuntimeDescriptor {
536 struct {
537 std::string id;
538 std::string version;
539 } nak;
540
541 struct {
542 std::string root;
543 std::string resource_root;
544 std::vector<std::string> lib_dirs;
545 } paths;
546
547 // Environment variables provided by this runtime
548 EnvMap environment;
549
550 // Loaders - how this runtime executes apps. Empty for libs-only NAKs.
551 // Key is loader name (use "default" for the primary loader).
552 std::unordered_map<std::string, LoaderConfig> loaders;
553
554 bool has_loaders() const { return !loaders.empty(); }
555
556 struct {
557 bool present = false;
558 std::string cwd;
559 } execution;
560
561 struct {
562 std::string package_hash;
563 std::string installed_at;
564 std::string installed_by;
565 std::string source;
566 } provenance;
567
568 TrustInfo trust;
569
570 std::string source_path;
571};
572
573// ============================================================================
574// INSTALL RECORD
575// ============================================================================
576
577// Records where an app is installed and which runtime version to use.
578//
579// Created at install time, this captures:
580// - Where the app lives on disk (paths.install_root)
581// - Which specific runtime version to use (nak.record_ref)
582// - Trust/verification state
583// - Per-install overrides
584//
585// Example - minimal install record:
586//
587// InstallRecord install;
588// install.install.instance_id = "550e8400-e29b-41d4-a716-446655440000";
589// install.paths.install_root = "/apps/myapp";
590// install.nak.record_ref = "lua@5.4.6.json"; // Key into RuntimeInventory
591//
592// Example - with environment override:
593//
594// InstallRecord install;
595// install.install.instance_id = "...";
596// install.paths.install_root = "/apps/myapp";
597// install.nak.record_ref = "lua@5.4.6.json";
598// install.overrides.environment["DEBUG"] = "1";
599//
600struct InstallRecord {
601 // Unique identifier for this installation
602 struct {
603 std::string instance_id;
604 } install;
605
606 // Snapshot of app info at install time (audit only, does not affect composition)
607 struct {
608 std::string id;
609 std::string version;
610 std::string nak_id;
611 std::string nak_version_req;
612 } app;
613
614 // Which runtime to use - resolved and pinned at install time
615 struct {
616 std::string id;
617 std::string version;
618 std::string record_ref;
619 std::string loader;
620 std::string selection_reason;
621 } nak;
622
623 struct {
624 std::string install_root;
625 } paths;
626
627 struct {
628 std::string package_hash;
629 std::string installed_at;
630 std::string installed_by;
631 std::string source;
632 } provenance;
633
634 TrustInfo trust;
635
636 // Per-install host-owned overrides
637 struct {
638 EnvMap environment;
639 struct {
640 std::vector<std::string> prepend;
641 std::vector<std::string> append;
642 } arguments;
643 struct {
644 std::vector<std::string> library_prepend;
645 std::vector<std::string> library_append;
646 } paths;
647 } overrides;
648
649 std::string source_path;
650};
651
652// ============================================================================
653// RUNTIME INVENTORY
654// ============================================================================
655
656// Collection of available runtimes on the host.
657//
658// Maps record_ref (e.g., "lua@5.4.6.json") to RuntimeDescriptor.
659// The InstallRecord's nak.record_ref is used as the key to look up the runtime.
660//
661// Example:
662//
663// RuntimeInventory inventory;
664// inventory.runtimes["lua@5.4.6.json"] = lua_descriptor;
665// inventory.runtimes["node@20.0.0.json"] = node_descriptor;
666//
667struct RuntimeInventory {
668 std::unordered_map<std::string, RuntimeDescriptor> runtimes;
669};
670
671// ============================================================================
672// ASSET EXPORT
673// ============================================================================
674
675// An exported asset in the contract (paths resolved to absolute).
676struct AssetExport {
677 std::string id;
678 std::string path;
679 std::string type;
680};
681
682// ============================================================================
683// LAUNCH CONTRACT
684// ============================================================================
685
686// The output of nah_compose() - everything needed to launch an application.
687//
688// The contract is self-contained: no additional lookups are needed to execute
689// the app. All paths are absolute, all environment variables are resolved,
690// and the exact binary and arguments are specified.
691//
692// To execute a contract:
693//
694// if (result.ok) {
695// const auto& c = result.contract;
696//
697// // Set environment
698// for (const auto& [key, value] : c.environment) {
699// setenv(key.c_str(), value.c_str(), 1);
700// }
701//
702// // Set library path
703// std::string lib_path;
704// for (const auto& p : c.execution.library_paths) {
705// if (!lib_path.empty()) lib_path += ":";
706// lib_path += p;
707// }
708// setenv(c.execution.library_path_env_key.c_str(), lib_path.c_str(), 1);
709//
710// // Change to working directory
711// chdir(c.execution.cwd.c_str());
712//
713// // Build argv: [binary, ...arguments]
714// std::vector<char*> argv;
715// argv.push_back(const_cast<char*>(c.execution.binary.c_str()));
716// for (const auto& arg : c.execution.arguments) {
717// argv.push_back(const_cast<char*>(arg.c_str()));
718// }
719// argv.push_back(nullptr);
720//
721// // Execute
722// execv(c.execution.binary.c_str(), argv.data());
723// }
724//
725struct LaunchContract {
726 // App identity and paths (all absolute)
727 struct {
728 std::string id;
729 std::string version;
730 std::string root;
731 std::string entrypoint;
732 std::string package_hash;
733 } app;
734
735 // Runtime info (empty if standalone app)
736 struct {
737 std::string id;
738 std::string version;
739 std::string root;
740 std::string resource_root;
741 std::string record_ref;
742 std::string package_hash;
743 } nak;
744
745 // How to execute the app
746 struct {
747 std::string binary;
748 std::vector<std::string> arguments;
749 std::string cwd;
750 std::string library_path_env_key;
751 std::vector<std::string> library_paths;
752 } execution;
753
754 // Complete environment map (ready to pass to exec)
755 std::unordered_map<std::string, std::string> environment;
756
757 // Permission requests for a host launcher to interpret and enforce
758 struct {
759 std::vector<std::string> filesystem;
760 std::vector<std::string> network;
761 } permissions;
762
763 // Aggregate trust for every executable artifact in this contract
764 TrustInfo trust;
765
766 // Exported assets (id -> absolute path)
767 std::unordered_map<std::string, AssetExport> exports;
768
769};
770
771// ============================================================================
772// COMPOSITION OPTIONS
773// ============================================================================
774
775// Options passed to nah_compose().
776struct CompositionOptions {
777 bool enable_trace = false;
778 std::string now;
779 std::string loader_override;
780};
781
782// ============================================================================
783// COMPOSITION RESULT
784// ============================================================================
785
786// The result of calling nah_compose().
787//
788// Check result.ok to see if composition succeeded. If true, result.contract
789// contains the launch specification. If false, check critical_error and
790// critical_error_context for what went wrong.
791//
792// Warnings are always populated (even on success) for non-fatal issues.
793//
794// Example:
795//
796// CompositionResult result = nah_compose(app, host_env, install, inventory);
797// if (!result.ok) {
798// std::cerr << "Composition failed: " << result.critical_error_context << "\n";
799// return 1;
800// }
801// for (const auto& w : result.warnings) {
802// std::cerr << "Warning: " << w.key << "\n";
803// }
804// // Use result.contract...
805//
806struct CompositionResult {
807 bool ok = false;
808 std::optional<CriticalError> critical_error;
809 std::string critical_error_context;
810 LaunchContract contract;
811 std::vector<WarningObject> warnings;
812 std::optional<CompositionTrace> trace;
813};
814
815// ============================================================================
816// PURE FUNCTIONS - Path Utilities
817// ============================================================================
818
825inline bool is_absolute_path(const std::string& path) {
826 if (path.empty()) return false;
827#ifdef _WIN32
828 if (path.size() >= 2) {
829 if (path[1] == ':') return true;
830 if (path[0] == '\\' && path[1] == '\\') return true;
831 }
832#endif
833 return path[0] == '/';
834}
835
839inline std::string normalize_separators(const std::string& path) {
840 std::string result = path;
841 for (char& c : result) {
842 if (c == '\\') c = '/';
843 }
844 return result;
845}
846
853inline bool path_escapes_root(const std::string& root, const std::string& path) {
854 std::string norm_root = normalize_separators(root);
855 std::string norm_path = normalize_separators(path);
856
857 while (!norm_root.empty() && norm_root.back() == '/') {
858 norm_root.pop_back();
859 }
860
861 // Path must start with root
862 if (norm_path.find(norm_root) != 0) {
863 return true;
864 }
865
866 // Path must either be exactly root, or have a / after the root prefix
867 // This prevents /app matching /application
868 std::string rel = norm_path.substr(norm_root.size());
869 if (!rel.empty() && rel[0] != '/') {
870 return true; // e.g., /application doesn't have / after /app
871 }
872 if (!rel.empty() && rel[0] == '/') {
873 rel = rel.substr(1);
874 }
875
876 int depth = 0;
877 size_t pos = 0;
878 while (pos < rel.size()) {
879 size_t next = rel.find('/', pos);
880 std::string component = (next == std::string::npos)
881 ? rel.substr(pos)
882 : rel.substr(pos, next - pos);
883
884 if (component == "..") {
885 depth--;
886 if (depth < 0) return true;
887 } else if (!component.empty() && component != ".") {
888 depth++;
889 }
890
891 if (next == std::string::npos) break;
892 pos = next + 1;
893 }
894
895 return false;
896}
897
901inline std::string join_path(const std::string& base, const std::string& rel) {
902 if (base.empty()) return rel;
903 if (rel.empty()) return base;
904
905 std::string result = base;
906 if (result.back() != '/' && result.back() != '\\') {
907 result += '/';
908 }
909
910 size_t start = 0;
911 while (start < rel.size() && (rel[start] == '/' || rel[start] == '\\')) {
912 start++;
913 }
914
915 result += rel.substr(start);
916 return normalize_separators(result);
917}
918
922inline std::string get_library_path_env_key() {
923#if defined(__APPLE__)
924 return "DYLD_LIBRARY_PATH";
925#elif defined(_WIN32)
926 return "PATH";
927#else
928 return "LD_LIBRARY_PATH";
929#endif
930}
931
935inline char get_path_separator() {
936#ifdef _WIN32
937 return ';';
938#else
939 return ':';
940#endif
941}
942
943// ============================================================================
944// PURE FUNCTIONS - Validation
945// ============================================================================
946
950struct ValidationResult {
951 bool ok = true;
952 std::vector<std::string> errors;
953 std::vector<std::string> warnings;
954};
955
965inline ValidationResult validate_declaration(const AppDeclaration& decl) {
966 ValidationResult result;
967
968 if (decl.id.empty()) {
969 result.ok = false;
970 result.errors.push_back("app.id is required");
971 }
972
973 if (decl.version.empty()) {
974 result.ok = false;
975 result.errors.push_back("app.version is required");
976 }
977
978 if (decl.entrypoint_path.empty()) {
979 result.ok = false;
980 result.errors.push_back("entrypoint_path is required");
981 }
982
983 if (!decl.entrypoint_path.empty() && is_absolute_path(decl.entrypoint_path)) {
984 result.ok = false;
985 result.errors.push_back("entrypoint_path must be relative");
986 }
987
988 for (const auto& lib_dir : decl.lib_dirs) {
989 if (is_absolute_path(lib_dir)) {
990 result.ok = false;
991 result.errors.push_back("lib_dir must be relative: " + lib_dir);
992 }
993 }
994
995 for (const auto& exp : decl.asset_exports) {
996 if (is_absolute_path(exp.path)) {
997 result.ok = false;
998 result.errors.push_back("asset_export path must be relative: " + exp.path);
999 }
1000 }
1001
1002 if (!decl.nak_id.empty() && decl.nak_version_req.empty()) {
1003 result.warnings.push_back("nak_id specified but nak_version_req is empty");
1004 }
1005
1006 return result;
1007}
1008
1012inline ValidationResult validate_install_record(const InstallRecord& record) {
1013 ValidationResult result;
1014
1015 if (record.install.instance_id.empty()) {
1016 result.ok = false;
1017 result.errors.push_back("install.instance_id is required");
1018 }
1019
1020 if (record.paths.install_root.empty()) {
1021 result.ok = false;
1022 result.errors.push_back("paths.install_root is required");
1023 }
1024
1025 if (!record.paths.install_root.empty() && !is_absolute_path(record.paths.install_root)) {
1026 result.ok = false;
1027 result.errors.push_back("paths.install_root must be absolute");
1028 }
1029
1030 return result;
1031}
1032
1036inline ValidationResult validate_runtime(const RuntimeDescriptor& runtime) {
1037 ValidationResult result;
1038
1039 if (runtime.nak.id.empty()) {
1040 result.ok = false;
1041 result.errors.push_back("nak.id is required");
1042 }
1043
1044 if (runtime.nak.version.empty()) {
1045 result.ok = false;
1046 result.errors.push_back("nak.version is required");
1047 }
1048
1049 if (runtime.paths.root.empty()) {
1050 result.ok = false;
1051 result.errors.push_back("paths.root is required");
1052 }
1053
1054 if (!runtime.paths.root.empty() && !is_absolute_path(runtime.paths.root)) {
1055 result.ok = false;
1056 result.errors.push_back("paths.root must be absolute");
1057 }
1058
1059 if (!runtime.paths.resource_root.empty() &&
1060 (!is_absolute_path(runtime.paths.resource_root) ||
1061 path_escapes_root(runtime.paths.root, runtime.paths.resource_root))) {
1062 result.ok = false;
1063 result.errors.push_back("paths.resource_root must be within paths.root");
1064 }
1065
1066 for (const auto& lib_dir : runtime.paths.lib_dirs) {
1067 if (!is_absolute_path(lib_dir) || path_escapes_root(runtime.paths.root, lib_dir)) {
1068 result.ok = false;
1069 result.errors.push_back("lib_dir must be within paths.root: " + lib_dir);
1070 }
1071 }
1072
1073 for (const auto& [name, loader] : runtime.loaders) {
1074 if (loader.exec_path.empty() || !is_absolute_path(loader.exec_path) ||
1075 path_escapes_root(runtime.paths.root, loader.exec_path)) {
1076 result.ok = false;
1077 result.errors.push_back("loader exec_path must be within paths.root: " + name);
1078 }
1079 }
1080
1081 return result;
1082}
1083
1084
1085
1086// ============================================================================
1087// PURE FUNCTIONS - Environment
1088// ============================================================================
1089
1095inline std::optional<std::string> apply_env_op(
1096 const std::string& key,
1097 const EnvValue& env_val,
1098 const std::unordered_map<std::string, std::string>& current_env)
1099{
1100 switch (env_val.op) {
1101 case EnvOp::Set:
1102 return env_val.value;
1103
1104 case EnvOp::Prepend: {
1105 auto it = current_env.find(key);
1106 if (it != current_env.end() && !it->second.empty()) {
1107 return env_val.value + env_val.separator + it->second;
1108 }
1109 return env_val.value;
1110 }
1111
1112 case EnvOp::Append: {
1113 auto it = current_env.find(key);
1114 if (it != current_env.end() && !it->second.empty()) {
1115 return it->second + env_val.separator + env_val.value;
1116 }
1117 return env_val.value;
1118 }
1119
1120 case EnvOp::Unset:
1121 return std::nullopt;
1122 }
1123
1124 return env_val.value;
1125}
1126
1127// ============================================================================
1128// PURE FUNCTIONS - Placeholder Expansion
1129// ============================================================================
1130
1134struct ExpansionResult {
1135 bool ok = true;
1136 std::string value;
1137 std::string error;
1138};
1139
1146inline ExpansionResult expand_placeholders(
1147 const std::string& input,
1148 const std::unordered_map<std::string, std::string>& env)
1149{
1150 ExpansionResult result;
1151 result.value.reserve(input.size());
1152
1153 size_t placeholder_count = 0;
1154 size_t i = 0;
1155
1156 while (i < input.size()) {
1157 if (input[i] == '{') {
1158 size_t end = input.find('}', i + 1);
1159 if (end != std::string::npos) {
1160 std::string var_name = input.substr(i + 1, end - i - 1);
1161
1162 placeholder_count++;
1163 if (placeholder_count > MAX_PLACEHOLDERS) {
1164 result.ok = false;
1165 result.error = "placeholder_limit";
1166 return result;
1167 }
1168
1169 auto it = env.find(var_name);
1170 if (it == env.end()) {
1171 result.ok = false;
1172 result.error = "missing_placeholder:" + var_name;
1173 return result;
1174 }
1175 result.value += it->second;
1176
1177 if (result.value.size() > MAX_EXPANDED_SIZE) {
1178 result.ok = false;
1179 result.error = "expansion_overflow";
1180 return result;
1181 }
1182
1183 i = end + 1;
1184 continue;
1185 }
1186 }
1187
1188 result.value += input[i];
1189 i++;
1190
1191 if (result.value.size() > MAX_EXPANDED_SIZE) {
1192 result.ok = false;
1193 result.error = "expansion_overflow";
1194 return result;
1195 }
1196 }
1197
1198 return result;
1199}
1200
1204struct ExpansionListResult {
1205 bool ok = true;
1206 std::vector<std::string> values;
1207 std::string error;
1208};
1209
1210inline ExpansionListResult expand_string_vector(
1211 const std::vector<std::string>& inputs,
1212 const std::unordered_map<std::string, std::string>& env)
1213{
1214 ExpansionListResult result;
1215 result.values.reserve(inputs.size());
1216
1217 for (const auto& input : inputs) {
1218 auto expanded = expand_placeholders(input, env);
1219 if (!expanded.ok) {
1220 result.ok = false;
1221 result.error = expanded.error;
1222 return result;
1223 }
1224 result.values.push_back(std::move(expanded.value));
1225 }
1226
1227 return result;
1228}
1229
1230// ============================================================================
1231// PURE FUNCTIONS - Runtime Resolution
1232// ============================================================================
1233
1237struct RuntimeResolutionResult {
1238 bool resolved = false;
1239 std::string record_ref;
1240 RuntimeDescriptor runtime;
1241 std::string selection_reason;
1242 std::vector<std::string> warnings;
1243};
1244
1250inline RuntimeResolutionResult resolve_runtime(
1251 const AppDeclaration& app,
1252 const InstallRecord& install,
1253 const RuntimeInventory& inventory)
1254{
1255 RuntimeResolutionResult result;
1256
1257 // Standalone apps don't need runtime resolution
1258 if (app.nak_id.empty()) {
1259 result.resolved = true;
1260 result.selection_reason = "standalone_app";
1261 return result;
1262 }
1263
1264 // Get record_ref from install record
1265 std::string record_ref = install.nak.record_ref;
1266
1267 if (record_ref.empty()) {
1268 result.warnings.push_back("nak.record_ref is empty in install record");
1269 return result;
1270 }
1271
1272 auto it = inventory.runtimes.find(record_ref);
1273 if (it == inventory.runtimes.end()) {
1274 result.warnings.push_back("NAK not found in inventory: " + record_ref);
1275 return result;
1276 }
1277
1278 if (it->second.nak.id != app.nak_id ||
1279 (!install.nak.id.empty() && install.nak.id != it->second.nak.id) ||
1280 (!install.nak.version.empty() && install.nak.version != it->second.nak.version)) {
1281 result.warnings.push_back("pinned NAK identity does not match app or install record");
1282 return result;
1283 }
1284
1285 result.resolved = true;
1286 result.record_ref = record_ref;
1287 result.runtime = it->second;
1288 result.selection_reason = "pinned_from_install_record";
1289
1290 return result;
1291}
1292
1293// ============================================================================
1294// PURE FUNCTIONS - Path Binding
1295// ============================================================================
1296
1300struct PathBindingResult {
1301 bool ok = true;
1302 std::string entrypoint;
1303 std::vector<std::string> library_paths;
1304 std::unordered_map<std::string, AssetExport> exports;
1305 std::vector<std::string> errors;
1306};
1307
1311inline PathBindingResult bind_paths(
1312 const AppDeclaration& decl,
1313 const InstallRecord& install,
1314 const RuntimeDescriptor* runtime,
1315 const HostEnvironment& host_env)
1316{
1317 PathBindingResult result;
1318 const std::string& app_root = install.paths.install_root;
1319
1320 // Entrypoint
1321 std::string entrypoint = join_path(app_root, decl.entrypoint_path);
1322 if (path_escapes_root(app_root, entrypoint)) {
1323 result.ok = false;
1324 result.errors.push_back("entrypoint escapes app root");
1325 return result;
1326 }
1327 result.entrypoint = entrypoint;
1328
1329 // Library paths in order: host prepend, install overrides, NAK, app, host append
1330 for (const auto& path : host_env.paths.library_prepend) {
1331 if (is_absolute_path(path)) {
1332 result.library_paths.push_back(path);
1333 }
1334 }
1335
1336 for (const auto& path : install.overrides.paths.library_prepend) {
1337 if (is_absolute_path(path)) {
1338 result.library_paths.push_back(path);
1339 }
1340 }
1341
1342 if (runtime) {
1343 for (const auto& lib_dir : runtime->paths.lib_dirs) {
1344 result.library_paths.push_back(lib_dir);
1345 }
1346 }
1347
1348 for (const auto& lib_dir : decl.lib_dirs) {
1349 std::string abs_lib = join_path(app_root, lib_dir);
1350 if (path_escapes_root(app_root, abs_lib)) {
1351 result.ok = false;
1352 result.errors.push_back("lib_dir escapes app root: " + lib_dir);
1353 return result;
1354 }
1355 result.library_paths.push_back(abs_lib);
1356 }
1357
1358 for (const auto& path : host_env.paths.library_append) {
1359 if (is_absolute_path(path)) {
1360 result.library_paths.push_back(path);
1361 }
1362 }
1363
1364 for (const auto& path : install.overrides.paths.library_append) {
1365 if (is_absolute_path(path)) {
1366 result.library_paths.push_back(path);
1367 }
1368 }
1369
1370 // Asset exports
1371 for (const auto& exp : decl.asset_exports) {
1372 std::string abs_path = join_path(app_root, exp.path);
1373 if (path_escapes_root(app_root, abs_path)) {
1374 result.ok = false;
1375 result.errors.push_back("asset export escapes app root: " + exp.id);
1376 return result;
1377 }
1378 result.exports[exp.id] = {exp.id, abs_path, exp.type};
1379 }
1380
1381 return result;
1382}
1383
1384// ============================================================================
1385// PURE FUNCTIONS - Environment Composition
1386// ============================================================================
1387
1394inline std::unordered_map<std::string, std::string> compose_environment(
1395 const AppDeclaration& decl,
1396 const InstallRecord& install,
1397 const RuntimeDescriptor* runtime,
1398 const HostEnvironment& host_env,
1399 const LaunchContract& contract,
1400 CompositionTrace* trace = nullptr)
1401{
1402 std::unordered_map<std::string, std::string> env;
1403
1404 auto record = [&](const std::string& key, const std::string& value,
1405 const std::string& kind, const std::string& path,
1406 int rank, EnvOp op, bool accepted) {
1407 if (trace) {
1408 TraceContribution contrib;
1409 contrib.value = value;
1410 contrib.source_kind = kind;
1411 contrib.source_path = path;
1412 contrib.precedence_rank = rank;
1413 contrib.operation = op;
1414 contrib.accepted = accepted;
1415 trace->environment[key].history.push_back(contrib);
1416 }
1417 };
1418
1419 // Layer 1: App environment (rank 5).
1420 for (const auto& [key, val] : decl.environment) {
1421 auto result = apply_env_op(key, val, env);
1422 if (result.has_value()) {
1423 env[key] = *result;
1424 record(key, *result, trace_source::MANIFEST, "manifest", 5, val.op, true);
1425 } else {
1426 env.erase(key);
1427 record(key, "", trace_source::MANIFEST, "manifest", 5, val.op, true);
1428 }
1429 }
1430
1431 // Layer 2: NAK environment (rank 4).
1432 if (runtime) {
1433 for (const auto& [key, val] : runtime->environment) {
1434 auto result = apply_env_op(key, val, env);
1435 if (result.has_value()) {
1436 env[key] = *result;
1437 record(key, *result, trace_source::NAK_RECORD, runtime->source_path, 4, val.op, true);
1438 } else {
1439 env.erase(key);
1440 record(key, "", trace_source::NAK_RECORD, runtime->source_path, 4, val.op, true);
1441 }
1442 }
1443 }
1444
1445 // Layer 3: Host environment (rank 3).
1446 for (const auto& [key, val] : host_env.vars) {
1447 auto result = apply_env_op(key, val, env);
1448 if (result.has_value()) {
1449 env[key] = *result;
1450 record(key, *result, trace_source::HOST, host_env.source_path, 3, val.op, true);
1451 } else {
1452 env.erase(key);
1453 record(key, "", trace_source::HOST, host_env.source_path, 3, val.op, true);
1454 }
1455 }
1456
1457 // Layer 4: Install record overrides (rank 2)
1458 for (const auto& [key, val] : install.overrides.environment) {
1459 auto result = apply_env_op(key, val, env);
1460 if (result.has_value()) {
1461 env[key] = *result;
1462 record(key, *result, trace_source::INSTALL_RECORD, install.source_path, 2, val.op, true);
1463 } else {
1464 env.erase(key);
1465 record(key, "", trace_source::INSTALL_RECORD, install.source_path, 2, val.op, true);
1466 }
1467 }
1468
1469 // Layer 5: NAH standard variables (rank 1, always set)
1470 env["NAH_APP_ID"] = contract.app.id;
1471 record("NAH_APP_ID", contract.app.id, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1472
1473 env["NAH_APP_VERSION"] = contract.app.version;
1474 record("NAH_APP_VERSION", contract.app.version, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1475
1476 env["NAH_APP_ROOT"] = contract.app.root;
1477 record("NAH_APP_ROOT", contract.app.root, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1478
1479 env["NAH_APP_ENTRY"] = contract.app.entrypoint;
1480 record("NAH_APP_ENTRY", contract.app.entrypoint, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1481
1482 if (runtime) {
1483 env["NAH_NAK_ID"] = runtime->nak.id;
1484 record("NAH_NAK_ID", runtime->nak.id, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1485
1486 env["NAH_NAK_VERSION"] = runtime->nak.version;
1487 record("NAH_NAK_VERSION", runtime->nak.version, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1488
1489 env["NAH_NAK_ROOT"] = runtime->paths.root;
1490 record("NAH_NAK_ROOT", runtime->paths.root, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1491 }
1492
1493 return env;
1494}
1495
1496// ============================================================================
1497// PURE FUNCTIONS - Timestamp Comparison
1498// ============================================================================
1499
1505inline std::string normalize_rfc3339(const std::string& ts) {
1506 if (ts.empty()) return ts;
1507
1508 std::string result = ts;
1509 if (result.size() >= 6) {
1510 std::string suffix = result.substr(result.size() - 6);
1511 if (suffix == "+00:00" || suffix == "-00:00") {
1512 result = result.substr(0, result.size() - 6) + "Z";
1513 }
1514 }
1515
1516 return result;
1517}
1518
1524inline bool timestamp_before(const std::string& a, const std::string& b) {
1525 return normalize_rfc3339(a) < normalize_rfc3339(b);
1526}
1527
1528// ============================================================================
1529// MAIN COMPOSITION FUNCTION
1530// ============================================================================
1531
1532// Compose a launch contract from app declaration and host state.
1533//
1534// This is the main entry point. Given:
1535// - app: What the application declares it needs
1536// - host_env: Host-provided environment variables
1537// - install: Where the app is installed and which runtime to use
1538// - inventory: Available runtimes on the host
1539//
1540// Returns a CompositionResult. Check result.ok - if true, result.contract
1541// contains everything needed to launch the application.
1542//
1543// This function is pure: no I/O, no syscalls, no side effects. Same inputs
1544// always produce the same output. This makes it safe to call from any context
1545// and easy to test.
1546//
1547// Example:
1548//
1549// AppDeclaration app;
1550// app.id = "com.example.game";
1551// app.version = "1.0.0";
1552// app.entrypoint_path = "main.lua";
1553// app.nak_id = "lua";
1554//
1555// InstallRecord install;
1556// install.install.instance_id = "abc123";
1557// install.paths.install_root = "/apps/game";
1558// install.nak.record_ref = "lua@5.4.6.json";
1559//
1560// HostEnvironment host_env; // Empty = no host overrides
1561//
1562// RuntimeInventory inventory;
1563// inventory.runtimes["lua@5.4.6.json"] = lua_runtime;
1564//
1565// auto result = nah_compose(app, host_env, install, inventory);
1566// if (result.ok) {
1567// // result.contract.execution.binary = "/runtimes/lua/bin/lua"
1568// // result.contract.execution.arguments = ["/apps/game/main.lua"]
1569// // result.contract.environment = {"LUA_PATH": "...", "NAH_APP_ID": "com.example.game", ...}
1570// }
1571//
1572inline CompositionResult nah_compose(
1573 const AppDeclaration& app,
1574 const HostEnvironment& host_env,
1575 const InstallRecord& install,
1576 const RuntimeInventory& inventory,
1577 const CompositionOptions& options = {})
1578{
1579 CompositionResult result;
1580
1581 // Initialize trace if enabled
1582 CompositionTrace* trace_ptr = nullptr;
1583 if (options.enable_trace) {
1584 result.trace = CompositionTrace{};
1585 trace_ptr = &(*result.trace);
1586 trace_ptr->decisions.push_back("Starting composition");
1587 }
1588
1589 // Validate declaration
1590 auto decl_valid = validate_declaration(app);
1591 if (!decl_valid.ok) {
1592 result.critical_error = CriticalError::MANIFEST_MISSING;
1593 result.critical_error_context = decl_valid.errors.empty() ?
1594 "invalid declaration" : decl_valid.errors[0];
1595 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Declaration validation failed");
1596 return result;
1597 }
1598 if (trace_ptr) trace_ptr->decisions.push_back("Declaration validated");
1599
1600 // Validate install record
1601 auto install_valid = validate_install_record(install);
1602 if ((!install.app.id.empty() && install.app.id != app.id) ||
1603 (!install.app.version.empty() && install.app.version != app.version)) {
1604 install_valid.ok = false;
1605 install_valid.errors.push_back("install record app identity does not match the manifest");
1606 }
1607 if (!install_valid.ok) {
1608 result.critical_error = CriticalError::INSTALL_RECORD_INVALID;
1609 result.critical_error_context = install_valid.errors.empty() ?
1610 "invalid install record" : install_valid.errors[0];
1611 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Install record validation failed");
1612 return result;
1613 }
1614 if (trace_ptr) trace_ptr->decisions.push_back("Install record validated");
1615
1616 // Resolve runtime
1617 auto runtime_result = resolve_runtime(app, install, inventory);
1618 RuntimeDescriptor* runtime_ptr = runtime_result.resolved && !runtime_result.runtime.nak.id.empty()
1619 ? &runtime_result.runtime : nullptr;
1620
1621 if (!app.nak_id.empty() && !runtime_ptr) {
1622 result.critical_error = CriticalError::NAK_NOT_FOUND;
1623 result.critical_error_context = runtime_result.warnings.empty()
1624 ? "required pinned NAK is unavailable"
1625 : runtime_result.warnings.front();
1626 return result;
1627 }
1628
1629 if (trace_ptr) {
1630 if (runtime_ptr) {
1631 trace_ptr->decisions.push_back("Runtime resolved: " + runtime_ptr->nak.id + "@" + runtime_ptr->nak.version);
1632 } else if (app.nak_id.empty()) {
1633 trace_ptr->decisions.push_back("Standalone app (no runtime)");
1634 } else {
1635 trace_ptr->decisions.push_back("Runtime not found");
1636 }
1637 }
1638
1639 // Validate runtime if present
1640 if (runtime_ptr) {
1641 auto runtime_valid = validate_runtime(*runtime_ptr);
1642 if (!runtime_valid.ok) {
1643 result.critical_error = CriticalError::PATH_TRAVERSAL;
1644 result.critical_error_context = runtime_valid.errors.empty() ?
1645 "invalid runtime" : runtime_valid.errors[0];
1646 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Runtime validation failed");
1647 return result;
1648 }
1649 }
1650
1651 // Populate basic contract fields
1652 LaunchContract& contract = result.contract;
1653
1654 contract.app.id = app.id;
1655 contract.app.version = app.version;
1656 contract.app.root = install.paths.install_root;
1657 contract.app.package_hash = install.provenance.package_hash;
1658
1659 if (runtime_ptr) {
1660 contract.nak.id = runtime_ptr->nak.id;
1661 contract.nak.version = runtime_ptr->nak.version;
1662 contract.nak.root = runtime_ptr->paths.root;
1663 contract.nak.resource_root = runtime_ptr->paths.resource_root.empty() ?
1664 runtime_ptr->paths.root : runtime_ptr->paths.resource_root;
1665 contract.nak.record_ref = runtime_result.record_ref;
1666 contract.nak.package_hash = runtime_ptr->provenance.package_hash;
1667 }
1668
1669 // Bind paths
1670 auto paths = bind_paths(app, install, runtime_ptr, host_env);
1671 if (!paths.ok) {
1672 result.critical_error = CriticalError::PATH_TRAVERSAL;
1673 result.critical_error_context = paths.errors.empty() ?
1674 "path binding failed" : paths.errors.front();
1675 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Path binding failed");
1676 return result;
1677 }
1678
1679 contract.app.entrypoint = paths.entrypoint;
1680 contract.exports = paths.exports;
1681 if (trace_ptr) trace_ptr->decisions.push_back("Paths bound successfully");
1682
1683 // Compose environment
1684 auto env = compose_environment(app, install, runtime_ptr, host_env, contract, trace_ptr);
1685 const auto unexpanded_env = env;
1686 for (auto& [key, value] : env) {
1687 auto expanded = expand_placeholders(value, unexpanded_env);
1688 if (!expanded.ok) {
1689 result.critical_error = CriticalError::EXPANSION_FAILED;
1690 result.critical_error_context = "environment " + key + ": " + expanded.error;
1691 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: " + result.critical_error_context);
1692 return result;
1693 }
1694 value = std::move(expanded.value);
1695 }
1696 contract.environment = env;
1697
1698 auto fail_expansion = [&](const std::string& context, const std::string& error) {
1699 result.critical_error = CriticalError::EXPANSION_FAILED;
1700 result.critical_error_context = context + ": " + error;
1701 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: " + result.critical_error_context);
1702 };
1703
1704 // Determine execution binary and arguments
1705 std::string pinned_loader = install.nak.loader;
1706
1707 // Override loader if specified in options
1708 if (!options.loader_override.empty()) {
1709 pinned_loader = options.loader_override;
1710 if (trace_ptr) trace_ptr->decisions.push_back("Loader override requested: " + pinned_loader);
1711 }
1712
1713 if (runtime_ptr && runtime_ptr->has_loaders()) {
1714 std::string effective_loader = pinned_loader;
1715
1716 if (effective_loader.empty()) {
1717 if (runtime_ptr->loaders.count("default")) {
1718 effective_loader = "default";
1719 if (trace_ptr) trace_ptr->decisions.push_back("Auto-selected 'default' loader");
1720 } else if (runtime_ptr->loaders.size() == 1) {
1721 effective_loader = runtime_ptr->loaders.begin()->first;
1722 if (trace_ptr) trace_ptr->decisions.push_back("Auto-selected single loader: " + effective_loader);
1723 } else {
1724 result.critical_error = CriticalError::NAK_LOADER_INVALID;
1725 result.critical_error_context = "multiple NAK loaders are available but none was selected";
1726 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: No NAK loader selected");
1727 return result;
1728 }
1729 } else {
1730 if (trace_ptr) trace_ptr->decisions.push_back("Using pinned loader: " + effective_loader);
1731 }
1732
1733 if (!effective_loader.empty()) {
1734 auto it = runtime_ptr->loaders.find(effective_loader);
1735 if (it == runtime_ptr->loaders.end()) {
1736 result.critical_error = CriticalError::NAK_LOADER_INVALID;
1737 result.critical_error_context = "loader not found: " + effective_loader;
1738 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Loader not found");
1739 return result;
1740 }
1741
1742 contract.execution.binary = it->second.exec_path;
1743 auto expanded = expand_string_vector(it->second.args_template, env);
1744 if (!expanded.ok) {
1745 fail_expansion("loader arguments", expanded.error);
1746 return result;
1747 }
1748 contract.execution.arguments = std::move(expanded.values);
1749 }
1750 } else {
1751 contract.execution.binary = contract.app.entrypoint;
1752 if (trace_ptr) trace_ptr->decisions.push_back("Using app entrypoint as binary");
1753 }
1754
1755 // Apply argument overrides
1756 auto expanded_prepend = expand_string_vector(install.overrides.arguments.prepend, env);
1757 if (!expanded_prepend.ok) {
1758 fail_expansion("prepended arguments", expanded_prepend.error);
1759 return result;
1760 }
1761 contract.execution.arguments.insert(
1762 contract.execution.arguments.begin(),
1763 expanded_prepend.values.begin(),
1764 expanded_prepend.values.end());
1765
1766 auto expanded_entry_args = expand_string_vector(app.entrypoint_args, env);
1767 if (!expanded_entry_args.ok) {
1768 fail_expansion("app arguments", expanded_entry_args.error);
1769 return result;
1770 }
1771 contract.execution.arguments.insert(
1772 contract.execution.arguments.end(),
1773 expanded_entry_args.values.begin(),
1774 expanded_entry_args.values.end());
1775
1776 auto expanded_append = expand_string_vector(install.overrides.arguments.append, env);
1777 if (!expanded_append.ok) {
1778 fail_expansion("appended arguments", expanded_append.error);
1779 return result;
1780 }
1781 contract.execution.arguments.insert(
1782 contract.execution.arguments.end(),
1783 expanded_append.values.begin(),
1784 expanded_append.values.end());
1785
1786 // Determine cwd
1787 if (runtime_ptr && runtime_ptr->execution.present && !runtime_ptr->execution.cwd.empty()) {
1788 auto cwd_expanded = expand_placeholders(runtime_ptr->execution.cwd, env);
1789 if (!cwd_expanded.ok) {
1790 fail_expansion("working directory", cwd_expanded.error);
1791 return result;
1792 }
1793 if (is_absolute_path(cwd_expanded.value)) {
1794 contract.execution.cwd = cwd_expanded.value;
1795 } else {
1796 contract.execution.cwd = join_path(runtime_ptr->paths.root, cwd_expanded.value);
1797 }
1798 if (path_escapes_root(runtime_ptr->paths.root, contract.execution.cwd) &&
1799 path_escapes_root(contract.app.root, contract.execution.cwd)) {
1800 result.critical_error = CriticalError::PATH_TRAVERSAL;
1801 result.critical_error_context = "working directory escapes app and runtime roots";
1802 return result;
1803 }
1804 } else {
1805 contract.execution.cwd = contract.app.root;
1806 }
1807
1808 // Library paths
1809 contract.execution.library_path_env_key = get_library_path_env_key();
1810 contract.execution.library_paths = paths.library_paths;
1811
1812 contract.permissions.filesystem = app.permissions_filesystem;
1813 contract.permissions.network = app.permissions_network;
1814
1815 // Trust. A runtime participates in the executable trust chain, so the
1816 // aggregate can only be verified when both app and runtime are verified.
1817 contract.trust = install.trust;
1818 if (runtime_ptr) {
1819 const auto app_state = install.trust.state;
1820 const auto runtime_state = runtime_ptr->trust.state;
1821 contract.trust.source = "nah.artifact-chain";
1822 contract.trust.details["app_state"] = trust_state_to_string(app_state);
1823 contract.trust.details["runtime_state"] = trust_state_to_string(runtime_state);
1824 contract.trust.details["app_digest"] = install.provenance.package_hash;
1825 contract.trust.details["runtime_digest"] = runtime_ptr->provenance.package_hash;
1826 if (app_state == TrustState::Failed || runtime_state == TrustState::Failed) {
1827 contract.trust.state = TrustState::Failed;
1828 } else if (app_state == TrustState::Unknown || runtime_state == TrustState::Unknown) {
1829 contract.trust.state = TrustState::Unknown;
1830 } else if (app_state == TrustState::Unverified || runtime_state == TrustState::Unverified) {
1831 contract.trust.state = TrustState::Unverified;
1832 } else {
1833 contract.trust.state = TrustState::Verified;
1834 }
1835 if (contract.trust.expires_at.empty() ||
1836 (!runtime_ptr->trust.expires_at.empty() &&
1837 timestamp_before(runtime_ptr->trust.expires_at, contract.trust.expires_at))) {
1838 contract.trust.expires_at = runtime_ptr->trust.expires_at;
1839 }
1840 }
1841
1842 if (contract.trust.source.empty() && contract.trust.evaluated_at.empty()) {
1843 contract.trust.state = TrustState::Unknown;
1844 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), {}});
1845 } else {
1846 switch (contract.trust.state) {
1847 case TrustState::Verified:
1848 break;
1849 case TrustState::Unverified:
1850 result.warnings.push_back({warning_to_string(Warning::trust_state_unverified), {}});
1851 break;
1852 case TrustState::Failed:
1853 result.warnings.push_back({warning_to_string(Warning::trust_state_failed), {}});
1854 break;
1855 case TrustState::Unknown:
1856 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), {}});
1857 break;
1858 }
1859 }
1860
1861 // Check trust staleness
1862 if (!contract.trust.expires_at.empty() && !options.now.empty()) {
1863 if (timestamp_before(contract.trust.expires_at, options.now)) {
1864 result.warnings.push_back({warning_to_string(Warning::trust_state_stale), {}});
1865 if (trace_ptr) trace_ptr->decisions.push_back("WARNING: Trust verification has expired");
1866 }
1867 }
1868
1869 if (trace_ptr) trace_ptr->decisions.push_back("Composition completed successfully");
1870
1871 result.ok = true;
1872 return result;
1873}
1874
1875// ============================================================================
1876// JSON SERIALIZATION (Pure, No External Dependencies)
1877// ============================================================================
1878
1879namespace json {
1880
1884inline std::string escape(const std::string& s) {
1885 std::string result;
1886 result.reserve(s.size() + 16);
1887 for (char c : s) {
1888 switch (c) {
1889 case '"': result += "\\\""; break;
1890 case '\\': result += "\\\\"; break;
1891 case '\b': result += "\\b"; break;
1892 case '\f': result += "\\f"; break;
1893 case '\n': result += "\\n"; break;
1894 case '\r': result += "\\r"; break;
1895 case '\t': result += "\\t"; break;
1896 default:
1897 if (static_cast<unsigned char>(c) < 0x20) {
1898 char buf[8];
1899 snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
1900 result += buf;
1901 } else {
1902 result += c;
1903 }
1904 }
1905 }
1906 return result;
1907}
1908
1912inline std::string str(const std::string& s) {
1913 return "\"" + escape(s) + "\"";
1914}
1915
1919inline std::string object(const std::unordered_map<std::string, std::string>& m, size_t indent = 0) {
1920 if (m.empty()) return "{}";
1921
1922 std::vector<std::string> keys;
1923 for (const auto& [k, _] : m) keys.push_back(k);
1924 std::sort(keys.begin(), keys.end());
1925
1926 std::string pad(indent + 2, ' ');
1927 std::string result = "{\n";
1928 for (size_t i = 0; i < keys.size(); i++) {
1929 result += pad + str(keys[i]) + ": " + str(m.at(keys[i]));
1930 if (i < keys.size() - 1) result += ",";
1931 result += "\n";
1932 }
1933 result += std::string(indent, ' ') + "}";
1934 return result;
1935}
1936
1940inline std::string array(const std::vector<std::string>& v, size_t indent = 0) {
1941 if (v.empty()) return "[]";
1942
1943 std::string pad(indent + 2, ' ');
1944 std::string result = "[\n";
1945 for (size_t i = 0; i < v.size(); i++) {
1946 result += pad + str(v[i]);
1947 if (i < v.size() - 1) result += ",";
1948 result += "\n";
1949 }
1950 result += std::string(indent, ' ') + "]";
1951 return result;
1952}
1953
1954} // namespace json
1955
1961inline std::string serialize_contract(const LaunchContract& c) {
1962 std::ostringstream out;
1963 out << "{\n";
1964 out << " \"schema\": \"" << NAH_CONTRACT_SCHEMA << "\",\n";
1965
1966 // app
1967 out << " \"app\": {\n";
1968 out << " \"id\": " << json::str(c.app.id) << ",\n";
1969 out << " \"version\": " << json::str(c.app.version) << ",\n";
1970 out << " \"root\": " << json::str(c.app.root) << ",\n";
1971 out << " \"entrypoint\": " << json::str(c.app.entrypoint) << ",\n";
1972 out << " \"package_hash\": " << json::str(c.app.package_hash) << "\n";
1973 out << " },\n";
1974
1975 // nak
1976 out << " \"nak\": {\n";
1977 out << " \"id\": " << json::str(c.nak.id) << ",\n";
1978 out << " \"version\": " << json::str(c.nak.version) << ",\n";
1979 out << " \"root\": " << json::str(c.nak.root) << ",\n";
1980 out << " \"resource_root\": " << json::str(c.nak.resource_root) << ",\n";
1981 out << " \"record_ref\": " << json::str(c.nak.record_ref) << ",\n";
1982 out << " \"package_hash\": " << json::str(c.nak.package_hash) << "\n";
1983 out << " },\n";
1984
1985 // execution
1986 out << " \"execution\": {\n";
1987 out << " \"binary\": " << json::str(c.execution.binary) << ",\n";
1988 out << " \"arguments\": " << json::array(c.execution.arguments, 4) << ",\n";
1989 out << " \"cwd\": " << json::str(c.execution.cwd) << ",\n";
1990 out << " \"library_path_env_key\": " << json::str(c.execution.library_path_env_key) << ",\n";
1991 out << " \"library_paths\": " << json::array(c.execution.library_paths, 4) << "\n";
1992 out << " },\n";
1993
1994 // environment
1995 out << " \"environment\": " << json::object(c.environment, 2) << ",\n";
1996
1997 // permission requests
1998 out << " \"permissions\": {\n";
1999 out << " \"filesystem\": " << json::array(c.permissions.filesystem, 4) << ",\n";
2000 out << " \"network\": " << json::array(c.permissions.network, 4) << "\n";
2001 out << " },\n";
2002
2003 // trust
2004 out << " \"trust\": {\n";
2005 out << " \"state\": " << json::str(trust_state_to_string(c.trust.state)) << ",\n";
2006 out << " \"source\": " << json::str(c.trust.source) << ",\n";
2007 out << " \"evaluated_at\": " << json::str(c.trust.evaluated_at) << ",\n";
2008 out << " \"expires_at\": " << json::str(c.trust.expires_at) << "\n";
2009 out << " }\n";
2010
2011 out << "}";
2012 return out.str();
2013}
2014
2018inline std::string serialize_result(const CompositionResult& r) {
2019 std::ostringstream out;
2020 out << "{\n";
2021 out << " \"ok\": " << (r.ok ? "true" : "false") << ",\n";
2022
2023 if (r.critical_error.has_value()) {
2024 out << " \"critical_error\": " << json::str(critical_error_to_string(*r.critical_error)) << ",\n";
2025 out << " \"critical_error_context\": " << json::str(r.critical_error_context) << ",\n";
2026 } else {
2027 out << " \"critical_error\": null,\n";
2028 }
2029
2030 // warnings
2031 out << " \"warnings\": [\n";
2032 for (size_t i = 0; i < r.warnings.size(); i++) {
2033 const auto& w = r.warnings[i];
2034 out << " {\n";
2035 out << " \"key\": " << json::str(w.key) << ",\n";
2036 out << " \"fields\": " << json::object(w.fields, 6) << "\n";
2037 out << " }";
2038 if (i < r.warnings.size() - 1) out << ",";
2039 out << "\n";
2040 }
2041 out << " ],\n";
2042
2043 if (r.ok) {
2044 out << " \"contract\": " << serialize_contract(r.contract) << "\n";
2045 } else {
2046 out << " \"contract\": null\n";
2047 }
2048
2049 out << "}";
2050 return out.str();
2051}
2052
2053} // namespace core
2054} // namespace nah
2055
2056#endif // __cplusplus
2057
2058#endif // NAH_CORE_H