NAH 2.0.16
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: Apache-2.0
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 <functional>
97#include <optional>
98#include <sstream>
99#include <string>
100#include <unordered_map>
101#include <vector>
102
103namespace nah {
104namespace core {
105
106// ============================================================================
107// VERSION AND CONSTANTS
108// ============================================================================
109
111constexpr const char* NAH_CORE_VERSION = "1.0.0";
112constexpr int NAH_CORE_VERSION_MAJOR = 1;
113constexpr int NAH_CORE_VERSION_MINOR = 0;
114constexpr int NAH_CORE_VERSION_PATCH = 0;
115
117constexpr const char* NAH_CONTRACT_SCHEMA = "nah.launch.contract.v1";
118
120constexpr size_t MAX_EXPANDED_SIZE = 64 * 1024;
121
123constexpr size_t MAX_PLACEHOLDERS = 128;
124
126constexpr size_t MAX_ENV_VARS = 1024;
127
129constexpr size_t MAX_LIBRARY_PATHS = 256;
130
132constexpr size_t MAX_ARGUMENTS = 1024;
133
134// ============================================================================
135// ENVIRONMENT OPERATIONS
136// ============================================================================
137
147enum class EnvOp {
148 Set,
149 Prepend,
150 Append,
151 Unset
152};
153
155inline const char* env_op_to_string(EnvOp op) {
156 switch (op) {
157 case EnvOp::Set: return "set";
158 case EnvOp::Prepend: return "prepend";
159 case EnvOp::Append: return "append";
160 case EnvOp::Unset: return "unset";
161 }
162 return "set";
163}
164
166inline std::optional<EnvOp> parse_env_op(const std::string& s) {
167 if (s == "set") return EnvOp::Set;
168 if (s == "prepend") return EnvOp::Prepend;
169 if (s == "append") return EnvOp::Append;
170 if (s == "unset") return EnvOp::Unset;
171 return std::nullopt;
172}
173
180struct EnvValue {
181 EnvOp op = EnvOp::Set;
182 std::string value;
183 std::string separator = ":";
184
185 EnvValue() = default;
186 EnvValue(const char* v) : op(EnvOp::Set), value(v) {}
187 EnvValue(const std::string& v) : op(EnvOp::Set), value(v) {}
188 EnvValue(EnvOp o, const std::string& v, const std::string& sep = ":")
189 : op(o), value(v), separator(sep) {}
190
191 bool is_simple() const { return op == EnvOp::Set; }
192
193 // Comparison operators
194 bool operator==(const std::string& other) const { return value == other; }
195 bool operator==(const char* other) const { return value == other; }
196 bool operator==(const EnvValue& other) const {
197 return op == other.op && value == other.value && separator == other.separator;
198 }
199 bool operator!=(const EnvValue& other) const { return !(*this == other); }
200};
201
203using EnvMap = std::unordered_map<std::string, EnvValue>;
204
205// ============================================================================
206// TRUST STATE
207// ============================================================================
208
217enum class TrustState {
218 Verified,
219 Unverified,
220 Failed,
221 Unknown
222};
223
224inline const char* trust_state_to_string(TrustState s) {
225 switch (s) {
226 case TrustState::Verified: return "verified";
227 case TrustState::Unverified: return "unverified";
228 case TrustState::Failed: return "failed";
229 case TrustState::Unknown: return "unknown";
230 }
231 return "unknown";
232}
233
234inline std::optional<TrustState> parse_trust_state(const std::string& s) {
235 if (s == "verified") return TrustState::Verified;
236 if (s == "unverified") return TrustState::Unverified;
237 if (s == "failed") return TrustState::Failed;
238 if (s == "unknown") return TrustState::Unknown;
239 return std::nullopt;
240}
241
248struct TrustInfo {
249 TrustState state = TrustState::Unknown;
250 std::string source;
251 std::string evaluated_at;
252 std::string expires_at;
253 std::string inputs_hash;
254 std::unordered_map<std::string, std::string> details;
255};
256
257// ============================================================================
258// WARNING SYSTEM
259// ============================================================================
260
267enum class Warning {
268 invalid_manifest,
269 invalid_configuration,
270 profile_invalid,
271 profile_missing,
272 profile_parse_error,
273 nak_pin_invalid,
274 nak_not_found,
275 nak_version_unsupported,
276 nak_loader_required,
277 nak_loader_missing,
278 binary_not_found,
279 capability_missing,
280 capability_malformed,
281 capability_unknown,
282 missing_env_var,
283 invalid_trust_state,
284 override_denied,
285 override_invalid,
286 invalid_library_path,
287 trust_state_unknown,
288 trust_state_unverified,
289 trust_state_failed,
290 trust_state_stale,
291};
292
293inline const char* warning_to_string(Warning w) {
294 switch (w) {
295 case Warning::invalid_manifest: return "invalid_manifest";
296 case Warning::invalid_configuration: return "invalid_configuration";
297 case Warning::profile_invalid: return "profile_invalid";
298 case Warning::profile_missing: return "profile_missing";
299 case Warning::profile_parse_error: return "profile_parse_error";
300 case Warning::nak_pin_invalid: return "nak_pin_invalid";
301 case Warning::nak_not_found: return "nak_not_found";
302 case Warning::nak_version_unsupported: return "nak_version_unsupported";
303 case Warning::nak_loader_required: return "nak_loader_required";
304 case Warning::nak_loader_missing: return "nak_loader_missing";
305 case Warning::binary_not_found: return "binary_not_found";
306 case Warning::capability_missing: return "capability_missing";
307 case Warning::capability_malformed: return "capability_malformed";
308 case Warning::capability_unknown: return "capability_unknown";
309 case Warning::missing_env_var: return "missing_env_var";
310 case Warning::invalid_trust_state: return "invalid_trust_state";
311 case Warning::override_denied: return "override_denied";
312 case Warning::override_invalid: return "override_invalid";
313 case Warning::invalid_library_path: return "invalid_library_path";
314 case Warning::trust_state_unknown: return "trust_state_unknown";
315 case Warning::trust_state_unverified: return "trust_state_unverified";
316 case Warning::trust_state_failed: return "trust_state_failed";
317 case Warning::trust_state_stale: return "trust_state_stale";
318 }
319 return "unknown";
320}
321
322inline std::optional<Warning> parse_warning_key(const std::string& key) {
323 if (key == "invalid_manifest") return Warning::invalid_manifest;
324 if (key == "invalid_configuration") return Warning::invalid_configuration;
325 if (key == "profile_invalid") return Warning::profile_invalid;
326 if (key == "profile_missing") return Warning::profile_missing;
327 if (key == "profile_parse_error") return Warning::profile_parse_error;
328 if (key == "nak_pin_invalid") return Warning::nak_pin_invalid;
329 if (key == "nak_not_found") return Warning::nak_not_found;
330 if (key == "nak_version_unsupported") return Warning::nak_version_unsupported;
331 if (key == "nak_loader_required") return Warning::nak_loader_required;
332 if (key == "nak_loader_missing") return Warning::nak_loader_missing;
333 if (key == "binary_not_found") return Warning::binary_not_found;
334 if (key == "capability_missing") return Warning::capability_missing;
335 if (key == "capability_malformed") return Warning::capability_malformed;
336 if (key == "capability_unknown") return Warning::capability_unknown;
337 if (key == "missing_env_var") return Warning::missing_env_var;
338 if (key == "invalid_trust_state") return Warning::invalid_trust_state;
339 if (key == "override_denied") return Warning::override_denied;
340 if (key == "override_invalid") return Warning::override_invalid;
341 if (key == "invalid_library_path") return Warning::invalid_library_path;
342 if (key == "trust_state_unknown") return Warning::trust_state_unknown;
343 if (key == "trust_state_unverified") return Warning::trust_state_unverified;
344 if (key == "trust_state_failed") return Warning::trust_state_failed;
345 if (key == "trust_state_stale") return Warning::trust_state_stale;
346 return std::nullopt;
347}
348
352struct WarningObject {
353 std::string key;
354 std::string action;
355 std::unordered_map<std::string, std::string> fields;
356
357 bool operator==(const WarningObject& other) const {
358 return key == other.key && action == other.action && fields == other.fields;
359 }
360};
361
362// ============================================================================
363// CRITICAL ERRORS
364// ============================================================================
365
372enum class CriticalError {
373 MANIFEST_MISSING,
374 ENTRYPOINT_NOT_FOUND,
375 PATH_TRAVERSAL,
376 INSTALL_RECORD_INVALID,
377 NAK_LOADER_INVALID,
378};
379
380inline const char* critical_error_to_string(CriticalError e) {
381 switch (e) {
382 case CriticalError::MANIFEST_MISSING: return "MANIFEST_MISSING";
383 case CriticalError::ENTRYPOINT_NOT_FOUND: return "ENTRYPOINT_NOT_FOUND";
384 case CriticalError::PATH_TRAVERSAL: return "PATH_TRAVERSAL";
385 case CriticalError::INSTALL_RECORD_INVALID: return "INSTALL_RECORD_INVALID";
386 case CriticalError::NAK_LOADER_INVALID: return "NAK_LOADER_INVALID";
387 }
388 return "UNKNOWN";
389}
390
391inline std::optional<CriticalError> parse_critical_error(const std::string& s) {
392 if (s == "MANIFEST_MISSING") return CriticalError::MANIFEST_MISSING;
393 if (s == "ENTRYPOINT_NOT_FOUND") return CriticalError::ENTRYPOINT_NOT_FOUND;
394 if (s == "PATH_TRAVERSAL") return CriticalError::PATH_TRAVERSAL;
395 if (s == "INSTALL_RECORD_INVALID") return CriticalError::INSTALL_RECORD_INVALID;
396 if (s == "NAK_LOADER_INVALID") return CriticalError::NAK_LOADER_INVALID;
397 return std::nullopt;
398}
399
400// ============================================================================
401// TRACE SYSTEM
402// ============================================================================
403
410namespace trace_source {
411 constexpr const char* HOST = "host";
412 constexpr const char* NAK_RECORD = "nak_record";
413 constexpr const char* NAK = "nak"; // Alias for NAK_RECORD
414 constexpr const char* MANIFEST = "manifest";
415 constexpr const char* INSTALL_RECORD = "install_record";
416 constexpr const char* INSTALL_OVERRIDE = "install_override";
417 constexpr const char* PROCESS_ENV = "process_env";
418 constexpr const char* OVERRIDES_FILE = "overrides_file";
419 constexpr const char* STANDARD = "standard";
420 constexpr const char* NAH_STANDARD = "nah_standard";
421 constexpr const char* COMPUTED = "computed";
422}
423
429struct TraceContribution {
430 std::string value;
431 std::string source_kind;
432 std::string source_path;
433 int precedence_rank = 0;
434 EnvOp operation = EnvOp::Set;
435 bool accepted = false;
436};
437
443struct TraceEntry {
444 std::string value;
445 std::string source_kind;
446 std::string source_path;
447 int precedence_rank = 0;
448 std::vector<TraceContribution> history;
449};
450
454struct CompositionTrace {
455 std::unordered_map<std::string, TraceEntry> environment;
456 std::unordered_map<std::string, TraceEntry> library_paths;
457 std::unordered_map<std::string, TraceEntry> arguments;
458 std::vector<std::string> decisions;
459};
460
461// ============================================================================
462// COMPONENT DECLARATION
463// ============================================================================
464
481struct ComponentDecl {
482 std::string id;
483 std::string name;
484 std::string description;
485 std::string icon;
486 std::string entrypoint;
487 std::string uri_pattern;
488 std::string loader;
489 bool standalone = true;
490 bool hidden = false;
491
492 // Per-component overrides (extend app-level settings)
493 EnvMap environment;
494 std::vector<std::string> permissions_filesystem;
495 std::vector<std::string> permissions_network;
496
497 std::unordered_map<std::string, std::string> metadata;
498};
499
500// ============================================================================
501// APP DECLARATION
502// ============================================================================
503
504// An asset the app exposes for other apps or the host to use.
505struct AssetExportDecl {
506 std::string id;
507 std::string path;
508 std::string type;
509};
510
511// What the app declares it needs to run.
512//
513// This is typically parsed from a manifest file (nah.json, package.json, etc.)
514// but can be constructed directly. All paths are relative to where the app
515// will be installed.
516//
517// Example - a Lua app:
518//
519// AppDeclaration app;
520// app.id = "com.example.game";
521// app.version = "2.1.0";
522// app.entrypoint_path = "main.lua";
523// app.nak_id = "lua";
524// app.nak_version_req = ">=5.4.0";
525// app.lib_dirs = {"lib", "vendor"};
526// app.env_vars = {"GAME_DATA=./data"};
527//
528// Example - a standalone binary (no runtime):
529//
530// AppDeclaration app;
531// app.id = "org.tool.converter";
532// app.version = "1.0.0";
533// app.entrypoint_path = "bin/converter";
534// // nak_id left empty - no runtime needed
535//
536struct AppDeclaration {
537 // Required: App identity
538 std::string id;
539 std::string version;
540
541 // Required: What to run
542 std::string entrypoint_path;
543
544 // Optional: Runtime requirements (leave nak_id empty for standalone binaries)
545 std::string nak_id;
546 std::string nak_version_req;
547 std::string nak_loader;
548
549 // Optional: Arguments passed after the entrypoint
550 std::vector<std::string> entrypoint_args;
551
552 // Optional: Environment variables (lowest precedence, fill-only)
553 // Format: "KEY=value" - only set if not already defined by host/runtime
554 std::vector<std::string> env_vars;
555
556 // Optional: Library search paths (relative to app root)
557 std::vector<std::string> lib_dirs;
558
559 // Optional: Asset directories and exports
560 std::vector<std::string> asset_dirs;
561 std::vector<AssetExportDecl> asset_exports;
562
563 // Optional: Permission requests
564 std::vector<std::string> permissions_filesystem;
565 std::vector<std::string> permissions_network;
566
567 // Optional: Metadata (informational only, does not affect composition)
568 std::string description;
569 std::string author;
570 std::string license;
571 std::string homepage;
572
573 // Optional: Components provided by this application
574 std::vector<ComponentDecl> components;
575};
576
577// ============================================================================
578// HOST ENVIRONMENT
579// ============================================================================
580
581// Host configuration loaded from host.json.
582//
583// This replaces the old "profiles" concept with a single host configuration.
584// It contains environment variables, library paths, and override policy.
585//
586// Example:
587//
588// HostEnvironment host_env;
589// host_env.vars["NAH_ENV"] = EnvValue(EnvOp::Set, "production");
590// host_env.vars["LOG_LEVEL"] = EnvValue(EnvOp::Set, "warn");
591// host_env.paths.library_prepend = {"/opt/libs"};
592//
593// Host environment takes precedence over app-declared environment variables
594// but can be overridden by install record overrides (subject to override policy).
595//
596struct HostEnvironment {
597 EnvMap vars;
598
599 struct {
600 std::vector<std::string> library_prepend;
601 std::vector<std::string> library_append;
602 } paths;
603
604 struct {
605 bool allow_env_overrides = true;
606 std::vector<std::string> allowed_env_keys;
607 } overrides;
608
609 std::string source_path;
610};
611
612// ============================================================================
613// LOADER CONFIGURATION
614// ============================================================================
615
616// How a runtime executes app entrypoints.
617//
618// For example, Lua's loader might be:
619// exec_path: "/runtimes/lua/bin/lua"
620// args_template: ["{NAH_APP_ENTRY}"]
621//
622// The args_template supports {VAR} placeholders that are expanded from the
623// environment before execution.
624struct LoaderConfig {
625 std::string exec_path;
626 std::vector<std::string> args_template;
627};
628
629// ============================================================================
630// RUNTIME DESCRIPTOR
631// ============================================================================
632
633// Describes an installed runtime (NAK - Native Application Kit).
634//
635// A NAK is a runtime like Lua, Node, or Python that apps can depend on.
636// The RuntimeDescriptor tells NAH where the runtime is installed and how
637// to use it.
638//
639// Example - Lua 5.4.6:
640//
641// RuntimeDescriptor lua;
642// lua.nak.id = "lua";
643// lua.nak.version = "5.4.6";
644// lua.paths.root = "/runtimes/lua/5.4.6";
645// lua.paths.lib_dirs = {"/runtimes/lua/5.4.6/lib"};
646// lua.environment["LUA_PATH"] = EnvValue(EnvOp::Prepend, "./?.lua", ";");
647// lua.loaders["default"] = {"/runtimes/lua/5.4.6/bin/lua", {"{NAH_APP_ENTRY}"}};
648//
649// NAKs without loaders (libs-only) just provide libraries and environment.
650//
651struct RuntimeDescriptor {
652 struct {
653 std::string id;
654 std::string version;
655 } nak;
656
657 struct {
658 std::string root;
659 std::string resource_root;
660 std::vector<std::string> lib_dirs;
661 } paths;
662
663 // Environment variables provided by this runtime
664 EnvMap environment;
665
666 // Loaders - how this runtime executes apps. Empty for libs-only NAKs.
667 // Key is loader name (use "default" for the primary loader).
668 std::unordered_map<std::string, LoaderConfig> loaders;
669
670 bool has_loaders() const { return !loaders.empty(); }
671
672 struct {
673 bool present = false;
674 std::string cwd;
675 } execution;
676
677 struct {
678 std::string package_hash;
679 std::string installed_at;
680 std::string installed_by;
681 std::string source;
682 } provenance;
683
684 std::string source_path;
685};
686
687// ============================================================================
688// INSTALL RECORD
689// ============================================================================
690
691// Records where an app is installed and which runtime version to use.
692//
693// Created at install time, this captures:
694// - Where the app lives on disk (paths.install_root)
695// - Which specific runtime version to use (nak.record_ref)
696// - Trust/verification state
697// - Per-install overrides
698//
699// Example - minimal install record:
700//
701// InstallRecord install;
702// install.install.instance_id = "550e8400-e29b-41d4-a716-446655440000";
703// install.paths.install_root = "/apps/myapp";
704// install.nak.record_ref = "lua@5.4.6.json"; // Key into RuntimeInventory
705//
706// Example - with environment override:
707//
708// InstallRecord install;
709// install.install.instance_id = "...";
710// install.paths.install_root = "/apps/myapp";
711// install.nak.record_ref = "lua@5.4.6.json";
712// install.overrides.environment["DEBUG"] = "1";
713//
714struct InstallRecord {
715 // Unique identifier for this installation
716 struct {
717 std::string instance_id;
718 } install;
719
720 // Snapshot of app info at install time (audit only, does not affect composition)
721 struct {
722 std::string id;
723 std::string version;
724 std::string nak_id;
725 std::string nak_version_req;
726 } app;
727
728 // Which runtime to use - resolved and pinned at install time
729 struct {
730 std::string id;
731 std::string version;
732 std::string record_ref;
733 std::string loader;
734 std::string selection_reason;
735 } nak;
736
737 struct {
738 std::string install_root;
739 } paths;
740
741 struct {
742 std::string package_hash;
743 std::string installed_at;
744 std::string installed_by;
745 std::string source;
746 } provenance;
747
748 TrustInfo trust;
749
750 // Verification info (optional)
751 struct {
752 std::string last_verified_at;
753 std::string last_verifier_version;
754 } verification;
755
756 // Per-install overrides (subject to host profile policy)
757 struct {
758 EnvMap environment;
759 struct {
760 std::vector<std::string> prepend;
761 std::vector<std::string> append;
762 } arguments;
763 struct {
764 std::vector<std::string> library_prepend;
765 } paths;
766 } overrides;
767
768 std::string source_path;
769};
770
771// ============================================================================
772// RUNTIME INVENTORY
773// ============================================================================
774
775// Collection of available runtimes on the host.
776//
777// Maps record_ref (e.g., "lua@5.4.6.json") to RuntimeDescriptor.
778// The InstallRecord's nak.record_ref is used as the key to look up the runtime.
779//
780// Example:
781//
782// RuntimeInventory inventory;
783// inventory.runtimes["lua@5.4.6.json"] = lua_descriptor;
784// inventory.runtimes["node@20.0.0.json"] = node_descriptor;
785//
786struct RuntimeInventory {
787 std::unordered_map<std::string, RuntimeDescriptor> runtimes;
788};
789
790// ============================================================================
791// ASSET EXPORT
792// ============================================================================
793
794// An exported asset in the contract (paths resolved to absolute).
795struct AssetExport {
796 std::string id;
797 std::string path;
798 std::string type;
799};
800
801// ============================================================================
802// COMPONENT URI
803// ============================================================================
804
812struct ComponentURI {
813 bool valid = false;
814 std::string raw_uri;
815 std::string app_id;
816 std::string component_path;
817 std::string query;
818 std::string fragment;
819};
820
831inline ComponentURI parse_component_uri(const std::string& uri) {
832 ComponentURI result;
833 result.raw_uri = uri;
834
835 // Find scheme separator
836 size_t scheme_end = uri.find("://");
837 if (scheme_end == std::string::npos) {
838 return result; // Invalid
839 }
840
841 // Extract app_id (scheme)
842 result.app_id = uri.substr(0, scheme_end);
843 if (result.app_id.empty()) {
844 return result;
845 }
846
847 std::string rest = uri.substr(scheme_end + 3);
848
849 // Extract fragment (if present)
850 size_t fragment_pos = rest.find('#');
851 if (fragment_pos != std::string::npos) {
852 result.fragment = rest.substr(fragment_pos + 1);
853 rest = rest.substr(0, fragment_pos);
854 }
855
856 // Extract query (if present)
857 size_t query_pos = rest.find('?');
858 if (query_pos != std::string::npos) {
859 result.query = rest.substr(query_pos + 1);
860 rest = rest.substr(0, query_pos);
861 }
862
863 // Remaining is component_path
864 result.component_path = rest;
865 result.valid = true;
866
867 return result;
868}
869
870// ============================================================================
871// CAPABILITY USAGE
872// ============================================================================
873
874// Summary of capabilities requested by the app.
875struct CapabilityUsage {
876 bool present = false;
877 std::vector<std::string> required_capabilities;
878 std::vector<std::string> optional_capabilities;
879 std::vector<std::string> critical_capabilities;
880};
881
882// ============================================================================
883// LAUNCH CONTRACT
884// ============================================================================
885
886// The output of nah_compose() - everything needed to launch an application.
887//
888// The contract is self-contained: no additional lookups are needed to execute
889// the app. All paths are absolute, all environment variables are resolved,
890// and the exact binary and arguments are specified.
891//
892// To execute a contract:
893//
894// if (result.ok) {
895// const auto& c = result.contract;
896//
897// // Set environment
898// for (const auto& [key, value] : c.environment) {
899// setenv(key.c_str(), value.c_str(), 1);
900// }
901//
902// // Set library path
903// std::string lib_path;
904// for (const auto& p : c.execution.library_paths) {
905// if (!lib_path.empty()) lib_path += ":";
906// lib_path += p;
907// }
908// setenv(c.execution.library_path_env_key.c_str(), lib_path.c_str(), 1);
909//
910// // Change to working directory
911// chdir(c.execution.cwd.c_str());
912//
913// // Build argv: [binary, ...arguments]
914// std::vector<char*> argv;
915// argv.push_back(const_cast<char*>(c.execution.binary.c_str()));
916// for (const auto& arg : c.execution.arguments) {
917// argv.push_back(const_cast<char*>(arg.c_str()));
918// }
919// argv.push_back(nullptr);
920//
921// // Execute
922// execv(c.execution.binary.c_str(), argv.data());
923// }
924//
925struct LaunchContract {
926 // App identity and paths (all absolute)
927 struct {
928 std::string id;
929 std::string version;
930 std::string root;
931 std::string entrypoint;
932 } app;
933
934 // Runtime info (empty if standalone app)
935 struct {
936 std::string id;
937 std::string version;
938 std::string root;
939 std::string resource_root;
940 std::string record_ref;
941 } nak;
942
943 // How to execute the app
944 struct {
945 std::string binary;
946 std::vector<std::string> arguments;
947 std::string cwd;
948 std::string library_path_env_key;
949 std::vector<std::string> library_paths;
950 } execution;
951
952 // Complete environment map (ready to pass to exec)
953 std::unordered_map<std::string, std::string> environment;
954
955 // Capability/permission requirements for sandboxing
956 struct {
957 std::vector<std::string> filesystem;
958 std::vector<std::string> network;
959 } enforcement;
960
961 // Trust/verification state from install record
962 TrustInfo trust;
963
964 // Exported assets (id -> absolute path)
965 std::unordered_map<std::string, AssetExport> exports;
966
967 // Summary of capability usage
968 CapabilityUsage capability_usage;
969};
970
971// ============================================================================
972// POLICY VIOLATION
973// ============================================================================
974
975// Describes a policy violation (e.g., path traversal attempt).
976struct PolicyViolation {
977 std::string type;
978 std::string target;
979 std::string context;
980};
981
982// ============================================================================
983// COMPOSITION OPTIONS
984// ============================================================================
985
986// Options passed to nah_compose().
987struct CompositionOptions {
988 bool enable_trace = false;
989 std::string now;
990 std::string loader_override;
991};
992
993// ============================================================================
994// COMPOSITION RESULT
995// ============================================================================
996
997// The result of calling nah_compose().
998//
999// Check result.ok to see if composition succeeded. If true, result.contract
1000// contains the launch specification. If false, check critical_error and
1001// critical_error_context for what went wrong.
1002//
1003// Warnings are always populated (even on success) for non-fatal issues.
1004//
1005// Example:
1006//
1007// CompositionResult result = nah_compose(app, profile, install, inventory);
1008// if (!result.ok) {
1009// std::cerr << "Composition failed: " << result.critical_error_context << "\n";
1010// return 1;
1011// }
1012// for (const auto& w : result.warnings) {
1013// std::cerr << "Warning: " << w.key << "\n";
1014// }
1015// // Use result.contract...
1016//
1017struct CompositionResult {
1018 bool ok = false;
1019 std::optional<CriticalError> critical_error;
1020 std::string critical_error_context;
1021 LaunchContract contract;
1022 std::vector<WarningObject> warnings;
1023 std::vector<PolicyViolation> policy_violations;
1024 std::optional<CompositionTrace> trace;
1025};
1026
1027// ============================================================================
1028// PURE FUNCTIONS - Path Utilities
1029// ============================================================================
1030
1037inline bool is_absolute_path(const std::string& path) {
1038 if (path.empty()) return false;
1039#ifdef _WIN32
1040 if (path.size() >= 2) {
1041 if (path[1] == ':') return true;
1042 if (path[0] == '\\' && path[1] == '\\') return true;
1043 }
1044#endif
1045 return path[0] == '/';
1046}
1047
1051inline std::string normalize_separators(const std::string& path) {
1052 std::string result = path;
1053 for (char& c : result) {
1054 if (c == '\\') c = '/';
1055 }
1056 return result;
1057}
1058
1065inline bool path_escapes_root(const std::string& root, const std::string& path) {
1066 std::string norm_root = normalize_separators(root);
1067 std::string norm_path = normalize_separators(path);
1068
1069 while (!norm_root.empty() && norm_root.back() == '/') {
1070 norm_root.pop_back();
1071 }
1072
1073 // Path must start with root
1074 if (norm_path.find(norm_root) != 0) {
1075 return true;
1076 }
1077
1078 // Path must either be exactly root, or have a / after the root prefix
1079 // This prevents /app matching /application
1080 std::string rel = norm_path.substr(norm_root.size());
1081 if (!rel.empty() && rel[0] != '/') {
1082 return true; // e.g., /application doesn't have / after /app
1083 }
1084 if (!rel.empty() && rel[0] == '/') {
1085 rel = rel.substr(1);
1086 }
1087
1088 int depth = 0;
1089 size_t pos = 0;
1090 while (pos < rel.size()) {
1091 size_t next = rel.find('/', pos);
1092 std::string component = (next == std::string::npos)
1093 ? rel.substr(pos)
1094 : rel.substr(pos, next - pos);
1095
1096 if (component == "..") {
1097 depth--;
1098 if (depth < 0) return true;
1099 } else if (!component.empty() && component != ".") {
1100 depth++;
1101 }
1102
1103 if (next == std::string::npos) break;
1104 pos = next + 1;
1105 }
1106
1107 return false;
1108}
1109
1113inline std::string join_path(const std::string& base, const std::string& rel) {
1114 if (base.empty()) return rel;
1115 if (rel.empty()) return base;
1116
1117 std::string result = base;
1118 if (result.back() != '/' && result.back() != '\\') {
1119 result += '/';
1120 }
1121
1122 size_t start = 0;
1123 while (start < rel.size() && (rel[start] == '/' || rel[start] == '\\')) {
1124 start++;
1125 }
1126
1127 result += rel.substr(start);
1128 return normalize_separators(result);
1129}
1130
1134inline std::string get_library_path_env_key() {
1135#if defined(__APPLE__)
1136 return "DYLD_LIBRARY_PATH";
1137#elif defined(_WIN32)
1138 return "PATH";
1139#else
1140 return "LD_LIBRARY_PATH";
1141#endif
1142}
1143
1147inline char get_path_separator() {
1148#ifdef _WIN32
1149 return ';';
1150#else
1151 return ':';
1152#endif
1153}
1154
1155// ============================================================================
1156// PURE FUNCTIONS - Validation
1157// ============================================================================
1158
1162struct ValidationResult {
1163 bool ok = true;
1164 std::vector<std::string> errors;
1165 std::vector<std::string> warnings;
1166};
1167
1177inline ValidationResult validate_declaration(const AppDeclaration& decl) {
1178 ValidationResult result;
1179
1180 if (decl.id.empty()) {
1181 result.ok = false;
1182 result.errors.push_back("app.id is required");
1183 }
1184
1185 if (decl.version.empty()) {
1186 result.ok = false;
1187 result.errors.push_back("app.version is required");
1188 }
1189
1190 if (decl.entrypoint_path.empty()) {
1191 result.ok = false;
1192 result.errors.push_back("entrypoint_path is required");
1193 }
1194
1195 if (!decl.entrypoint_path.empty() && is_absolute_path(decl.entrypoint_path)) {
1196 result.ok = false;
1197 result.errors.push_back("entrypoint_path must be relative");
1198 }
1199
1200 for (const auto& lib_dir : decl.lib_dirs) {
1201 if (is_absolute_path(lib_dir)) {
1202 result.ok = false;
1203 result.errors.push_back("lib_dir must be relative: " + lib_dir);
1204 }
1205 }
1206
1207 for (const auto& exp : decl.asset_exports) {
1208 if (is_absolute_path(exp.path)) {
1209 result.ok = false;
1210 result.errors.push_back("asset_export path must be relative: " + exp.path);
1211 }
1212 }
1213
1214 if (!decl.nak_id.empty() && decl.nak_version_req.empty()) {
1215 result.warnings.push_back("nak_id specified but nak_version_req is empty");
1216 }
1217
1218 return result;
1219}
1220
1224inline ValidationResult validate_install_record(const InstallRecord& record) {
1225 ValidationResult result;
1226
1227 if (record.install.instance_id.empty()) {
1228 result.ok = false;
1229 result.errors.push_back("install.instance_id is required");
1230 }
1231
1232 if (record.paths.install_root.empty()) {
1233 result.ok = false;
1234 result.errors.push_back("paths.install_root is required");
1235 }
1236
1237 if (!record.paths.install_root.empty() && !is_absolute_path(record.paths.install_root)) {
1238 result.ok = false;
1239 result.errors.push_back("paths.install_root must be absolute");
1240 }
1241
1242 return result;
1243}
1244
1248inline ValidationResult validate_runtime(const RuntimeDescriptor& runtime) {
1249 ValidationResult result;
1250
1251 if (runtime.nak.id.empty()) {
1252 result.ok = false;
1253 result.errors.push_back("nak.id is required");
1254 }
1255
1256 if (runtime.nak.version.empty()) {
1257 result.ok = false;
1258 result.errors.push_back("nak.version is required");
1259 }
1260
1261 if (runtime.paths.root.empty()) {
1262 result.ok = false;
1263 result.errors.push_back("paths.root is required");
1264 }
1265
1266 if (!runtime.paths.root.empty() && !is_absolute_path(runtime.paths.root)) {
1267 result.ok = false;
1268 result.errors.push_back("paths.root must be absolute");
1269 }
1270
1271 for (const auto& lib_dir : runtime.paths.lib_dirs) {
1272 if (!is_absolute_path(lib_dir)) {
1273 result.ok = false;
1274 result.errors.push_back("lib_dir must be absolute: " + lib_dir);
1275 }
1276 }
1277
1278 for (const auto& [name, loader] : runtime.loaders) {
1279 if (!loader.exec_path.empty() && !is_absolute_path(loader.exec_path)) {
1280 result.ok = false;
1281 result.errors.push_back("loader exec_path must be absolute: " + name);
1282 }
1283 }
1284
1285 return result;
1286}
1287
1288
1289
1290// ============================================================================
1291// PURE FUNCTIONS - Environment
1292// ============================================================================
1293
1299inline std::optional<std::string> apply_env_op(
1300 const std::string& key,
1301 const EnvValue& env_val,
1302 const std::unordered_map<std::string, std::string>& current_env)
1303{
1304 switch (env_val.op) {
1305 case EnvOp::Set:
1306 return env_val.value;
1307
1308 case EnvOp::Prepend: {
1309 auto it = current_env.find(key);
1310 if (it != current_env.end() && !it->second.empty()) {
1311 return env_val.value + env_val.separator + it->second;
1312 }
1313 return env_val.value;
1314 }
1315
1316 case EnvOp::Append: {
1317 auto it = current_env.find(key);
1318 if (it != current_env.end() && !it->second.empty()) {
1319 return it->second + env_val.separator + env_val.value;
1320 }
1321 return env_val.value;
1322 }
1323
1324 case EnvOp::Unset:
1325 return std::nullopt;
1326 }
1327
1328 return env_val.value;
1329}
1330
1331// ============================================================================
1332// PURE FUNCTIONS - Placeholder Expansion
1333// ============================================================================
1334
1338struct ExpansionResult {
1339 bool ok = true;
1340 std::string value;
1341 std::string error;
1342};
1343
1350inline ExpansionResult expand_placeholders(
1351 const std::string& input,
1352 const std::unordered_map<std::string, std::string>& env)
1353{
1354 ExpansionResult result;
1355 result.value.reserve(input.size());
1356
1357 size_t placeholder_count = 0;
1358 size_t i = 0;
1359
1360 while (i < input.size()) {
1361 if (input[i] == '{') {
1362 size_t end = input.find('}', i + 1);
1363 if (end != std::string::npos) {
1364 std::string var_name = input.substr(i + 1, end - i - 1);
1365
1366 placeholder_count++;
1367 if (placeholder_count > MAX_PLACEHOLDERS) {
1368 result.ok = false;
1369 result.error = "placeholder_limit";
1370 return result;
1371 }
1372
1373 auto it = env.find(var_name);
1374 if (it != env.end()) {
1375 result.value += it->second;
1376 }
1377
1378 if (result.value.size() > MAX_EXPANDED_SIZE) {
1379 result.ok = false;
1380 result.error = "expansion_overflow";
1381 return result;
1382 }
1383
1384 i = end + 1;
1385 continue;
1386 }
1387 }
1388
1389 result.value += input[i];
1390 i++;
1391
1392 if (result.value.size() > MAX_EXPANDED_SIZE) {
1393 result.ok = false;
1394 result.error = "expansion_overflow";
1395 return result;
1396 }
1397 }
1398
1399 return result;
1400}
1401
1405inline std::vector<std::string> expand_string_vector(
1406 const std::vector<std::string>& inputs,
1407 const std::unordered_map<std::string, std::string>& env)
1408{
1409 std::vector<std::string> result;
1410 result.reserve(inputs.size());
1411
1412 for (const auto& input : inputs) {
1413 auto expanded = expand_placeholders(input, env);
1414 result.push_back(expanded.ok ? expanded.value : input);
1415 }
1416
1417 return result;
1418}
1419
1420// ============================================================================
1421// PURE FUNCTIONS - Runtime Resolution
1422// ============================================================================
1423
1427struct RuntimeResolutionResult {
1428 bool resolved = false;
1429 std::string record_ref;
1430 RuntimeDescriptor runtime;
1431 std::string selection_reason;
1432 std::vector<std::string> warnings;
1433};
1434
1440inline RuntimeResolutionResult resolve_runtime(
1441 const AppDeclaration& app,
1442 const InstallRecord& install,
1443 const RuntimeInventory& inventory)
1444{
1445 RuntimeResolutionResult result;
1446
1447 // Standalone apps don't need runtime resolution
1448 if (app.nak_id.empty()) {
1449 result.resolved = true;
1450 result.selection_reason = "standalone_app";
1451 return result;
1452 }
1453
1454 // Get record_ref from install record
1455 std::string record_ref = install.nak.record_ref;
1456
1457 if (record_ref.empty()) {
1458 result.warnings.push_back("nak.record_ref is empty in install record");
1459 return result;
1460 }
1461
1462 auto it = inventory.runtimes.find(record_ref);
1463 if (it == inventory.runtimes.end()) {
1464 result.warnings.push_back("NAK not found in inventory: " + record_ref);
1465 return result;
1466 }
1467
1468 result.resolved = true;
1469 result.record_ref = record_ref;
1470 result.runtime = it->second;
1471 result.selection_reason = "pinned_from_install_record";
1472
1473 return result;
1474}
1475
1476// ============================================================================
1477// PURE FUNCTIONS - Path Binding
1478// ============================================================================
1479
1483struct PathBindingResult {
1484 bool ok = true;
1485 std::string entrypoint;
1486 std::vector<std::string> library_paths;
1487 std::unordered_map<std::string, AssetExport> exports;
1488 std::vector<PolicyViolation> violations;
1489};
1490
1494inline PathBindingResult bind_paths(
1495 const AppDeclaration& decl,
1496 const InstallRecord& install,
1497 const RuntimeDescriptor* runtime,
1498 const HostEnvironment& host_env)
1499{
1500 PathBindingResult result;
1501 const std::string& app_root = install.paths.install_root;
1502
1503 // Entrypoint
1504 std::string entrypoint = join_path(app_root, decl.entrypoint_path);
1505 if (path_escapes_root(app_root, entrypoint)) {
1506 result.ok = false;
1507 result.violations.push_back({
1508 "path_traversal", "entrypoint", "entrypoint escapes app root"
1509 });
1510 return result;
1511 }
1512 result.entrypoint = entrypoint;
1513
1514 // Library paths in order: host prepend, install overrides, NAK, app, host append
1515 for (const auto& path : host_env.paths.library_prepend) {
1516 if (is_absolute_path(path)) {
1517 result.library_paths.push_back(path);
1518 }
1519 }
1520
1521 for (const auto& path : install.overrides.paths.library_prepend) {
1522 if (is_absolute_path(path)) {
1523 result.library_paths.push_back(path);
1524 }
1525 }
1526
1527 if (runtime) {
1528 for (const auto& lib_dir : runtime->paths.lib_dirs) {
1529 result.library_paths.push_back(lib_dir);
1530 }
1531 }
1532
1533 for (const auto& lib_dir : decl.lib_dirs) {
1534 std::string abs_lib = join_path(app_root, lib_dir);
1535 if (path_escapes_root(app_root, abs_lib)) {
1536 result.ok = false;
1537 result.violations.push_back({
1538 "path_traversal", "lib_dir", "lib_dir escapes app root: " + lib_dir
1539 });
1540 return result;
1541 }
1542 result.library_paths.push_back(abs_lib);
1543 }
1544
1545 for (const auto& path : host_env.paths.library_append) {
1546 if (is_absolute_path(path)) {
1547 result.library_paths.push_back(path);
1548 }
1549 }
1550
1551 // Asset exports
1552 for (const auto& exp : decl.asset_exports) {
1553 std::string abs_path = join_path(app_root, exp.path);
1554 if (path_escapes_root(app_root, abs_path)) {
1555 result.ok = false;
1556 result.violations.push_back({
1557 "path_traversal", "asset_export", "asset export escapes app root: " + exp.id
1558 });
1559 return result;
1560 }
1561 result.exports[exp.id] = {exp.id, abs_path, exp.type};
1562 }
1563
1564 return result;
1565}
1566
1567// ============================================================================
1568// PURE FUNCTIONS - Environment Composition
1569// ============================================================================
1570
1581inline std::unordered_map<std::string, std::string> compose_environment(
1582 const AppDeclaration& decl,
1583 const InstallRecord& install,
1584 const RuntimeDescriptor* runtime,
1585 const HostEnvironment& host_env,
1586 const LaunchContract& contract,
1587 CompositionTrace* trace = nullptr)
1588{
1589 std::unordered_map<std::string, std::string> env;
1590
1591 auto record = [&](const std::string& key, const std::string& value,
1592 const std::string& kind, const std::string& path,
1593 int rank, EnvOp op, bool accepted) {
1594 if (trace) {
1595 TraceContribution contrib;
1596 contrib.value = value;
1597 contrib.source_kind = kind;
1598 contrib.source_path = path;
1599 contrib.precedence_rank = rank;
1600 contrib.operation = op;
1601 contrib.accepted = accepted;
1602 trace->environment[key].history.push_back(contrib);
1603 }
1604 };
1605
1606 // Layer 1: Host environment (rank 5)
1607 for (const auto& [key, val] : host_env.vars) {
1608 auto result = apply_env_op(key, val, env);
1609 if (result.has_value()) {
1610 env[key] = *result;
1611 record(key, *result, trace_source::HOST, host_env.source_path, 5, val.op, true);
1612 } else {
1613 env.erase(key);
1614 record(key, "", trace_source::HOST, host_env.source_path, 5, val.op, true);
1615 }
1616 }
1617
1618 // Layer 2: NAK environment (rank 4)
1619 if (runtime) {
1620 for (const auto& [key, val] : runtime->environment) {
1621 auto result = apply_env_op(key, val, env);
1622 if (result.has_value()) {
1623 env[key] = *result;
1624 record(key, *result, trace_source::NAK_RECORD, runtime->source_path, 4, val.op, true);
1625 } else {
1626 env.erase(key);
1627 record(key, "", trace_source::NAK_RECORD, runtime->source_path, 4, val.op, true);
1628 }
1629 }
1630 }
1631
1632 // Layer 3: App manifest defaults (rank 3, fill-only)
1633 for (const auto& env_var : decl.env_vars) {
1634 auto eq = env_var.find('=');
1635 if (eq != std::string::npos) {
1636 std::string key = env_var.substr(0, eq);
1637 std::string val = env_var.substr(eq + 1);
1638 bool accepted = (env.find(key) == env.end());
1639 if (accepted) {
1640 env[key] = val;
1641 }
1642 record(key, val, trace_source::MANIFEST, "manifest", 3, EnvOp::Set, accepted);
1643 }
1644 }
1645
1646 // Layer 4: Install record overrides (rank 2)
1647 for (const auto& [key, val] : install.overrides.environment) {
1648 auto result = apply_env_op(key, val, env);
1649 if (result.has_value()) {
1650 env[key] = *result;
1651 record(key, *result, trace_source::INSTALL_RECORD, install.source_path, 2, val.op, true);
1652 } else {
1653 env.erase(key);
1654 record(key, "", trace_source::INSTALL_RECORD, install.source_path, 2, val.op, true);
1655 }
1656 }
1657
1658 // Layer 5: NAH standard variables (rank 1, always set)
1659 env["NAH_APP_ID"] = contract.app.id;
1660 record("NAH_APP_ID", contract.app.id, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1661
1662 env["NAH_APP_VERSION"] = contract.app.version;
1663 record("NAH_APP_VERSION", contract.app.version, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1664
1665 env["NAH_APP_ROOT"] = contract.app.root;
1666 record("NAH_APP_ROOT", contract.app.root, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1667
1668 env["NAH_APP_ENTRY"] = contract.app.entrypoint;
1669 record("NAH_APP_ENTRY", contract.app.entrypoint, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1670
1671 if (runtime) {
1672 env["NAH_NAK_ID"] = runtime->nak.id;
1673 record("NAH_NAK_ID", runtime->nak.id, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1674
1675 env["NAH_NAK_VERSION"] = runtime->nak.version;
1676 record("NAH_NAK_VERSION", runtime->nak.version, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1677
1678 env["NAH_NAK_ROOT"] = runtime->paths.root;
1679 record("NAH_NAK_ROOT", runtime->paths.root, trace_source::NAH_STANDARD, "nah", 1, EnvOp::Set, true);
1680 }
1681
1682 return env;
1683}
1684
1685// ============================================================================
1686// PURE FUNCTIONS - Timestamp Comparison
1687// ============================================================================
1688
1694inline std::string normalize_rfc3339(const std::string& ts) {
1695 if (ts.empty()) return ts;
1696
1697 std::string result = ts;
1698 if (result.size() >= 6) {
1699 std::string suffix = result.substr(result.size() - 6);
1700 if (suffix == "+00:00" || suffix == "-00:00") {
1701 result = result.substr(0, result.size() - 6) + "Z";
1702 }
1703 }
1704
1705 return result;
1706}
1707
1713inline bool timestamp_before(const std::string& a, const std::string& b) {
1714 return normalize_rfc3339(a) < normalize_rfc3339(b);
1715}
1716
1717// ============================================================================
1718// MAIN COMPOSITION FUNCTION
1719// ============================================================================
1720
1721// Compose a launch contract from app declaration and host state.
1722//
1723// This is the main entry point. Given:
1724// - app: What the application declares it needs
1725// - host_env: Host-provided environment variables
1726// - install: Where the app is installed and which runtime to use
1727// - inventory: Available runtimes on the host
1728//
1729// Returns a CompositionResult. Check result.ok - if true, result.contract
1730// contains everything needed to launch the application.
1731//
1732// This function is pure: no I/O, no syscalls, no side effects. Same inputs
1733// always produce the same output. This makes it safe to call from any context
1734// and easy to test.
1735//
1736// Example:
1737//
1738// AppDeclaration app;
1739// app.id = "com.example.game";
1740// app.version = "1.0.0";
1741// app.entrypoint_path = "main.lua";
1742// app.nak_id = "lua";
1743//
1744// InstallRecord install;
1745// install.install.instance_id = "abc123";
1746// install.paths.install_root = "/apps/game";
1747// install.nak.record_ref = "lua@5.4.6.json";
1748//
1749// HostEnvironment host_env; // Empty = no host overrides
1750//
1751// RuntimeInventory inventory;
1752// inventory.runtimes["lua@5.4.6.json"] = lua_runtime;
1753//
1754// auto result = nah_compose(app, host_env, install, inventory);
1755// if (result.ok) {
1756// // result.contract.execution.binary = "/runtimes/lua/bin/lua"
1757// // result.contract.execution.arguments = ["/apps/game/main.lua"]
1758// // result.contract.environment = {"LUA_PATH": "...", "NAH_APP_ID": "com.example.game", ...}
1759// }
1760//
1761inline CompositionResult nah_compose(
1762 const AppDeclaration& app,
1763 const HostEnvironment& host_env,
1764 const InstallRecord& install,
1765 const RuntimeInventory& inventory,
1766 const CompositionOptions& options = {})
1767{
1768 CompositionResult result;
1769
1770 // Initialize trace if enabled
1771 CompositionTrace* trace_ptr = nullptr;
1772 if (options.enable_trace) {
1773 result.trace = CompositionTrace{};
1774 trace_ptr = &(*result.trace);
1775 trace_ptr->decisions.push_back("Starting composition");
1776 }
1777
1778 // Validate declaration
1779 auto decl_valid = validate_declaration(app);
1780 if (!decl_valid.ok) {
1781 result.critical_error = CriticalError::MANIFEST_MISSING;
1782 result.critical_error_context = decl_valid.errors.empty() ?
1783 "invalid declaration" : decl_valid.errors[0];
1784 for (const auto& err : decl_valid.errors) {
1785 result.warnings.push_back({
1786 warning_to_string(Warning::invalid_manifest), "error", {{"reason", err}}
1787 });
1788 }
1789 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Declaration validation failed");
1790 return result;
1791 }
1792 if (trace_ptr) trace_ptr->decisions.push_back("Declaration validated");
1793
1794 // Validate install record
1795 auto install_valid = validate_install_record(install);
1796 if (!install_valid.ok) {
1797 result.critical_error = CriticalError::INSTALL_RECORD_INVALID;
1798 result.critical_error_context = install_valid.errors.empty() ?
1799 "invalid install record" : install_valid.errors[0];
1800 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Install record validation failed");
1801 return result;
1802 }
1803 if (trace_ptr) trace_ptr->decisions.push_back("Install record validated");
1804
1805 // Resolve runtime
1806 auto runtime_result = resolve_runtime(app, install, inventory);
1807 for (const auto& warn : runtime_result.warnings) {
1808 result.warnings.push_back({
1809 warning_to_string(Warning::nak_not_found), "warn", {{"reason", warn}}
1810 });
1811 }
1812
1813 RuntimeDescriptor* runtime_ptr = runtime_result.resolved && !runtime_result.runtime.nak.id.empty()
1814 ? &runtime_result.runtime : nullptr;
1815
1816 if (trace_ptr) {
1817 if (runtime_ptr) {
1818 trace_ptr->decisions.push_back("Runtime resolved: " + runtime_ptr->nak.id + "@" + runtime_ptr->nak.version);
1819 } else if (app.nak_id.empty()) {
1820 trace_ptr->decisions.push_back("Standalone app (no runtime)");
1821 } else {
1822 trace_ptr->decisions.push_back("Runtime not found");
1823 }
1824 }
1825
1826 // Validate runtime if present
1827 if (runtime_ptr) {
1828 auto runtime_valid = validate_runtime(*runtime_ptr);
1829 if (!runtime_valid.ok) {
1830 result.critical_error = CriticalError::PATH_TRAVERSAL;
1831 result.critical_error_context = runtime_valid.errors.empty() ?
1832 "invalid runtime" : runtime_valid.errors[0];
1833 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Runtime validation failed");
1834 return result;
1835 }
1836 }
1837
1838 // Populate basic contract fields
1839 LaunchContract& contract = result.contract;
1840
1841 contract.app.id = app.id;
1842 contract.app.version = app.version;
1843 contract.app.root = install.paths.install_root;
1844
1845 if (runtime_ptr) {
1846 contract.nak.id = runtime_ptr->nak.id;
1847 contract.nak.version = runtime_ptr->nak.version;
1848 contract.nak.root = runtime_ptr->paths.root;
1849 contract.nak.resource_root = runtime_ptr->paths.resource_root.empty() ?
1850 runtime_ptr->paths.root : runtime_ptr->paths.resource_root;
1851 contract.nak.record_ref = runtime_result.record_ref;
1852 }
1853
1854 // Bind paths
1855 auto paths = bind_paths(app, install, runtime_ptr, host_env);
1856 if (!paths.ok) {
1857 result.critical_error = CriticalError::PATH_TRAVERSAL;
1858 result.critical_error_context = paths.violations.empty() ?
1859 "path binding failed" : paths.violations[0].context;
1860 result.policy_violations = paths.violations;
1861 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Path binding failed");
1862 return result;
1863 }
1864
1865 contract.app.entrypoint = paths.entrypoint;
1866 contract.exports = paths.exports;
1867 if (trace_ptr) trace_ptr->decisions.push_back("Paths bound successfully");
1868
1869 // Compose environment
1870 auto env = compose_environment(app, install, runtime_ptr, host_env, contract, trace_ptr);
1871
1872 // Determine execution binary and arguments
1873 std::string pinned_loader = install.nak.loader;
1874
1875 // Override loader if specified in options
1876 if (!options.loader_override.empty()) {
1877 pinned_loader = options.loader_override;
1878 if (trace_ptr) trace_ptr->decisions.push_back("Loader override requested: " + pinned_loader);
1879 }
1880
1881 if (runtime_ptr && runtime_ptr->has_loaders()) {
1882 std::string effective_loader = pinned_loader;
1883
1884 if (effective_loader.empty()) {
1885 if (runtime_ptr->loaders.count("default")) {
1886 effective_loader = "default";
1887 if (trace_ptr) trace_ptr->decisions.push_back("Auto-selected 'default' loader");
1888 } else if (runtime_ptr->loaders.size() == 1) {
1889 effective_loader = runtime_ptr->loaders.begin()->first;
1890 if (trace_ptr) trace_ptr->decisions.push_back("Auto-selected single loader: " + effective_loader);
1891 } else {
1892 result.warnings.push_back({
1893 warning_to_string(Warning::nak_loader_required),
1894 "warn",
1895 {{"reason", "multiple loaders but none specified"}}
1896 });
1897 contract.execution.binary = contract.app.entrypoint;
1898 if (trace_ptr) trace_ptr->decisions.push_back("WARNING: Multiple loaders, using entrypoint");
1899 }
1900 } else {
1901 if (trace_ptr) trace_ptr->decisions.push_back("Using pinned loader: " + effective_loader);
1902 }
1903
1904 if (!effective_loader.empty()) {
1905 auto it = runtime_ptr->loaders.find(effective_loader);
1906 if (it == runtime_ptr->loaders.end()) {
1907 result.critical_error = CriticalError::NAK_LOADER_INVALID;
1908 result.critical_error_context = "loader not found: " + effective_loader;
1909 if (trace_ptr) trace_ptr->decisions.push_back("FAILED: Loader not found");
1910 return result;
1911 }
1912
1913 contract.execution.binary = it->second.exec_path;
1914 contract.execution.arguments = expand_string_vector(it->second.args_template, env);
1915 }
1916 } else {
1917 contract.execution.binary = contract.app.entrypoint;
1918 if (trace_ptr) trace_ptr->decisions.push_back("Using app entrypoint as binary");
1919 }
1920
1921 // Apply argument overrides
1922 auto expanded_prepend = expand_string_vector(install.overrides.arguments.prepend, env);
1923 contract.execution.arguments.insert(
1924 contract.execution.arguments.begin(),
1925 expanded_prepend.begin(),
1926 expanded_prepend.end());
1927
1928 auto expanded_entry_args = expand_string_vector(app.entrypoint_args, env);
1929 contract.execution.arguments.insert(
1930 contract.execution.arguments.end(),
1931 expanded_entry_args.begin(),
1932 expanded_entry_args.end());
1933
1934 auto expanded_append = expand_string_vector(install.overrides.arguments.append, env);
1935 contract.execution.arguments.insert(
1936 contract.execution.arguments.end(),
1937 expanded_append.begin(),
1938 expanded_append.end());
1939
1940 // Determine cwd
1941 if (runtime_ptr && runtime_ptr->execution.present && !runtime_ptr->execution.cwd.empty()) {
1942 auto cwd_expanded = expand_placeholders(runtime_ptr->execution.cwd, env);
1943 if (cwd_expanded.ok && is_absolute_path(cwd_expanded.value)) {
1944 contract.execution.cwd = cwd_expanded.value;
1945 } else if (cwd_expanded.ok) {
1946 contract.execution.cwd = join_path(runtime_ptr->paths.root, cwd_expanded.value);
1947 } else {
1948 contract.execution.cwd = contract.app.root;
1949 }
1950 } else {
1951 contract.execution.cwd = contract.app.root;
1952 }
1953
1954 // Library paths
1955 contract.execution.library_path_env_key = get_library_path_env_key();
1956 contract.execution.library_paths = paths.library_paths;
1957
1958 // Expand environment placeholders
1959 for (auto& [key, val] : env) {
1960 auto expanded = expand_placeholders(val, env);
1961 if (expanded.ok) {
1962 val = expanded.value;
1963 }
1964 }
1965 contract.environment = env;
1966
1967 // Enforcement
1968 for (const auto& perm : app.permissions_filesystem) {
1969 contract.enforcement.filesystem.push_back(perm);
1970 }
1971 for (const auto& perm : app.permissions_network) {
1972 contract.enforcement.network.push_back(perm);
1973 }
1974
1975 // Capability usage
1976 if (!app.permissions_filesystem.empty() || !app.permissions_network.empty()) {
1977 contract.capability_usage.present = true;
1978 for (const auto& perm : app.permissions_filesystem) {
1979 contract.capability_usage.required_capabilities.push_back("fs." + perm);
1980 }
1981 for (const auto& perm : app.permissions_network) {
1982 contract.capability_usage.required_capabilities.push_back("net." + perm);
1983 }
1984 }
1985
1986 // Trust
1987 contract.trust = install.trust;
1988
1989 if (install.trust.source.empty() && install.trust.evaluated_at.empty()) {
1990 contract.trust.state = TrustState::Unknown;
1991 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), "warn", {}});
1992 } else {
1993 switch (install.trust.state) {
1994 case TrustState::Verified:
1995 break;
1996 case TrustState::Unverified:
1997 result.warnings.push_back({warning_to_string(Warning::trust_state_unverified), "warn", {}});
1998 break;
1999 case TrustState::Failed:
2000 result.warnings.push_back({warning_to_string(Warning::trust_state_failed), "warn", {}});
2001 break;
2002 case TrustState::Unknown:
2003 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), "warn", {}});
2004 break;
2005 }
2006 }
2007
2008 // Check trust staleness
2009 if (!install.trust.expires_at.empty() && !options.now.empty()) {
2010 if (timestamp_before(install.trust.expires_at, options.now)) {
2011 result.warnings.push_back({warning_to_string(Warning::trust_state_stale), "warn", {}});
2012 if (trace_ptr) trace_ptr->decisions.push_back("WARNING: Trust verification has expired");
2013 }
2014 }
2015
2016 if (trace_ptr) trace_ptr->decisions.push_back("Composition completed successfully");
2017
2018 result.ok = true;
2019 return result;
2020}
2021
2022// ============================================================================
2023// JSON SERIALIZATION (Pure, No External Dependencies)
2024// ============================================================================
2025
2026namespace json {
2027
2031inline std::string escape(const std::string& s) {
2032 std::string result;
2033 result.reserve(s.size() + 16);
2034 for (char c : s) {
2035 switch (c) {
2036 case '"': result += "\\\""; break;
2037 case '\\': result += "\\\\"; break;
2038 case '\b': result += "\\b"; break;
2039 case '\f': result += "\\f"; break;
2040 case '\n': result += "\\n"; break;
2041 case '\r': result += "\\r"; break;
2042 case '\t': result += "\\t"; break;
2043 default:
2044 if (static_cast<unsigned char>(c) < 0x20) {
2045 char buf[8];
2046 snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
2047 result += buf;
2048 } else {
2049 result += c;
2050 }
2051 }
2052 }
2053 return result;
2054}
2055
2059inline std::string str(const std::string& s) {
2060 return "\"" + escape(s) + "\"";
2061}
2062
2066inline std::string object(const std::unordered_map<std::string, std::string>& m, size_t indent = 0) {
2067 if (m.empty()) return "{}";
2068
2069 std::vector<std::string> keys;
2070 for (const auto& [k, _] : m) keys.push_back(k);
2071 std::sort(keys.begin(), keys.end());
2072
2073 std::string pad(indent + 2, ' ');
2074 std::string result = "{\n";
2075 for (size_t i = 0; i < keys.size(); i++) {
2076 result += pad + str(keys[i]) + ": " + str(m.at(keys[i]));
2077 if (i < keys.size() - 1) result += ",";
2078 result += "\n";
2079 }
2080 result += std::string(indent, ' ') + "}";
2081 return result;
2082}
2083
2087inline std::string array(const std::vector<std::string>& v, size_t indent = 0) {
2088 if (v.empty()) return "[]";
2089
2090 std::string pad(indent + 2, ' ');
2091 std::string result = "[\n";
2092 for (size_t i = 0; i < v.size(); i++) {
2093 result += pad + str(v[i]);
2094 if (i < v.size() - 1) result += ",";
2095 result += "\n";
2096 }
2097 result += std::string(indent, ' ') + "]";
2098 return result;
2099}
2100
2101} // namespace json
2102
2108inline std::string serialize_contract(const LaunchContract& c) {
2109 std::ostringstream out;
2110 out << "{\n";
2111 out << " \"schema\": \"" << NAH_CONTRACT_SCHEMA << "\",\n";
2112
2113 // app
2114 out << " \"app\": {\n";
2115 out << " \"id\": " << json::str(c.app.id) << ",\n";
2116 out << " \"version\": " << json::str(c.app.version) << ",\n";
2117 out << " \"root\": " << json::str(c.app.root) << ",\n";
2118 out << " \"entrypoint\": " << json::str(c.app.entrypoint) << "\n";
2119 out << " },\n";
2120
2121 // nak
2122 out << " \"nak\": {\n";
2123 out << " \"id\": " << json::str(c.nak.id) << ",\n";
2124 out << " \"version\": " << json::str(c.nak.version) << ",\n";
2125 out << " \"root\": " << json::str(c.nak.root) << ",\n";
2126 out << " \"resource_root\": " << json::str(c.nak.resource_root) << ",\n";
2127 out << " \"record_ref\": " << json::str(c.nak.record_ref) << "\n";
2128 out << " },\n";
2129
2130 // execution
2131 out << " \"execution\": {\n";
2132 out << " \"binary\": " << json::str(c.execution.binary) << ",\n";
2133 out << " \"arguments\": " << json::array(c.execution.arguments, 4) << ",\n";
2134 out << " \"cwd\": " << json::str(c.execution.cwd) << ",\n";
2135 out << " \"library_path_env_key\": " << json::str(c.execution.library_path_env_key) << ",\n";
2136 out << " \"library_paths\": " << json::array(c.execution.library_paths, 4) << "\n";
2137 out << " },\n";
2138
2139 // environment
2140 out << " \"environment\": " << json::object(c.environment, 2) << ",\n";
2141
2142 // enforcement
2143 out << " \"enforcement\": {\n";
2144 out << " \"filesystem\": " << json::array(c.enforcement.filesystem, 4) << ",\n";
2145 out << " \"network\": " << json::array(c.enforcement.network, 4) << "\n";
2146 out << " },\n";
2147
2148 // trust
2149 out << " \"trust\": {\n";
2150 out << " \"state\": " << json::str(trust_state_to_string(c.trust.state)) << ",\n";
2151 out << " \"source\": " << json::str(c.trust.source) << ",\n";
2152 out << " \"evaluated_at\": " << json::str(c.trust.evaluated_at) << ",\n";
2153 out << " \"expires_at\": " << json::str(c.trust.expires_at) << "\n";
2154 out << " },\n";
2155
2156 // capability_usage
2157 out << " \"capability_usage\": {\n";
2158 out << " \"present\": " << (c.capability_usage.present ? "true" : "false") << ",\n";
2159 out << " \"required_capabilities\": " << json::array(c.capability_usage.required_capabilities, 4) << "\n";
2160 out << " }\n";
2161
2162 out << "}";
2163 return out.str();
2164}
2165
2169inline std::string serialize_result(const CompositionResult& r) {
2170 std::ostringstream out;
2171 out << "{\n";
2172 out << " \"ok\": " << (r.ok ? "true" : "false") << ",\n";
2173
2174 if (r.critical_error.has_value()) {
2175 out << " \"critical_error\": " << json::str(critical_error_to_string(*r.critical_error)) << ",\n";
2176 out << " \"critical_error_context\": " << json::str(r.critical_error_context) << ",\n";
2177 } else {
2178 out << " \"critical_error\": null,\n";
2179 }
2180
2181 // warnings
2182 out << " \"warnings\": [\n";
2183 for (size_t i = 0; i < r.warnings.size(); i++) {
2184 const auto& w = r.warnings[i];
2185 out << " {\n";
2186 out << " \"key\": " << json::str(w.key) << ",\n";
2187 out << " \"action\": " << json::str(w.action) << ",\n";
2188 out << " \"fields\": " << json::object(w.fields, 6) << "\n";
2189 out << " }";
2190 if (i < r.warnings.size() - 1) out << ",";
2191 out << "\n";
2192 }
2193 out << " ],\n";
2194
2195 if (r.ok) {
2196 out << " \"contract\": " << serialize_contract(r.contract) << "\n";
2197 } else {
2198 out << " \"contract\": null\n";
2199 }
2200
2201 out << "}";
2202 return out.str();
2203}
2204
2205} // namespace core
2206} // namespace nah
2207
2208#endif // __cplusplus
2209
2210#endif // NAH_CORE_H