100#include <unordered_map>
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;
117constexpr const char* NAH_CONTRACT_SCHEMA =
"nah.launch.contract.v1";
120constexpr size_t MAX_EXPANDED_SIZE = 64 * 1024;
123constexpr size_t MAX_PLACEHOLDERS = 128;
126constexpr size_t MAX_ENV_VARS = 1024;
129constexpr size_t MAX_LIBRARY_PATHS = 256;
132constexpr size_t MAX_ARGUMENTS = 1024;
155inline const char* env_op_to_string(EnvOp 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";
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;
181 EnvOp op = EnvOp::Set;
183 std::string separator =
":";
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) {}
191 bool is_simple()
const {
return op == EnvOp::Set; }
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;
199 bool operator!=(
const EnvValue& other)
const {
return !(*
this == other); }
203using EnvMap = std::unordered_map<std::string, EnvValue>;
217enum class TrustState {
224inline const char* trust_state_to_string(TrustState 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";
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;
249 TrustState state = TrustState::Unknown;
251 std::string evaluated_at;
252 std::string expires_at;
253 std::string inputs_hash;
254 std::unordered_map<std::string, std::string> details;
269 invalid_configuration,
275 nak_version_unsupported,
280 capability_malformed,
286 invalid_library_path,
288 trust_state_unverified,
293inline const char* warning_to_string(Warning 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";
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;
352struct WarningObject {
355 std::unordered_map<std::string, std::string> fields;
357 bool operator==(
const WarningObject& other)
const {
358 return key == other.key && action == other.action && fields == other.fields;
372enum class CriticalError {
374 ENTRYPOINT_NOT_FOUND,
376 INSTALL_RECORD_INVALID,
380inline const char* critical_error_to_string(CriticalError 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";
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;
410namespace trace_source {
411 constexpr const char* HOST =
"host";
412 constexpr const char* NAK_RECORD =
"nak_record";
413 constexpr const char* NAK =
"nak";
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";
429struct TraceContribution {
431 std::string source_kind;
432 std::string source_path;
433 int precedence_rank = 0;
434 EnvOp operation = EnvOp::Set;
435 bool accepted =
false;
445 std::string source_kind;
446 std::string source_path;
447 int precedence_rank = 0;
448 std::vector<TraceContribution> history;
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;
481struct ComponentDecl {
484 std::string description;
486 std::string entrypoint;
487 std::string uri_pattern;
489 bool standalone =
true;
494 std::vector<std::string> permissions_filesystem;
495 std::vector<std::string> permissions_network;
497 std::unordered_map<std::string, std::string> metadata;
505struct AssetExportDecl {
536struct AppDeclaration {
542 std::string entrypoint_path;
546 std::string nak_version_req;
547 std::string nak_loader;
550 std::vector<std::string> entrypoint_args;
554 std::vector<std::string> env_vars;
557 std::vector<std::string> lib_dirs;
560 std::vector<std::string> asset_dirs;
561 std::vector<AssetExportDecl> asset_exports;
564 std::vector<std::string> permissions_filesystem;
565 std::vector<std::string> permissions_network;
568 std::string description;
571 std::string homepage;
574 std::vector<ComponentDecl> components;
596struct HostEnvironment {
600 std::vector<std::string> library_prepend;
601 std::vector<std::string> library_append;
605 bool allow_env_overrides =
true;
606 std::vector<std::string> allowed_env_keys;
609 std::string source_path;
625 std::string exec_path;
626 std::vector<std::string> args_template;
651struct RuntimeDescriptor {
659 std::string resource_root;
660 std::vector<std::string> lib_dirs;
668 std::unordered_map<std::string, LoaderConfig> loaders;
670 bool has_loaders()
const {
return !loaders.empty(); }
673 bool present =
false;
678 std::string package_hash;
679 std::string installed_at;
680 std::string installed_by;
684 std::string source_path;
714struct InstallRecord {
717 std::string instance_id;
725 std::string nak_version_req;
732 std::string record_ref;
734 std::string selection_reason;
738 std::string install_root;
742 std::string package_hash;
743 std::string installed_at;
744 std::string installed_by;
752 std::string last_verified_at;
753 std::string last_verifier_version;
760 std::vector<std::string> prepend;
761 std::vector<std::string> append;
764 std::vector<std::string> library_prepend;
768 std::string source_path;
786struct RuntimeInventory {
787 std::unordered_map<std::string, RuntimeDescriptor> runtimes;
816 std::string component_path;
818 std::string fragment;
831inline ComponentURI parse_component_uri(
const std::string& uri) {
833 result.raw_uri = uri;
836 size_t scheme_end = uri.find(
"://");
837 if (scheme_end == std::string::npos) {
842 result.app_id = uri.substr(0, scheme_end);
843 if (result.app_id.empty()) {
847 std::string rest = uri.substr(scheme_end + 3);
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);
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);
864 result.component_path = rest;
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;
925struct LaunchContract {
931 std::string entrypoint;
939 std::string resource_root;
940 std::string record_ref;
946 std::vector<std::string> arguments;
948 std::string library_path_env_key;
949 std::vector<std::string> library_paths;
953 std::unordered_map<std::string, std::string> environment;
957 std::vector<std::string> filesystem;
958 std::vector<std::string> network;
965 std::unordered_map<std::string, AssetExport> exports;
968 CapabilityUsage capability_usage;
976struct PolicyViolation {
987struct CompositionOptions {
988 bool enable_trace =
false;
990 std::string loader_override;
1017struct CompositionResult {
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;
1037inline bool is_absolute_path(
const std::string& path) {
1038 if (path.empty())
return false;
1040 if (path.size() >= 2) {
1041 if (path[1] ==
':')
return true;
1042 if (path[0] ==
'\\' && path[1] ==
'\\')
return true;
1045 return path[0] ==
'/';
1051inline std::string normalize_separators(
const std::string& path) {
1052 std::string result = path;
1053 for (
char& c : result) {
1054 if (c ==
'\\') c =
'/';
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);
1069 while (!norm_root.empty() && norm_root.back() ==
'/') {
1070 norm_root.pop_back();
1074 if (norm_path.find(norm_root) != 0) {
1080 std::string rel = norm_path.substr(norm_root.size());
1081 if (!rel.empty() && rel[0] !=
'/') {
1084 if (!rel.empty() && rel[0] ==
'/') {
1085 rel = rel.substr(1);
1090 while (pos < rel.size()) {
1091 size_t next = rel.find(
'/', pos);
1092 std::string component = (next == std::string::npos)
1094 : rel.substr(pos, next - pos);
1096 if (component ==
"..") {
1098 if (depth < 0)
return true;
1099 }
else if (!component.empty() && component !=
".") {
1103 if (next == std::string::npos)
break;
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;
1117 std::string result = base;
1118 if (result.back() !=
'/' && result.back() !=
'\\') {
1123 while (start < rel.size() && (rel[start] ==
'/' || rel[start] ==
'\\')) {
1127 result += rel.substr(start);
1128 return normalize_separators(result);
1134inline std::string get_library_path_env_key() {
1135#if defined(__APPLE__)
1136 return "DYLD_LIBRARY_PATH";
1137#elif defined(_WIN32)
1140 return "LD_LIBRARY_PATH";
1147inline char get_path_separator() {
1162struct ValidationResult {
1164 std::vector<std::string> errors;
1165 std::vector<std::string> warnings;
1177inline ValidationResult validate_declaration(
const AppDeclaration& decl) {
1178 ValidationResult result;
1180 if (decl.id.empty()) {
1182 result.errors.push_back(
"app.id is required");
1185 if (decl.version.empty()) {
1187 result.errors.push_back(
"app.version is required");
1190 if (decl.entrypoint_path.empty()) {
1192 result.errors.push_back(
"entrypoint_path is required");
1195 if (!decl.entrypoint_path.empty() && is_absolute_path(decl.entrypoint_path)) {
1197 result.errors.push_back(
"entrypoint_path must be relative");
1200 for (
const auto& lib_dir : decl.lib_dirs) {
1201 if (is_absolute_path(lib_dir)) {
1203 result.errors.push_back(
"lib_dir must be relative: " + lib_dir);
1207 for (
const auto& exp : decl.asset_exports) {
1208 if (is_absolute_path(exp.path)) {
1210 result.errors.push_back(
"asset_export path must be relative: " + exp.path);
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");
1224inline ValidationResult validate_install_record(
const InstallRecord& record) {
1225 ValidationResult result;
1227 if (record.install.instance_id.empty()) {
1229 result.errors.push_back(
"install.instance_id is required");
1232 if (record.paths.install_root.empty()) {
1234 result.errors.push_back(
"paths.install_root is required");
1237 if (!record.paths.install_root.empty() && !is_absolute_path(record.paths.install_root)) {
1239 result.errors.push_back(
"paths.install_root must be absolute");
1248inline ValidationResult validate_runtime(
const RuntimeDescriptor& runtime) {
1249 ValidationResult result;
1251 if (runtime.nak.id.empty()) {
1253 result.errors.push_back(
"nak.id is required");
1256 if (runtime.nak.version.empty()) {
1258 result.errors.push_back(
"nak.version is required");
1261 if (runtime.paths.root.empty()) {
1263 result.errors.push_back(
"paths.root is required");
1266 if (!runtime.paths.root.empty() && !is_absolute_path(runtime.paths.root)) {
1268 result.errors.push_back(
"paths.root must be absolute");
1271 for (
const auto& lib_dir : runtime.paths.lib_dirs) {
1272 if (!is_absolute_path(lib_dir)) {
1274 result.errors.push_back(
"lib_dir must be absolute: " + lib_dir);
1278 for (
const auto& [name, loader] : runtime.loaders) {
1279 if (!loader.exec_path.empty() && !is_absolute_path(loader.exec_path)) {
1281 result.errors.push_back(
"loader exec_path must be absolute: " + name);
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)
1304 switch (env_val.op) {
1306 return env_val.value;
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;
1313 return env_val.value;
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;
1321 return env_val.value;
1325 return std::nullopt;
1328 return env_val.value;
1338struct ExpansionResult {
1350inline ExpansionResult expand_placeholders(
1351 const std::string& input,
1352 const std::unordered_map<std::string, std::string>& env)
1354 ExpansionResult result;
1355 result.value.reserve(input.size());
1357 size_t placeholder_count = 0;
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);
1366 placeholder_count++;
1367 if (placeholder_count > MAX_PLACEHOLDERS) {
1369 result.error =
"placeholder_limit";
1373 auto it = env.find(var_name);
1374 if (it != env.end()) {
1375 result.value += it->second;
1378 if (result.value.size() > MAX_EXPANDED_SIZE) {
1380 result.error =
"expansion_overflow";
1389 result.value += input[i];
1392 if (result.value.size() > MAX_EXPANDED_SIZE) {
1394 result.error =
"expansion_overflow";
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)
1409 std::vector<std::string> result;
1410 result.reserve(inputs.size());
1412 for (
const auto& input : inputs) {
1413 auto expanded = expand_placeholders(input, env);
1414 result.push_back(expanded.ok ? expanded.value : input);
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;
1440inline RuntimeResolutionResult resolve_runtime(
1441 const AppDeclaration& app,
1442 const InstallRecord& install,
1443 const RuntimeInventory& inventory)
1445 RuntimeResolutionResult result;
1448 if (app.nak_id.empty()) {
1449 result.resolved =
true;
1450 result.selection_reason =
"standalone_app";
1455 std::string record_ref = install.nak.record_ref;
1457 if (record_ref.empty()) {
1458 result.warnings.push_back(
"nak.record_ref is empty in install record");
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);
1468 result.resolved =
true;
1469 result.record_ref = record_ref;
1470 result.runtime = it->second;
1471 result.selection_reason =
"pinned_from_install_record";
1483struct PathBindingResult {
1485 std::string entrypoint;
1486 std::vector<std::string> library_paths;
1487 std::unordered_map<std::string, AssetExport> exports;
1488 std::vector<PolicyViolation> violations;
1494inline PathBindingResult bind_paths(
1495 const AppDeclaration& decl,
1496 const InstallRecord& install,
1497 const RuntimeDescriptor* runtime,
1498 const HostEnvironment& host_env)
1500 PathBindingResult result;
1501 const std::string& app_root = install.paths.install_root;
1504 std::string entrypoint = join_path(app_root, decl.entrypoint_path);
1505 if (path_escapes_root(app_root, entrypoint)) {
1507 result.violations.push_back({
1508 "path_traversal",
"entrypoint",
"entrypoint escapes app root"
1512 result.entrypoint = entrypoint;
1515 for (
const auto& path : host_env.paths.library_prepend) {
1516 if (is_absolute_path(path)) {
1517 result.library_paths.push_back(path);
1521 for (
const auto& path : install.overrides.paths.library_prepend) {
1522 if (is_absolute_path(path)) {
1523 result.library_paths.push_back(path);
1528 for (
const auto& lib_dir : runtime->paths.lib_dirs) {
1529 result.library_paths.push_back(lib_dir);
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)) {
1537 result.violations.push_back({
1538 "path_traversal",
"lib_dir",
"lib_dir escapes app root: " + lib_dir
1542 result.library_paths.push_back(abs_lib);
1545 for (
const auto& path : host_env.paths.library_append) {
1546 if (is_absolute_path(path)) {
1547 result.library_paths.push_back(path);
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)) {
1556 result.violations.push_back({
1557 "path_traversal",
"asset_export",
"asset export escapes app root: " + exp.id
1561 result.exports[exp.id] = {exp.id, abs_path, exp.type};
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)
1589 std::unordered_map<std::string, std::string> env;
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) {
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);
1607 for (
const auto& [key, val] : host_env.vars) {
1608 auto result = apply_env_op(key, val, env);
1609 if (result.has_value()) {
1611 record(key, *result, trace_source::HOST, host_env.source_path, 5, val.op,
true);
1614 record(key,
"", trace_source::HOST, host_env.source_path, 5, val.op,
true);
1620 for (
const auto& [key, val] : runtime->environment) {
1621 auto result = apply_env_op(key, val, env);
1622 if (result.has_value()) {
1624 record(key, *result, trace_source::NAK_RECORD, runtime->source_path, 4, val.op,
true);
1627 record(key,
"", trace_source::NAK_RECORD, runtime->source_path, 4, val.op,
true);
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());
1642 record(key, val, trace_source::MANIFEST,
"manifest", 3, EnvOp::Set, accepted);
1647 for (
const auto& [key, val] : install.overrides.environment) {
1648 auto result = apply_env_op(key, val, env);
1649 if (result.has_value()) {
1651 record(key, *result, trace_source::INSTALL_RECORD, install.source_path, 2, val.op,
true);
1654 record(key,
"", trace_source::INSTALL_RECORD, install.source_path, 2, val.op,
true);
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);
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);
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);
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);
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);
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);
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);
1694inline std::string normalize_rfc3339(
const std::string& ts) {
1695 if (ts.empty())
return ts;
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";
1713inline bool timestamp_before(
const std::string& a,
const std::string& b) {
1714 return normalize_rfc3339(a) < normalize_rfc3339(b);
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 = {})
1768 CompositionResult result;
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");
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}}
1789 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Declaration validation failed");
1792 if (trace_ptr) trace_ptr->decisions.push_back(
"Declaration validated");
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");
1803 if (trace_ptr) trace_ptr->decisions.push_back(
"Install record validated");
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}}
1813 RuntimeDescriptor* runtime_ptr = runtime_result.resolved && !runtime_result.runtime.nak.id.empty()
1814 ? &runtime_result.runtime :
nullptr;
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)");
1822 trace_ptr->decisions.push_back(
"Runtime not found");
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");
1839 LaunchContract& contract = result.contract;
1841 contract.app.id = app.id;
1842 contract.app.version = app.version;
1843 contract.app.root = install.paths.install_root;
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;
1855 auto paths = bind_paths(app, install, runtime_ptr, host_env);
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");
1865 contract.app.entrypoint = paths.entrypoint;
1866 contract.exports = paths.exports;
1867 if (trace_ptr) trace_ptr->decisions.push_back(
"Paths bound successfully");
1870 auto env = compose_environment(app, install, runtime_ptr, host_env, contract, trace_ptr);
1873 std::string pinned_loader = install.nak.loader;
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);
1881 if (runtime_ptr && runtime_ptr->has_loaders()) {
1882 std::string effective_loader = pinned_loader;
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);
1892 result.warnings.push_back({
1893 warning_to_string(Warning::nak_loader_required),
1895 {{
"reason",
"multiple loaders but none specified"}}
1897 contract.execution.binary = contract.app.entrypoint;
1898 if (trace_ptr) trace_ptr->decisions.push_back(
"WARNING: Multiple loaders, using entrypoint");
1901 if (trace_ptr) trace_ptr->decisions.push_back(
"Using pinned loader: " + effective_loader);
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");
1913 contract.execution.binary = it->second.exec_path;
1914 contract.execution.arguments = expand_string_vector(it->second.args_template, env);
1917 contract.execution.binary = contract.app.entrypoint;
1918 if (trace_ptr) trace_ptr->decisions.push_back(
"Using app entrypoint as binary");
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());
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());
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());
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);
1948 contract.execution.cwd = contract.app.root;
1951 contract.execution.cwd = contract.app.root;
1955 contract.execution.library_path_env_key = get_library_path_env_key();
1956 contract.execution.library_paths = paths.library_paths;
1959 for (
auto& [key, val] : env) {
1960 auto expanded = expand_placeholders(val, env);
1962 val = expanded.value;
1965 contract.environment = env;
1968 for (
const auto& perm : app.permissions_filesystem) {
1969 contract.enforcement.filesystem.push_back(perm);
1971 for (
const auto& perm : app.permissions_network) {
1972 contract.enforcement.network.push_back(perm);
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);
1981 for (
const auto& perm : app.permissions_network) {
1982 contract.capability_usage.required_capabilities.push_back(
"net." + perm);
1987 contract.trust = install.trust;
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", {}});
1993 switch (install.trust.state) {
1994 case TrustState::Verified:
1996 case TrustState::Unverified:
1997 result.warnings.push_back({warning_to_string(Warning::trust_state_unverified),
"warn", {}});
1999 case TrustState::Failed:
2000 result.warnings.push_back({warning_to_string(Warning::trust_state_failed),
"warn", {}});
2002 case TrustState::Unknown:
2003 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown),
"warn", {}});
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");
2016 if (trace_ptr) trace_ptr->decisions.push_back(
"Composition completed successfully");
2031inline std::string escape(
const std::string& s) {
2033 result.reserve(s.size() + 16);
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;
2044 if (
static_cast<unsigned char>(c) < 0x20) {
2046 snprintf(buf,
sizeof(buf),
"\\u%04x",
static_cast<unsigned char>(c));
2059inline std::string str(
const std::string& s) {
2060 return "\"" + escape(s) +
"\"";
2066inline std::string object(
const std::unordered_map<std::string, std::string>& m,
size_t indent = 0) {
2067 if (m.empty())
return "{}";
2069 std::vector<std::string> keys;
2070 for (
const auto& [k, _] : m) keys.push_back(k);
2071 std::sort(keys.begin(), keys.end());
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 +=
",";
2080 result += std::string(indent,
' ') +
"}";
2087inline std::string array(
const std::vector<std::string>& v,
size_t indent = 0) {
2088 if (v.empty())
return "[]";
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 +=
",";
2097 result += std::string(indent,
' ') +
"]";
2108inline std::string serialize_contract(
const LaunchContract& c) {
2109 std::ostringstream out;
2111 out <<
" \"schema\": \"" << NAH_CONTRACT_SCHEMA <<
"\",\n";
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";
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";
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";
2140 out <<
" \"environment\": " << json::object(c.environment, 2) <<
",\n";
2143 out <<
" \"enforcement\": {\n";
2144 out <<
" \"filesystem\": " << json::array(c.enforcement.filesystem, 4) <<
",\n";
2145 out <<
" \"network\": " << json::array(c.enforcement.network, 4) <<
"\n";
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";
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";
2169inline std::string serialize_result(
const CompositionResult& r) {
2170 std::ostringstream out;
2172 out <<
" \"ok\": " << (r.ok ?
"true" :
"false") <<
",\n";
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";
2178 out <<
" \"critical_error\": null,\n";
2182 out <<
" \"warnings\": [\n";
2183 for (
size_t i = 0; i < r.warnings.size(); i++) {
2184 const auto& w = r.warnings[i];
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";
2190 if (i < r.warnings.size() - 1) out <<
",";
2196 out <<
" \"contract\": " << serialize_contract(r.contract) <<
"\n";
2198 out <<
" \"contract\": null\n";