99#include <unordered_map>
110constexpr const char* NAH_CORE_VERSION =
"2.0.0";
111constexpr int NAH_CORE_VERSION_MAJOR = 2;
112constexpr int NAH_CORE_VERSION_MINOR = 0;
113constexpr int NAH_CORE_VERSION_PATCH = 0;
116constexpr const char* NAH_CONTRACT_SCHEMA =
"nah.launch.contract.v2";
119constexpr size_t MAX_EXPANDED_SIZE = 64 * 1024;
122constexpr size_t MAX_PLACEHOLDERS = 128;
125constexpr size_t MAX_ENV_VARS = 1024;
128constexpr size_t MAX_LIBRARY_PATHS = 256;
131constexpr size_t MAX_ARGUMENTS = 1024;
154inline const char* env_op_to_string(EnvOp op) {
156 case EnvOp::Set:
return "set";
157 case EnvOp::Prepend:
return "prepend";
158 case EnvOp::Append:
return "append";
159 case EnvOp::Unset:
return "unset";
165inline std::optional<EnvOp> parse_env_op(
const std::string& s) {
166 if (s ==
"set")
return EnvOp::Set;
167 if (s ==
"prepend")
return EnvOp::Prepend;
168 if (s ==
"append")
return EnvOp::Append;
169 if (s ==
"unset")
return EnvOp::Unset;
180 EnvOp op = EnvOp::Set;
182 std::string separator =
":";
184 EnvValue() =
default;
185 EnvValue(
const char* v) : op(EnvOp::Set), value(v) {}
186 EnvValue(
const std::string& v) : op(EnvOp::Set), value(v) {}
187 EnvValue(EnvOp o,
const std::string& v,
const std::string& sep =
":")
188 : op(o), value(v), separator(sep) {}
190 bool is_simple()
const {
return op == EnvOp::Set; }
193 bool operator==(
const std::string& other)
const {
return value == other; }
194 bool operator==(
const char* other)
const {
return value == other; }
195 bool operator==(
const EnvValue& other)
const {
196 return op == other.op && value == other.value && separator == other.separator;
198 bool operator!=(
const EnvValue& other)
const {
return !(*
this == other); }
202using EnvMap = std::unordered_map<std::string, EnvValue>;
216enum class TrustState {
223inline const char* trust_state_to_string(TrustState s) {
225 case TrustState::Verified:
return "verified";
226 case TrustState::Unverified:
return "unverified";
227 case TrustState::Failed:
return "failed";
228 case TrustState::Unknown:
return "unknown";
233inline std::optional<TrustState> parse_trust_state(
const std::string& s) {
234 if (s ==
"verified")
return TrustState::Verified;
235 if (s ==
"unverified")
return TrustState::Unverified;
236 if (s ==
"failed")
return TrustState::Failed;
237 if (s ==
"unknown")
return TrustState::Unknown;
248 TrustState state = TrustState::Unknown;
250 std::string evaluated_at;
251 std::string expires_at;
252 std::string inputs_hash;
253 std::unordered_map<std::string, std::string> details;
267 trust_state_unverified,
272inline const char* warning_to_string(Warning w) {
274 case Warning::trust_state_unknown:
return "trust_state_unknown";
275 case Warning::trust_state_unverified:
return "trust_state_unverified";
276 case Warning::trust_state_failed:
return "trust_state_failed";
277 case Warning::trust_state_stale:
return "trust_state_stale";
285struct WarningObject {
287 std::unordered_map<std::string, std::string> fields;
289 bool operator==(
const WarningObject& other)
const {
290 return key == other.key && fields == other.fields;
304enum class CriticalError {
306 ENTRYPOINT_NOT_FOUND,
310 INSTALL_RECORD_INVALID,
315inline const char* critical_error_to_string(CriticalError e) {
317 case CriticalError::MANIFEST_MISSING:
return "MANIFEST_MISSING";
318 case CriticalError::ENTRYPOINT_NOT_FOUND:
return "ENTRYPOINT_NOT_FOUND";
319 case CriticalError::PATH_TRAVERSAL:
return "PATH_TRAVERSAL";
320 case CriticalError::EXPANSION_FAILED:
return "EXPANSION_FAILED";
321 case CriticalError::HOST_CONFIG_INVALID:
return "HOST_CONFIG_INVALID";
322 case CriticalError::INSTALL_RECORD_INVALID:
return "INSTALL_RECORD_INVALID";
323 case CriticalError::NAK_NOT_FOUND:
return "NAK_NOT_FOUND";
324 case CriticalError::NAK_LOADER_INVALID:
return "NAK_LOADER_INVALID";
329inline std::optional<CriticalError> parse_critical_error(
const std::string& s) {
330 if (s ==
"MANIFEST_MISSING")
return CriticalError::MANIFEST_MISSING;
331 if (s ==
"ENTRYPOINT_NOT_FOUND")
return CriticalError::ENTRYPOINT_NOT_FOUND;
332 if (s ==
"PATH_TRAVERSAL")
return CriticalError::PATH_TRAVERSAL;
333 if (s ==
"EXPANSION_FAILED")
return CriticalError::EXPANSION_FAILED;
334 if (s ==
"HOST_CONFIG_INVALID")
return CriticalError::HOST_CONFIG_INVALID;
335 if (s ==
"INSTALL_RECORD_INVALID")
return CriticalError::INSTALL_RECORD_INVALID;
336 if (s ==
"NAK_NOT_FOUND")
return CriticalError::NAK_NOT_FOUND;
337 if (s ==
"NAK_LOADER_INVALID")
return CriticalError::NAK_LOADER_INVALID;
348namespace trace_source {
349 constexpr const char* HOST =
"host";
350 constexpr const char* NAK_RECORD =
"nak_record";
351 constexpr const char* MANIFEST =
"manifest";
352 constexpr const char* INSTALL_RECORD =
"install_record";
353 constexpr const char* NAH_STANDARD =
"nah_standard";
361struct TraceContribution {
363 std::string source_kind;
364 std::string source_path;
365 int precedence_rank = 0;
366 EnvOp operation = EnvOp::Set;
367 bool accepted =
false;
377 std::string source_kind;
378 std::string source_path;
379 int precedence_rank = 0;
380 std::vector<TraceContribution> history;
386struct CompositionTrace {
387 std::unordered_map<std::string, TraceEntry> environment;
388 std::unordered_map<std::string, TraceEntry> library_paths;
389 std::unordered_map<std::string, TraceEntry> arguments;
390 std::vector<std::string> decisions;
398struct AssetExportDecl {
429struct AppDeclaration {
435 std::string entrypoint_path;
439 std::string nak_version_req;
440 std::string nak_loader;
443 std::vector<std::string> entrypoint_args;
449 std::vector<std::string> lib_dirs;
452 std::vector<std::string> asset_dirs;
453 std::vector<AssetExportDecl> asset_exports;
456 std::vector<std::string> permissions_filesystem;
457 std::vector<std::string> permissions_network;
460 std::string description;
463 std::string homepage;
485struct HostEnvironment {
489 std::vector<std::string> library_prepend;
490 std::vector<std::string> library_append;
493 std::string source_path;
509 std::string exec_path;
510 std::vector<std::string> args_template;
535struct RuntimeDescriptor {
543 std::string resource_root;
544 std::vector<std::string> lib_dirs;
552 std::unordered_map<std::string, LoaderConfig> loaders;
554 bool has_loaders()
const {
return !loaders.empty(); }
557 bool present =
false;
562 std::string package_hash;
563 std::string installed_at;
564 std::string installed_by;
570 std::string source_path;
600struct InstallRecord {
603 std::string instance_id;
611 std::string nak_version_req;
618 std::string record_ref;
620 std::string selection_reason;
624 std::string install_root;
628 std::string package_hash;
629 std::string installed_at;
630 std::string installed_by;
640 std::vector<std::string> prepend;
641 std::vector<std::string> append;
644 std::vector<std::string> library_prepend;
645 std::vector<std::string> library_append;
649 std::string source_path;
667struct RuntimeInventory {
668 std::unordered_map<std::string, RuntimeDescriptor> runtimes;
725struct LaunchContract {
731 std::string entrypoint;
732 std::string package_hash;
740 std::string resource_root;
741 std::string record_ref;
742 std::string package_hash;
748 std::vector<std::string> arguments;
750 std::string library_path_env_key;
751 std::vector<std::string> library_paths;
755 std::unordered_map<std::string, std::string> environment;
759 std::vector<std::string> filesystem;
760 std::vector<std::string> network;
767 std::unordered_map<std::string, AssetExport> exports;
776struct CompositionOptions {
777 bool enable_trace =
false;
779 std::string loader_override;
806struct CompositionResult {
808 std::optional<CriticalError> critical_error;
809 std::string critical_error_context;
810 LaunchContract contract;
811 std::vector<WarningObject> warnings;
812 std::optional<CompositionTrace> trace;
825inline bool is_absolute_path(
const std::string& path) {
826 if (path.empty())
return false;
828 if (path.size() >= 2) {
829 if (path[1] ==
':')
return true;
830 if (path[0] ==
'\\' && path[1] ==
'\\')
return true;
833 return path[0] ==
'/';
839inline std::string normalize_separators(
const std::string& path) {
840 std::string result = path;
841 for (
char& c : result) {
842 if (c ==
'\\') c =
'/';
853inline bool path_escapes_root(
const std::string& root,
const std::string& path) {
854 std::string norm_root = normalize_separators(root);
855 std::string norm_path = normalize_separators(path);
857 while (!norm_root.empty() && norm_root.back() ==
'/') {
858 norm_root.pop_back();
862 if (norm_path.find(norm_root) != 0) {
868 std::string rel = norm_path.substr(norm_root.size());
869 if (!rel.empty() && rel[0] !=
'/') {
872 if (!rel.empty() && rel[0] ==
'/') {
878 while (pos < rel.size()) {
879 size_t next = rel.find(
'/', pos);
880 std::string component = (next == std::string::npos)
882 : rel.substr(pos, next - pos);
884 if (component ==
"..") {
886 if (depth < 0)
return true;
887 }
else if (!component.empty() && component !=
".") {
891 if (next == std::string::npos)
break;
901inline std::string join_path(
const std::string& base,
const std::string& rel) {
902 if (base.empty())
return rel;
903 if (rel.empty())
return base;
905 std::string result = base;
906 if (result.back() !=
'/' && result.back() !=
'\\') {
911 while (start < rel.size() && (rel[start] ==
'/' || rel[start] ==
'\\')) {
915 result += rel.substr(start);
916 return normalize_separators(result);
922inline std::string get_library_path_env_key() {
923#if defined(__APPLE__)
924 return "DYLD_LIBRARY_PATH";
928 return "LD_LIBRARY_PATH";
935inline char get_path_separator() {
950struct ValidationResult {
952 std::vector<std::string> errors;
953 std::vector<std::string> warnings;
965inline ValidationResult validate_declaration(
const AppDeclaration& decl) {
966 ValidationResult result;
968 if (decl.id.empty()) {
970 result.errors.push_back(
"app.id is required");
973 if (decl.version.empty()) {
975 result.errors.push_back(
"app.version is required");
978 if (decl.entrypoint_path.empty()) {
980 result.errors.push_back(
"entrypoint_path is required");
983 if (!decl.entrypoint_path.empty() && is_absolute_path(decl.entrypoint_path)) {
985 result.errors.push_back(
"entrypoint_path must be relative");
988 for (
const auto& lib_dir : decl.lib_dirs) {
989 if (is_absolute_path(lib_dir)) {
991 result.errors.push_back(
"lib_dir must be relative: " + lib_dir);
995 for (
const auto& exp : decl.asset_exports) {
996 if (is_absolute_path(exp.path)) {
998 result.errors.push_back(
"asset_export path must be relative: " + exp.path);
1002 if (!decl.nak_id.empty() && decl.nak_version_req.empty()) {
1003 result.warnings.push_back(
"nak_id specified but nak_version_req is empty");
1012inline ValidationResult validate_install_record(
const InstallRecord& record) {
1013 ValidationResult result;
1015 if (record.install.instance_id.empty()) {
1017 result.errors.push_back(
"install.instance_id is required");
1020 if (record.paths.install_root.empty()) {
1022 result.errors.push_back(
"paths.install_root is required");
1025 if (!record.paths.install_root.empty() && !is_absolute_path(record.paths.install_root)) {
1027 result.errors.push_back(
"paths.install_root must be absolute");
1036inline ValidationResult validate_runtime(
const RuntimeDescriptor& runtime) {
1037 ValidationResult result;
1039 if (runtime.nak.id.empty()) {
1041 result.errors.push_back(
"nak.id is required");
1044 if (runtime.nak.version.empty()) {
1046 result.errors.push_back(
"nak.version is required");
1049 if (runtime.paths.root.empty()) {
1051 result.errors.push_back(
"paths.root is required");
1054 if (!runtime.paths.root.empty() && !is_absolute_path(runtime.paths.root)) {
1056 result.errors.push_back(
"paths.root must be absolute");
1059 if (!runtime.paths.resource_root.empty() &&
1060 (!is_absolute_path(runtime.paths.resource_root) ||
1061 path_escapes_root(runtime.paths.root, runtime.paths.resource_root))) {
1063 result.errors.push_back(
"paths.resource_root must be within paths.root");
1066 for (
const auto& lib_dir : runtime.paths.lib_dirs) {
1067 if (!is_absolute_path(lib_dir) || path_escapes_root(runtime.paths.root, lib_dir)) {
1069 result.errors.push_back(
"lib_dir must be within paths.root: " + lib_dir);
1073 for (
const auto& [name, loader] : runtime.loaders) {
1074 if (loader.exec_path.empty() || !is_absolute_path(loader.exec_path) ||
1075 path_escapes_root(runtime.paths.root, loader.exec_path)) {
1077 result.errors.push_back(
"loader exec_path must be within paths.root: " + name);
1095inline std::optional<std::string> apply_env_op(
1096 const std::string& key,
1097 const EnvValue& env_val,
1098 const std::unordered_map<std::string, std::string>& current_env)
1100 switch (env_val.op) {
1102 return env_val.value;
1104 case EnvOp::Prepend: {
1105 auto it = current_env.find(key);
1106 if (it != current_env.end() && !it->second.empty()) {
1107 return env_val.value + env_val.separator + it->second;
1109 return env_val.value;
1112 case EnvOp::Append: {
1113 auto it = current_env.find(key);
1114 if (it != current_env.end() && !it->second.empty()) {
1115 return it->second + env_val.separator + env_val.value;
1117 return env_val.value;
1121 return std::nullopt;
1124 return env_val.value;
1134struct ExpansionResult {
1146inline ExpansionResult expand_placeholders(
1147 const std::string& input,
1148 const std::unordered_map<std::string, std::string>& env)
1150 ExpansionResult result;
1151 result.value.reserve(input.size());
1153 size_t placeholder_count = 0;
1156 while (i < input.size()) {
1157 if (input[i] ==
'{') {
1158 size_t end = input.find(
'}', i + 1);
1159 if (end != std::string::npos) {
1160 std::string var_name = input.substr(i + 1, end - i - 1);
1162 placeholder_count++;
1163 if (placeholder_count > MAX_PLACEHOLDERS) {
1165 result.error =
"placeholder_limit";
1169 auto it = env.find(var_name);
1170 if (it == env.end()) {
1172 result.error =
"missing_placeholder:" + var_name;
1175 result.value += it->second;
1177 if (result.value.size() > MAX_EXPANDED_SIZE) {
1179 result.error =
"expansion_overflow";
1188 result.value += input[i];
1191 if (result.value.size() > MAX_EXPANDED_SIZE) {
1193 result.error =
"expansion_overflow";
1204struct ExpansionListResult {
1206 std::vector<std::string> values;
1210inline ExpansionListResult expand_string_vector(
1211 const std::vector<std::string>& inputs,
1212 const std::unordered_map<std::string, std::string>& env)
1214 ExpansionListResult result;
1215 result.values.reserve(inputs.size());
1217 for (
const auto& input : inputs) {
1218 auto expanded = expand_placeholders(input, env);
1221 result.error = expanded.error;
1224 result.values.push_back(std::move(expanded.value));
1237struct RuntimeResolutionResult {
1238 bool resolved =
false;
1239 std::string record_ref;
1240 RuntimeDescriptor runtime;
1241 std::string selection_reason;
1242 std::vector<std::string> warnings;
1250inline RuntimeResolutionResult resolve_runtime(
1251 const AppDeclaration& app,
1252 const InstallRecord& install,
1253 const RuntimeInventory& inventory)
1255 RuntimeResolutionResult result;
1258 if (app.nak_id.empty()) {
1259 result.resolved =
true;
1260 result.selection_reason =
"standalone_app";
1265 std::string record_ref = install.nak.record_ref;
1267 if (record_ref.empty()) {
1268 result.warnings.push_back(
"nak.record_ref is empty in install record");
1272 auto it = inventory.runtimes.find(record_ref);
1273 if (it == inventory.runtimes.end()) {
1274 result.warnings.push_back(
"NAK not found in inventory: " + record_ref);
1278 if (it->second.nak.id != app.nak_id ||
1279 (!install.nak.id.empty() && install.nak.id != it->second.nak.id) ||
1280 (!install.nak.version.empty() && install.nak.version != it->second.nak.version)) {
1281 result.warnings.push_back(
"pinned NAK identity does not match app or install record");
1285 result.resolved =
true;
1286 result.record_ref = record_ref;
1287 result.runtime = it->second;
1288 result.selection_reason =
"pinned_from_install_record";
1300struct PathBindingResult {
1302 std::string entrypoint;
1303 std::vector<std::string> library_paths;
1304 std::unordered_map<std::string, AssetExport> exports;
1305 std::vector<std::string> errors;
1311inline PathBindingResult bind_paths(
1312 const AppDeclaration& decl,
1313 const InstallRecord& install,
1314 const RuntimeDescriptor* runtime,
1315 const HostEnvironment& host_env)
1317 PathBindingResult result;
1318 const std::string& app_root = install.paths.install_root;
1321 std::string entrypoint = join_path(app_root, decl.entrypoint_path);
1322 if (path_escapes_root(app_root, entrypoint)) {
1324 result.errors.push_back(
"entrypoint escapes app root");
1327 result.entrypoint = entrypoint;
1330 for (
const auto& path : host_env.paths.library_prepend) {
1331 if (is_absolute_path(path)) {
1332 result.library_paths.push_back(path);
1336 for (
const auto& path : install.overrides.paths.library_prepend) {
1337 if (is_absolute_path(path)) {
1338 result.library_paths.push_back(path);
1343 for (
const auto& lib_dir : runtime->paths.lib_dirs) {
1344 result.library_paths.push_back(lib_dir);
1348 for (
const auto& lib_dir : decl.lib_dirs) {
1349 std::string abs_lib = join_path(app_root, lib_dir);
1350 if (path_escapes_root(app_root, abs_lib)) {
1352 result.errors.push_back(
"lib_dir escapes app root: " + lib_dir);
1355 result.library_paths.push_back(abs_lib);
1358 for (
const auto& path : host_env.paths.library_append) {
1359 if (is_absolute_path(path)) {
1360 result.library_paths.push_back(path);
1364 for (
const auto& path : install.overrides.paths.library_append) {
1365 if (is_absolute_path(path)) {
1366 result.library_paths.push_back(path);
1371 for (
const auto& exp : decl.asset_exports) {
1372 std::string abs_path = join_path(app_root, exp.path);
1373 if (path_escapes_root(app_root, abs_path)) {
1375 result.errors.push_back(
"asset export escapes app root: " + exp.id);
1378 result.exports[exp.id] = {exp.id, abs_path, exp.type};
1394inline std::unordered_map<std::string, std::string> compose_environment(
1395 const AppDeclaration& decl,
1396 const InstallRecord& install,
1397 const RuntimeDescriptor* runtime,
1398 const HostEnvironment& host_env,
1399 const LaunchContract& contract,
1400 CompositionTrace* trace =
nullptr)
1402 std::unordered_map<std::string, std::string> env;
1404 auto record = [&](
const std::string& key,
const std::string& value,
1405 const std::string& kind,
const std::string& path,
1406 int rank, EnvOp op,
bool accepted) {
1408 TraceContribution contrib;
1409 contrib.value = value;
1410 contrib.source_kind = kind;
1411 contrib.source_path = path;
1412 contrib.precedence_rank = rank;
1413 contrib.operation = op;
1414 contrib.accepted = accepted;
1415 trace->environment[key].history.push_back(contrib);
1420 for (
const auto& [key, val] : decl.environment) {
1421 auto result = apply_env_op(key, val, env);
1422 if (result.has_value()) {
1424 record(key, *result, trace_source::MANIFEST,
"manifest", 5, val.op,
true);
1427 record(key,
"", trace_source::MANIFEST,
"manifest", 5, val.op,
true);
1433 for (
const auto& [key, val] : runtime->environment) {
1434 auto result = apply_env_op(key, val, env);
1435 if (result.has_value()) {
1437 record(key, *result, trace_source::NAK_RECORD, runtime->source_path, 4, val.op,
true);
1440 record(key,
"", trace_source::NAK_RECORD, runtime->source_path, 4, val.op,
true);
1446 for (
const auto& [key, val] : host_env.vars) {
1447 auto result = apply_env_op(key, val, env);
1448 if (result.has_value()) {
1450 record(key, *result, trace_source::HOST, host_env.source_path, 3, val.op,
true);
1453 record(key,
"", trace_source::HOST, host_env.source_path, 3, val.op,
true);
1458 for (
const auto& [key, val] : install.overrides.environment) {
1459 auto result = apply_env_op(key, val, env);
1460 if (result.has_value()) {
1462 record(key, *result, trace_source::INSTALL_RECORD, install.source_path, 2, val.op,
true);
1465 record(key,
"", trace_source::INSTALL_RECORD, install.source_path, 2, val.op,
true);
1470 env[
"NAH_APP_ID"] = contract.app.id;
1471 record(
"NAH_APP_ID", contract.app.id, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1473 env[
"NAH_APP_VERSION"] = contract.app.version;
1474 record(
"NAH_APP_VERSION", contract.app.version, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1476 env[
"NAH_APP_ROOT"] = contract.app.root;
1477 record(
"NAH_APP_ROOT", contract.app.root, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1479 env[
"NAH_APP_ENTRY"] = contract.app.entrypoint;
1480 record(
"NAH_APP_ENTRY", contract.app.entrypoint, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1483 env[
"NAH_NAK_ID"] = runtime->nak.id;
1484 record(
"NAH_NAK_ID", runtime->nak.id, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1486 env[
"NAH_NAK_VERSION"] = runtime->nak.version;
1487 record(
"NAH_NAK_VERSION", runtime->nak.version, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1489 env[
"NAH_NAK_ROOT"] = runtime->paths.root;
1490 record(
"NAH_NAK_ROOT", runtime->paths.root, trace_source::NAH_STANDARD,
"nah", 1, EnvOp::Set,
true);
1505inline std::string normalize_rfc3339(
const std::string& ts) {
1506 if (ts.empty())
return ts;
1508 std::string result = ts;
1509 if (result.size() >= 6) {
1510 std::string suffix = result.substr(result.size() - 6);
1511 if (suffix ==
"+00:00" || suffix ==
"-00:00") {
1512 result = result.substr(0, result.size() - 6) +
"Z";
1524inline bool timestamp_before(
const std::string& a,
const std::string& b) {
1525 return normalize_rfc3339(a) < normalize_rfc3339(b);
1572inline CompositionResult nah_compose(
1573 const AppDeclaration& app,
1574 const HostEnvironment& host_env,
1575 const InstallRecord& install,
1576 const RuntimeInventory& inventory,
1577 const CompositionOptions& options = {})
1579 CompositionResult result;
1582 CompositionTrace* trace_ptr =
nullptr;
1583 if (options.enable_trace) {
1584 result.trace = CompositionTrace{};
1585 trace_ptr = &(*result.trace);
1586 trace_ptr->decisions.push_back(
"Starting composition");
1590 auto decl_valid = validate_declaration(app);
1591 if (!decl_valid.ok) {
1592 result.critical_error = CriticalError::MANIFEST_MISSING;
1593 result.critical_error_context = decl_valid.errors.empty() ?
1594 "invalid declaration" : decl_valid.errors[0];
1595 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Declaration validation failed");
1598 if (trace_ptr) trace_ptr->decisions.push_back(
"Declaration validated");
1601 auto install_valid = validate_install_record(install);
1602 if ((!install.app.id.empty() && install.app.id != app.id) ||
1603 (!install.app.version.empty() && install.app.version != app.version)) {
1604 install_valid.ok =
false;
1605 install_valid.errors.push_back(
"install record app identity does not match the manifest");
1607 if (!install_valid.ok) {
1608 result.critical_error = CriticalError::INSTALL_RECORD_INVALID;
1609 result.critical_error_context = install_valid.errors.empty() ?
1610 "invalid install record" : install_valid.errors[0];
1611 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Install record validation failed");
1614 if (trace_ptr) trace_ptr->decisions.push_back(
"Install record validated");
1617 auto runtime_result = resolve_runtime(app, install, inventory);
1618 RuntimeDescriptor* runtime_ptr = runtime_result.resolved && !runtime_result.runtime.nak.id.empty()
1619 ? &runtime_result.runtime :
nullptr;
1621 if (!app.nak_id.empty() && !runtime_ptr) {
1622 result.critical_error = CriticalError::NAK_NOT_FOUND;
1623 result.critical_error_context = runtime_result.warnings.empty()
1624 ?
"required pinned NAK is unavailable"
1625 : runtime_result.warnings.front();
1631 trace_ptr->decisions.push_back(
"Runtime resolved: " + runtime_ptr->nak.id +
"@" + runtime_ptr->nak.version);
1632 }
else if (app.nak_id.empty()) {
1633 trace_ptr->decisions.push_back(
"Standalone app (no runtime)");
1635 trace_ptr->decisions.push_back(
"Runtime not found");
1641 auto runtime_valid = validate_runtime(*runtime_ptr);
1642 if (!runtime_valid.ok) {
1643 result.critical_error = CriticalError::PATH_TRAVERSAL;
1644 result.critical_error_context = runtime_valid.errors.empty() ?
1645 "invalid runtime" : runtime_valid.errors[0];
1646 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Runtime validation failed");
1652 LaunchContract& contract = result.contract;
1654 contract.app.id = app.id;
1655 contract.app.version = app.version;
1656 contract.app.root = install.paths.install_root;
1657 contract.app.package_hash = install.provenance.package_hash;
1660 contract.nak.id = runtime_ptr->nak.id;
1661 contract.nak.version = runtime_ptr->nak.version;
1662 contract.nak.root = runtime_ptr->paths.root;
1663 contract.nak.resource_root = runtime_ptr->paths.resource_root.empty() ?
1664 runtime_ptr->paths.root : runtime_ptr->paths.resource_root;
1665 contract.nak.record_ref = runtime_result.record_ref;
1666 contract.nak.package_hash = runtime_ptr->provenance.package_hash;
1670 auto paths = bind_paths(app, install, runtime_ptr, host_env);
1672 result.critical_error = CriticalError::PATH_TRAVERSAL;
1673 result.critical_error_context = paths.errors.empty() ?
1674 "path binding failed" : paths.errors.front();
1675 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Path binding failed");
1679 contract.app.entrypoint = paths.entrypoint;
1680 contract.exports = paths.exports;
1681 if (trace_ptr) trace_ptr->decisions.push_back(
"Paths bound successfully");
1684 auto env = compose_environment(app, install, runtime_ptr, host_env, contract, trace_ptr);
1685 const auto unexpanded_env = env;
1686 for (
auto& [key, value] : env) {
1687 auto expanded = expand_placeholders(value, unexpanded_env);
1689 result.critical_error = CriticalError::EXPANSION_FAILED;
1690 result.critical_error_context =
"environment " + key +
": " + expanded.error;
1691 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: " + result.critical_error_context);
1694 value = std::move(expanded.value);
1696 contract.environment = env;
1698 auto fail_expansion = [&](
const std::string& context,
const std::string& error) {
1699 result.critical_error = CriticalError::EXPANSION_FAILED;
1700 result.critical_error_context = context +
": " + error;
1701 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: " + result.critical_error_context);
1705 std::string pinned_loader = install.nak.loader;
1708 if (!options.loader_override.empty()) {
1709 pinned_loader = options.loader_override;
1710 if (trace_ptr) trace_ptr->decisions.push_back(
"Loader override requested: " + pinned_loader);
1713 if (runtime_ptr && runtime_ptr->has_loaders()) {
1714 std::string effective_loader = pinned_loader;
1716 if (effective_loader.empty()) {
1717 if (runtime_ptr->loaders.count(
"default")) {
1718 effective_loader =
"default";
1719 if (trace_ptr) trace_ptr->decisions.push_back(
"Auto-selected 'default' loader");
1720 }
else if (runtime_ptr->loaders.size() == 1) {
1721 effective_loader = runtime_ptr->loaders.begin()->first;
1722 if (trace_ptr) trace_ptr->decisions.push_back(
"Auto-selected single loader: " + effective_loader);
1724 result.critical_error = CriticalError::NAK_LOADER_INVALID;
1725 result.critical_error_context =
"multiple NAK loaders are available but none was selected";
1726 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: No NAK loader selected");
1730 if (trace_ptr) trace_ptr->decisions.push_back(
"Using pinned loader: " + effective_loader);
1733 if (!effective_loader.empty()) {
1734 auto it = runtime_ptr->loaders.find(effective_loader);
1735 if (it == runtime_ptr->loaders.end()) {
1736 result.critical_error = CriticalError::NAK_LOADER_INVALID;
1737 result.critical_error_context =
"loader not found: " + effective_loader;
1738 if (trace_ptr) trace_ptr->decisions.push_back(
"FAILED: Loader not found");
1742 contract.execution.binary = it->second.exec_path;
1743 auto expanded = expand_string_vector(it->second.args_template, env);
1745 fail_expansion(
"loader arguments", expanded.error);
1748 contract.execution.arguments = std::move(expanded.values);
1751 contract.execution.binary = contract.app.entrypoint;
1752 if (trace_ptr) trace_ptr->decisions.push_back(
"Using app entrypoint as binary");
1756 auto expanded_prepend = expand_string_vector(install.overrides.arguments.prepend, env);
1757 if (!expanded_prepend.ok) {
1758 fail_expansion(
"prepended arguments", expanded_prepend.error);
1761 contract.execution.arguments.insert(
1762 contract.execution.arguments.begin(),
1763 expanded_prepend.values.begin(),
1764 expanded_prepend.values.end());
1766 auto expanded_entry_args = expand_string_vector(app.entrypoint_args, env);
1767 if (!expanded_entry_args.ok) {
1768 fail_expansion(
"app arguments", expanded_entry_args.error);
1771 contract.execution.arguments.insert(
1772 contract.execution.arguments.end(),
1773 expanded_entry_args.values.begin(),
1774 expanded_entry_args.values.end());
1776 auto expanded_append = expand_string_vector(install.overrides.arguments.append, env);
1777 if (!expanded_append.ok) {
1778 fail_expansion(
"appended arguments", expanded_append.error);
1781 contract.execution.arguments.insert(
1782 contract.execution.arguments.end(),
1783 expanded_append.values.begin(),
1784 expanded_append.values.end());
1787 if (runtime_ptr && runtime_ptr->execution.present && !runtime_ptr->execution.cwd.empty()) {
1788 auto cwd_expanded = expand_placeholders(runtime_ptr->execution.cwd, env);
1789 if (!cwd_expanded.ok) {
1790 fail_expansion(
"working directory", cwd_expanded.error);
1793 if (is_absolute_path(cwd_expanded.value)) {
1794 contract.execution.cwd = cwd_expanded.value;
1796 contract.execution.cwd = join_path(runtime_ptr->paths.root, cwd_expanded.value);
1798 if (path_escapes_root(runtime_ptr->paths.root, contract.execution.cwd) &&
1799 path_escapes_root(contract.app.root, contract.execution.cwd)) {
1800 result.critical_error = CriticalError::PATH_TRAVERSAL;
1801 result.critical_error_context =
"working directory escapes app and runtime roots";
1805 contract.execution.cwd = contract.app.root;
1809 contract.execution.library_path_env_key = get_library_path_env_key();
1810 contract.execution.library_paths = paths.library_paths;
1812 contract.permissions.filesystem = app.permissions_filesystem;
1813 contract.permissions.network = app.permissions_network;
1817 contract.trust = install.trust;
1819 const auto app_state = install.trust.state;
1820 const auto runtime_state = runtime_ptr->trust.state;
1821 contract.trust.source =
"nah.artifact-chain";
1822 contract.trust.details[
"app_state"] = trust_state_to_string(app_state);
1823 contract.trust.details[
"runtime_state"] = trust_state_to_string(runtime_state);
1824 contract.trust.details[
"app_digest"] = install.provenance.package_hash;
1825 contract.trust.details[
"runtime_digest"] = runtime_ptr->provenance.package_hash;
1826 if (app_state == TrustState::Failed || runtime_state == TrustState::Failed) {
1827 contract.trust.state = TrustState::Failed;
1828 }
else if (app_state == TrustState::Unknown || runtime_state == TrustState::Unknown) {
1829 contract.trust.state = TrustState::Unknown;
1830 }
else if (app_state == TrustState::Unverified || runtime_state == TrustState::Unverified) {
1831 contract.trust.state = TrustState::Unverified;
1833 contract.trust.state = TrustState::Verified;
1835 if (contract.trust.expires_at.empty() ||
1836 (!runtime_ptr->trust.expires_at.empty() &&
1837 timestamp_before(runtime_ptr->trust.expires_at, contract.trust.expires_at))) {
1838 contract.trust.expires_at = runtime_ptr->trust.expires_at;
1842 if (contract.trust.source.empty() && contract.trust.evaluated_at.empty()) {
1843 contract.trust.state = TrustState::Unknown;
1844 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), {}});
1846 switch (contract.trust.state) {
1847 case TrustState::Verified:
1849 case TrustState::Unverified:
1850 result.warnings.push_back({warning_to_string(Warning::trust_state_unverified), {}});
1852 case TrustState::Failed:
1853 result.warnings.push_back({warning_to_string(Warning::trust_state_failed), {}});
1855 case TrustState::Unknown:
1856 result.warnings.push_back({warning_to_string(Warning::trust_state_unknown), {}});
1862 if (!contract.trust.expires_at.empty() && !options.now.empty()) {
1863 if (timestamp_before(contract.trust.expires_at, options.now)) {
1864 result.warnings.push_back({warning_to_string(Warning::trust_state_stale), {}});
1865 if (trace_ptr) trace_ptr->decisions.push_back(
"WARNING: Trust verification has expired");
1869 if (trace_ptr) trace_ptr->decisions.push_back(
"Composition completed successfully");
1884inline std::string escape(
const std::string& s) {
1886 result.reserve(s.size() + 16);
1889 case '"': result +=
"\\\"";
break;
1890 case '\\': result +=
"\\\\";
break;
1891 case '\b': result +=
"\\b";
break;
1892 case '\f': result +=
"\\f";
break;
1893 case '\n': result +=
"\\n";
break;
1894 case '\r': result +=
"\\r";
break;
1895 case '\t': result +=
"\\t";
break;
1897 if (
static_cast<unsigned char>(c) < 0x20) {
1899 snprintf(buf,
sizeof(buf),
"\\u%04x",
static_cast<unsigned char>(c));
1912inline std::string str(
const std::string& s) {
1913 return "\"" + escape(s) +
"\"";
1919inline std::string object(
const std::unordered_map<std::string, std::string>& m,
size_t indent = 0) {
1920 if (m.empty())
return "{}";
1922 std::vector<std::string> keys;
1923 for (
const auto& [k, _] : m) keys.push_back(k);
1924 std::sort(keys.begin(), keys.end());
1926 std::string pad(indent + 2,
' ');
1927 std::string result =
"{\n";
1928 for (
size_t i = 0; i < keys.size(); i++) {
1929 result += pad + str(keys[i]) +
": " + str(m.at(keys[i]));
1930 if (i < keys.size() - 1) result +=
",";
1933 result += std::string(indent,
' ') +
"}";
1940inline std::string array(
const std::vector<std::string>& v,
size_t indent = 0) {
1941 if (v.empty())
return "[]";
1943 std::string pad(indent + 2,
' ');
1944 std::string result =
"[\n";
1945 for (
size_t i = 0; i < v.size(); i++) {
1946 result += pad + str(v[i]);
1947 if (i < v.size() - 1) result +=
",";
1950 result += std::string(indent,
' ') +
"]";
1961inline std::string serialize_contract(
const LaunchContract& c) {
1962 std::ostringstream out;
1964 out <<
" \"schema\": \"" << NAH_CONTRACT_SCHEMA <<
"\",\n";
1967 out <<
" \"app\": {\n";
1968 out <<
" \"id\": " << json::str(c.app.id) <<
",\n";
1969 out <<
" \"version\": " << json::str(c.app.version) <<
",\n";
1970 out <<
" \"root\": " << json::str(c.app.root) <<
",\n";
1971 out <<
" \"entrypoint\": " << json::str(c.app.entrypoint) <<
",\n";
1972 out <<
" \"package_hash\": " << json::str(c.app.package_hash) <<
"\n";
1976 out <<
" \"nak\": {\n";
1977 out <<
" \"id\": " << json::str(c.nak.id) <<
",\n";
1978 out <<
" \"version\": " << json::str(c.nak.version) <<
",\n";
1979 out <<
" \"root\": " << json::str(c.nak.root) <<
",\n";
1980 out <<
" \"resource_root\": " << json::str(c.nak.resource_root) <<
",\n";
1981 out <<
" \"record_ref\": " << json::str(c.nak.record_ref) <<
",\n";
1982 out <<
" \"package_hash\": " << json::str(c.nak.package_hash) <<
"\n";
1986 out <<
" \"execution\": {\n";
1987 out <<
" \"binary\": " << json::str(c.execution.binary) <<
",\n";
1988 out <<
" \"arguments\": " << json::array(c.execution.arguments, 4) <<
",\n";
1989 out <<
" \"cwd\": " << json::str(c.execution.cwd) <<
",\n";
1990 out <<
" \"library_path_env_key\": " << json::str(c.execution.library_path_env_key) <<
",\n";
1991 out <<
" \"library_paths\": " << json::array(c.execution.library_paths, 4) <<
"\n";
1995 out <<
" \"environment\": " << json::object(c.environment, 2) <<
",\n";
1998 out <<
" \"permissions\": {\n";
1999 out <<
" \"filesystem\": " << json::array(c.permissions.filesystem, 4) <<
",\n";
2000 out <<
" \"network\": " << json::array(c.permissions.network, 4) <<
"\n";
2004 out <<
" \"trust\": {\n";
2005 out <<
" \"state\": " << json::str(trust_state_to_string(c.trust.state)) <<
",\n";
2006 out <<
" \"source\": " << json::str(c.trust.source) <<
",\n";
2007 out <<
" \"evaluated_at\": " << json::str(c.trust.evaluated_at) <<
",\n";
2008 out <<
" \"expires_at\": " << json::str(c.trust.expires_at) <<
"\n";
2018inline std::string serialize_result(
const CompositionResult& r) {
2019 std::ostringstream out;
2021 out <<
" \"ok\": " << (r.ok ?
"true" :
"false") <<
",\n";
2023 if (r.critical_error.has_value()) {
2024 out <<
" \"critical_error\": " << json::str(critical_error_to_string(*r.critical_error)) <<
",\n";
2025 out <<
" \"critical_error_context\": " << json::str(r.critical_error_context) <<
",\n";
2027 out <<
" \"critical_error\": null,\n";
2031 out <<
" \"warnings\": [\n";
2032 for (
size_t i = 0; i < r.warnings.size(); i++) {
2033 const auto& w = r.warnings[i];
2035 out <<
" \"key\": " << json::str(w.key) <<
",\n";
2036 out <<
" \"fields\": " << json::object(w.fields, 6) <<
"\n";
2038 if (i < r.warnings.size() - 1) out <<
",";
2044 out <<
" \"contract\": " << serialize_contract(r.contract) <<
"\n";
2046 out <<
" \"contract\": null\n";