luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

Run.lua (92310B)


      1 #!/usr/bin/env luajit
      2 -- Application Runner Module
      3 -- Handles running applications with sandboxing, permissions, and CLI buffer management
      4 
      5 -- Test if osprint is available when module loads
      6 if osprint then
      7     osprint("RUN MODULE LOADED: osprint is available!\n")
      8 end
      9 
     10 local run = {}
     11 
     12 -- Apps are registered in _G.sys.applications (maintained by Sys.lua)
     13 -- This provides a unified way to access running apps across the system
     14 local function get_app_registry()
     15     return (_G.sys and _G.sys.applications) or {}
     16 end
     17 
     18 -- Scheduler instance (loaded on demand)
     19 local scheduler = nil
     20 
     21 -- Utility functions that are needed for run functionality
     22 
     23 -- Utility: Split string by delimiter
     24 local function split(str, delimiter)
     25     local result = {}
     26     local pattern = string.format("([^%s]+)", delimiter)
     27     for match in string.gmatch(str, pattern) do
     28         table.insert(result, match)
     29     end
     30     return result
     31 end
     32 
     33 -- Utility: Parse command line arguments
     34 local function parse_args(argStr)
     35     if not argStr or argStr == "" then
     36         return { str = "" }
     37     end
     38 
     39     local args = { str = argStr }
     40     local current = ""
     41     local in_quote = false
     42     local raw_args = {}
     43 
     44     -- First pass: split into tokens
     45     for i = 1, #argStr do
     46         local c = argStr:sub(i, i)
     47         if c == '"' then
     48             in_quote = not in_quote
     49         elseif c == ' ' and not in_quote then
     50             if current ~= "" then
     51                 table.insert(raw_args, current)
     52                 current = ""
     53             end
     54         else
     55             current = current .. c
     56         end
     57     end
     58 
     59     if current ~= "" then
     60         table.insert(raw_args, current)
     61     end
     62 
     63     -- Second pass: parse flags and populate args table
     64     local i = 1
     65     local positional_index = 1
     66     while i <= #raw_args do
     67         local arg = raw_args[i]
     68 
     69         -- Check if it's a flag (-X or --X)
     70         local flag_name = nil
     71         if arg:match("^%-%-(.+)") then
     72             -- Long flag: --name
     73             flag_name = arg:match("^%-%-(.+)")
     74         elseif arg:match("^%-(.+)") then
     75             -- Short flag: -n
     76             flag_name = arg:match("^%-(.+)")
     77         end
     78 
     79         if flag_name then
     80             -- It's a flag - check if it's "str" or a number (reserved names)
     81             if flag_name ~= "str" and not tonumber(flag_name) then
     82                 -- Get the next value if it exists and isn't another flag
     83                 if i + 1 <= #raw_args and not raw_args[i + 1]:match("^%-") then
     84                     args[flag_name] = raw_args[i + 1]
     85                     i = i + 1  -- Skip the value in next iteration
     86                 else
     87                     -- Flag without value, set to true
     88                     args[flag_name] = true
     89                 end
     90             end
     91         else
     92             -- Not a flag, add as positional argument
     93             args[positional_index] = arg
     94             positional_index = positional_index + 1
     95         end
     96 
     97         i = i + 1
     98     end
     99 
    100     return args
    101 end
    102 
    103 -- Utility: Deep copy a table
    104 local function deep_copy(obj)
    105     if type(obj) ~= 'table' then
    106         return obj
    107     end
    108 
    109     local copy = {}
    110     for k, v in pairs(obj) do
    111         copy[k] = deep_copy(v)
    112     end
    113 
    114     return copy
    115 end
    116 
    117 -- Utility: Parse manifest (simple Lua code execution)
    118 -- Parse manifest content as text (no Lua execution for security)
    119 local function parse_manifest(manifest_code)
    120     if not manifest_code then
    121         return {}
    122     end
    123 
    124     -- Remove "return" if present at start
    125     manifest_code = string.gsub(manifest_code, "^%s*return%s*", "")
    126 
    127     -- Remove outer braces only if both opening and closing exist as a pair
    128     local trimmed = string.gsub(manifest_code, "^%s+", "")
    129     trimmed = string.gsub(trimmed, "%s+$", "")
    130     if string.sub(trimmed, 1, 1) == "{" and string.sub(trimmed, -1) == "}" then
    131         manifest_code = string.sub(trimmed, 2, -2)
    132     end
    133 
    134     -- Extract [[long strings]] before removing comments (to preserve their content)
    135     local long_strings = {}
    136     local placeholder_idx = 0
    137     manifest_code = string.gsub(manifest_code, "%[%[(.-)%]%]", function(content)
    138         placeholder_idx = placeholder_idx + 1
    139         local placeholder = "\001LONGSTR" .. placeholder_idx .. "\001"
    140         long_strings[placeholder] = content
    141         return placeholder
    142     end)
    143 
    144     -- Remove single-line comments (-- to end of line)
    145     manifest_code = string.gsub(manifest_code, "%-%-[^\n]*", "")
    146 
    147     -- Normalize whitespace: replace newlines/tabs with spaces, collapse multiple spaces
    148     manifest_code = string.gsub(manifest_code, "[\r\n\t]+", " ")
    149     manifest_code = string.gsub(manifest_code, " +", " ")
    150 
    151     local manifest = {}
    152 
    153     -- Helper to parse array content (handles [[strings]] in arrays)
    154     local function parse_array_item(item)
    155         item = string.gsub(item, "^%s+", "")
    156         item = string.gsub(item, "%s+$", "")
    157         -- Check for long string placeholder
    158         if long_strings[item] then
    159             return long_strings[item]
    160         elseif string.match(item, '^".-"$') then
    161             return string.sub(item, 2, -2)
    162         elseif string.match(item, "^'.-'$") then
    163             return string.sub(item, 2, -2)
    164         elseif item ~= "" then
    165             return item
    166         end
    167         return nil
    168     end
    169 
    170     -- Match key = value pairs (handles arrays with spaces like { "a", "b" })
    171     -- Pattern: word = (value until next word= or end)
    172     local pos = 1
    173     while pos <= #manifest_code do
    174         -- Skip whitespace, commas, and semicolons
    175         local ws_end = string.match(manifest_code, "^[%s,;]*()", pos)
    176         if ws_end then pos = ws_end end
    177 
    178         -- Match key = value
    179         local key, value_start = string.match(manifest_code, "^([%w_]+)%s*=%s*()", pos)
    180         if not key then break end
    181 
    182         pos = value_start
    183 
    184         -- Determine value type and extract it
    185         local value
    186         local char = string.sub(manifest_code, pos, pos)
    187 
    188         if char == "{" then
    189             -- Array: find matching } by counting brace depth
    190             local depth = 1
    191             local brace_end = pos + 1
    192             while brace_end <= #manifest_code and depth > 0 do
    193                 local c = string.sub(manifest_code, brace_end, brace_end)
    194                 if c == "{" then
    195                     depth = depth + 1
    196                 elseif c == "}" then
    197                     depth = depth - 1
    198                 end
    199                 brace_end = brace_end + 1
    200             end
    201             brace_end = brace_end - 1  -- Point to the closing }
    202             if depth == 0 then
    203                 local arr_content = string.sub(manifest_code, pos + 1, brace_end - 1)
    204                 local arr = {}
    205                 for item in string.gmatch(arr_content, '[^,;]+') do
    206                     local parsed = parse_array_item(item)
    207                     if parsed then
    208                         table.insert(arr, parsed)
    209                     end
    210                 end
    211                 value = arr
    212                 pos = brace_end + 1
    213             else
    214                 break
    215             end
    216         elseif char == "\001" then
    217             -- Long string placeholder
    218             local placeholder = string.match(manifest_code, "^(\001LONGSTR%d+\001)", pos)
    219             if placeholder and long_strings[placeholder] then
    220                 value = long_strings[placeholder]
    221                 pos = pos + #placeholder
    222             else
    223                 break
    224             end
    225         elseif char == '"' then
    226             -- Double-quoted string
    227             local str_end = string.find(manifest_code, '"', pos + 1, true)
    228             if str_end then
    229                 value = string.sub(manifest_code, pos + 1, str_end - 1)
    230                 pos = str_end + 1
    231             else
    232                 break
    233             end
    234         elseif char == "'" then
    235             -- Single-quoted string
    236             local str_end = string.find(manifest_code, "'", pos + 1, true)
    237             if str_end then
    238                 value = string.sub(manifest_code, pos + 1, str_end - 1)
    239                 pos = str_end + 1
    240             else
    241                 break
    242             end
    243         else
    244             -- Unquoted value (boolean, number, or identifier)
    245             local val_str = string.match(manifest_code, "^([^%s,;}]+)", pos)
    246             if val_str then
    247                 if val_str == "true" then
    248                     value = true
    249                 elseif val_str == "false" then
    250                     value = false
    251                 elseif tonumber(val_str) then
    252                     value = tonumber(val_str)
    253                 else
    254                     value = val_str
    255                 end
    256                 pos = pos + #val_str
    257             else
    258                 break
    259             end
    260         end
    261 
    262         if key and value ~= nil then
    263             manifest[key] = value
    264         end
    265     end
    266 
    267     return manifest
    268 end
    269 
    270 -- Utility: Check if directory exists
    271 local function dir_exists(path)
    272     local ok, err, code = os.rename(path, path)
    273     if not ok then
    274         if code == 13 then
    275             return true  -- Permission denied, but exists
    276         end
    277     end
    278     return ok
    279 end
    280 
    281 -- Utility: Read file content
    282 local function read_file(filepath)
    283     local file = io.open(filepath, "rb")
    284     if not file then
    285         return nil
    286     end
    287     local content = file:read("*all")
    288     file:close()
    289     return content
    290 end
    291 
    292 -- Helper function to read from ramdisk
    293 local function read_from_ramdisk(fsRoot, filepath)
    294     if not fsRoot then
    295         -- Use CRamdisk functions when no fsRoot available
    296         if CRamdiskOpen and CRamdiskRead and CRamdiskClose then
    297             local handle = CRamdiskOpen(filepath, "r")
    298             if handle then
    299                 local content = CRamdiskRead(handle)
    300                 CRamdiskClose(handle)
    301                 return content
    302             end
    303         end
    304         -- Fallback to regular file reading
    305         return read_file(filepath)
    306     end
    307 
    308     -- Try fsRoot:traverse first (works with Lua ramdisk implementation)
    309     local node = fsRoot:traverse(filepath)
    310     if node and node.type == "file" then
    311         return node.content
    312     end
    313 
    314     -- Fallback to CRamdiskRead if available (C ramdisk API)
    315     if CRamdiskRead then
    316         -- Strip leading slash for CRamdiskRead
    317         local path = filepath
    318         if path:sub(1,1) == "/" then
    319             path = path:sub(2)
    320         end
    321         local result = CRamdiskRead(path)
    322         return result
    323     end
    324 
    325     return nil
    326 end
    327 
    328 -- Find installed apps by name
    329 local function find_apps(app_name, fsRoot)
    330     local apps_dir = "/apps"
    331     local matches = {}
    332 
    333     -- Always use CRamdisk functions for finding apps (more reliable than SafeFS)
    334     if osprint then
    335         osprint("[find_apps] Looking for '" .. app_name .. "' in " .. apps_dir .. "\n")
    336     end
    337 
    338     -- Check if direct match (com.dev.appname or com.*.appname)
    339     if string.match(app_name, "^com%.") then
    340         local app_path = apps_dir .. "/" .. app_name
    341         -- Use CRamdiskExists to check if directory exists
    342         if CRamdiskExists and CRamdiskExists(app_path) then
    343             if osprint then
    344                 osprint("[find_apps] Direct match found: " .. app_name .. "\n")
    345             end
    346             table.insert(matches, app_name)
    347         end
    348         return matches
    349     end
    350 
    351     -- Search for matches using CRamdiskList
    352     if CRamdiskList then
    353         if osprint then
    354             osprint("[find_apps] Using CRamdiskList to search...\n")
    355         end
    356         local entries = CRamdiskList(apps_dir)
    357         if entries then
    358             if osprint then
    359                 osprint("[find_apps] Found " .. #entries .. " entries in " .. apps_dir .. "\n")
    360             end
    361             for _, entry in ipairs(entries) do
    362                 if osprint then
    363                     osprint("[find_apps]   Entry: '" .. entry.name .. "' type=" .. entry.type .. "\n")
    364                 end
    365                 local pattern = "%." .. app_name .. "$"
    366                 if osprint then
    367                     osprint("[find_apps]   Testing pattern: '" .. pattern .. "' against '" .. entry.name .. "'\n")
    368                 end
    369                 if (entry.type == "directory" or entry.type == "dir") and string.match(entry.name, pattern) then
    370                     if osprint then
    371                         osprint("[find_apps]   MATCH: " .. entry.name .. "\n")
    372                     end
    373                     table.insert(matches, entry.name)
    374                 end
    375             end
    376         else
    377             if osprint then
    378                 osprint("[find_apps] CRamdiskList returned nil\n")
    379             end
    380         end
    381     else
    382         if osprint then
    383             osprint("[find_apps] CRamdiskList not available\n")
    384         end
    385     end
    386 
    387     if osprint then
    388         osprint("[find_apps] Total matches: " .. #matches .. "\n")
    389     end
    390 
    391     return matches
    392 end
    393 
    394 -- Extract embedded manifest from Lua script
    395 -- Looks for comment block between ----- MANIFEST START and ----- MANIFEST END
    396 -- Returns manifest table or nil if not found
    397 local function extract_embedded_manifest(script_content)
    398     if not script_content then
    399         return nil
    400     end
    401 
    402     -- Look for --[[ MANIFEST ... --]] block comment format
    403     local manifest_block = string.match(script_content, "%-%-%[%[%s*MANIFEST%s*(.-)%-%-%]%]")
    404 
    405     if not manifest_block then
    406         return nil
    407     end
    408 
    409     -- Reuse parse_manifest which handles multi-line arrays and text-based parsing
    410     local manifest = parse_manifest(manifest_block)
    411 
    412     -- Return nil if manifest is empty
    413     local has_keys = false
    414     for _ in pairs(manifest) do
    415         has_keys = true
    416         break
    417     end
    418 
    419     return has_keys and manifest or nil
    420 end
    421 
    422 -- Run an installed app with sandboxing
    423 function run.execute(app_name, fsRoot)
    424     -- Use osprint directly in run code (global print doesn't work in bare metal)
    425     local run_print = osprint or print
    426 
    427     if osprint then
    428         osprint("[RUN.EXECUTE] Called with app_name='" .. tostring(app_name) .. "' fsRoot=" .. tostring(fsRoot) .. "\n")
    429     end
    430 
    431     -- Utility functions are now defined locally in this module (no LPM dependency)
    432 
    433     -- Check if this is a direct .lua file execution
    434     local is_lua_script = string.match(app_name, "%.lua$") ~= nil
    435     local selected_app, init_content, init_file, app_dir, data_dir
    436     local permissions = {}
    437     local manifest = {}
    438     local app_instance = nil
    439 
    440     if is_lua_script then
    441         -- Direct .lua file execution
    442         run_print("Running Lua script: " .. app_name .. "\n")
    443 
    444         -- If script name has no path separator, look in /scripts/
    445         if not string.match(app_name, "/") then
    446             init_file = "/scripts/" .. app_name
    447             run_print("Looking for script in: " .. init_file .. "\n")
    448         else
    449             init_file = app_name
    450         end
    451 
    452         -- Read the script file
    453         init_content = read_from_ramdisk(fsRoot, init_file)
    454 
    455         if not init_content then
    456             local err_msg = "Script file not found: " .. init_file
    457             run_print("Error: " .. err_msg .. "\n")
    458             return false, err_msg
    459         end
    460 
    461         -- Extract embedded manifest
    462         local extracted_manifest = extract_embedded_manifest(init_content)
    463 
    464         if extracted_manifest then
    465             manifest = extracted_manifest
    466             run_print("Found embedded manifest\n")
    467 
    468             -- Parse permissions from embedded manifest
    469             if manifest.permission then
    470                 if type(manifest.permission) == "table" then
    471                     for _, perm in ipairs(manifest.permission) do
    472                         if type(perm) == "string" then
    473                             table.insert(permissions, perm)
    474                         end
    475                     end
    476                 end
    477             elseif manifest.permissions then
    478                 if type(manifest.permissions) == "table" then
    479                     for _, perm in ipairs(manifest.permissions) do
    480                         if type(perm) == "string" then
    481                             table.insert(permissions, perm)
    482                         end
    483                     end
    484                 end
    485             end
    486         else
    487             run_print("No embedded manifest found, running with no permissions\n")
    488             manifest = {}  -- Ensure manifest is an empty table, not nil
    489         end
    490 
    491         -- For standalone scripts, use a simple app name based on the file path
    492         selected_app = string.gsub(app_name, "%.lua$", "")
    493         selected_app = string.gsub(selected_app, "^/", "")
    494         selected_app = string.gsub(selected_app, "/", ".")
    495 
    496         -- Create a hash of the script path for consistent proc directory
    497         local script_hash = 0
    498         for i = 1, #init_file do
    499             script_hash = (script_hash * 31 + string.byte(init_file, i)) % 0x7FFFFFFF
    500         end
    501 
    502         -- Use /proc/script_<hash> as the data directory
    503         app_dir = "/proc/script_" .. string.format("%08x", script_hash)
    504         data_dir = app_dir
    505 
    506     else
    507         -- Standard app execution from /apps directory
    508         local matches = find_apps(app_name, fsRoot)
    509 
    510         if #matches == 0 then
    511             local err_msg = "No app found matching '" .. app_name .. "'"
    512             run_print("Error: " .. err_msg .. "\n")
    513             return false, err_msg
    514         end
    515 
    516         if #matches > 1 then
    517             run_print("Multiple apps found:\n")
    518             for i, app in ipairs(matches) do
    519                 run_print(i .. ": " .. app .. "\n")
    520             end
    521             run_print("Enter selection (1-" .. #matches .. "): \n")
    522             local choice = tonumber(io.read())
    523 
    524             if not choice or choice < 1 or choice > #matches then
    525                 local err_msg = "Invalid selection"
    526                 run_print(err_msg .. "\n")
    527                 return false, err_msg
    528             end
    529 
    530             selected_app = matches[choice]
    531         else
    532             selected_app = matches[1]
    533         end
    534 
    535         local apps_dir = "/apps"
    536         app_dir = apps_dir .. "/" .. selected_app
    537         data_dir = app_dir .. "/data"
    538         local manifest_file = app_dir .. "/manifest.lua"
    539 
    540         -- Read manifest to get permissions and entry point
    541         local manifest_content = read_from_ramdisk(fsRoot, manifest_file)
    542 
    543         local entry_point = "src/init.lua"  -- Default entry point
    544 
    545         if not manifest_content then
    546             run_print("Warning: manifest.lua not found in " .. manifest_file .. ", running with no permissions\n")
    547             -- Continue with empty permissions and manifest
    548         else
    549             manifest = parse_manifest(manifest_content)
    550 
    551             -- Parse permissions into array of strings
    552             if manifest.permissions then
    553                 if type(manifest.permissions) == "table" then
    554                     for _, perm in ipairs(manifest.permissions) do
    555                         if type(perm) == "string" then
    556                             table.insert(permissions, perm)
    557                         end
    558                     end
    559                 end
    560             end
    561 
    562             -- Get entry point from manifest (default to src/init.lua)
    563             -- Entry is always relative to $/src/
    564             if manifest.entry and type(manifest.entry) == "string" then
    565                 entry_point = "src/" .. manifest.entry
    566             end
    567         end
    568 
    569         run_print("Running: " .. selected_app .. "\n")
    570         run_print("Entry point: " .. entry_point .. "\n")
    571 
    572         -- Build full path to entry file
    573         init_file = app_dir .. "/" .. entry_point
    574 
    575         -- Check if entry file exists
    576         init_content = read_from_ramdisk(fsRoot, init_file)
    577         if not init_content then
    578             local err_msg = "Entry file not found: " .. init_file
    579             run_print("Error: " .. err_msg .. "\n")
    580             return false, err_msg
    581         end
    582     end
    583 
    584     -- Common code for both script and app execution
    585     run_print("Permissions: " .. table.concat(permissions, ", ") .. "\n")
    586 
    587     -- Create sandboxed environment
    588     local sandbox_env = {
    589         -- Basic Lua functions
    590         print = nil,  -- Will be set later after app_instance is created
    591         osprint = osprint,  -- Also expose osprint directly for explicit use
    592         tonumber = tonumber,
    593         tostring = tostring,
    594         type = type,
    595         pairs = pairs,
    596         ipairs = ipairs,
    597         next = next,
    598         select = select,
    599         assert = assert,
    600         error = error,
    601         pcall = pcall,
    602         xpcall = xpcall,
    603         setmetatable = setmetatable,
    604         getmetatable = getmetatable,
    605 
    606         -- Safe standard libraries
    607         string = string,
    608         table = table,
    609         math = math,
    610         bit = bit,  -- LuaJIT bit operations library
    611 
    612         -- Restricted os library (only safe functions)
    613         os = {
    614             date = os.date,
    615             time = os.time,
    616             difftime = os.difftime,
    617             clock = os.clock,
    618         },
    619 
    620         -- Pre-define optional items as nil so accessing them doesn't throw
    621         fs = nil,  -- SafeFS instance (set later if filesystem permission granted)
    622     }
    623 
    624     -- Add optional globals only if they exist (avoid nil values in sandbox)
    625     if _G.Dialog then
    626         sandbox_env.Dialog = _G.Dialog
    627     end
    628 
    629     -- Check for crypto permission and add crypto object
    630     local has_crypto = false
    631     for _, perm in ipairs(permissions) do
    632         if perm == "crypto" then
    633             has_crypto = true
    634             break
    635         end
    636     end
    637     if has_crypto and _G.crypto then
    638         sandbox_env.crypto = _G.crypto
    639         if osprint then
    640             osprint("Crypto permission granted - crypto object available\n")
    641         end
    642     end
    643 
    644     -- Check for system-all permission and add sys object
    645     local has_system_all = false
    646     for _, perm in ipairs(permissions) do
    647         if perm == "system-all" then
    648             has_system_all = true
    649             break
    650         end
    651     end
    652 
    653     -- Check for admin permission first
    654     local has_admin = false
    655     for _, perm in ipairs(permissions) do
    656         if perm == "admin" then
    657             has_admin = true
    658             break
    659         end
    660     end
    661 
    662     -- Check for system-hook permission
    663     local has_system_hook = false
    664     for _, perm in ipairs(permissions) do
    665         if perm == "system-hook" then
    666             has_system_hook = true
    667             break
    668         end
    669     end
    670 
    671     -- Check for system-apps permission
    672     local has_system_apps = false
    673     for _, perm in ipairs(permissions) do
    674         if perm == "system-apps" then
    675             has_system_apps = true
    676             break
    677         end
    678     end
    679 
    680     if has_system_all then
    681         -- Create a proxy for sys that:
    682         -- 1. Blocks hook unless system-hook permission
    683         -- 2. Exposes applications as 'apps' only if system-apps permission
    684         -- 3. Blocks direct access to 'applications'
    685         sandbox_env.sys = setmetatable({}, {
    686             __index = function(t, k)
    687                 if k == "hook" and not has_system_hook then
    688                     error("sys.hook requires 'system-hook' permission")
    689                 end
    690                 if k == "applications" then
    691                     error("sys.applications is not accessible, use sys.apps with 'system-apps' permission")
    692                 end
    693                 if k == "apps" then
    694                     if has_system_apps then
    695                         return _G.sys.applications
    696                     else
    697                         error("sys.apps requires 'system-apps' permission")
    698                     end
    699                 end
    700                 return _G.sys[k]
    701             end,
    702             __newindex = function(t, k, v)
    703                 if k == "hook" and not has_system_hook then
    704                     error("sys.hook requires 'system-hook' permission")
    705                 end
    706                 if k == "applications" or k == "apps" then
    707                     error("Cannot modify sys.apps")
    708                 end
    709                 _G.sys[k] = v
    710             end,
    711             __pairs = function(t)
    712                 return function(tbl, k)
    713                     local nextKey, nextValue = next(_G.sys, k)
    714                     -- Skip hook if no permission
    715                     if nextKey == "hook" and not has_system_hook then
    716                         nextKey, nextValue = next(_G.sys, nextKey)
    717                     end
    718                     -- Skip applications (exposed as apps)
    719                     if nextKey == "applications" then
    720                         nextKey, nextValue = next(_G.sys, nextKey)
    721                     end
    722                     return nextKey, nextValue
    723                 end, t, nil
    724             end,
    725             __ipairs = function(t)
    726                 return ipairs(_G.sys)
    727             end,
    728             __metatable = false  -- Prevent metatable access/modification
    729         })
    730         if osprint then
    731             osprint("System-all permission granted - sys object available\n")
    732             if has_system_hook then osprint("  - sys.hook available\n") end
    733             if has_system_apps then osprint("  - sys.apps available\n") end
    734         end
    735     end
    736 
    737     if has_admin then
    738         -- Create admin API object with restricted admin functions
    739         sandbox_env.ADMIN_AppAddPermission = _G.sys.ADMIN_AppAddPermission
    740         sandbox_env.ADMIN_AppAddPath = _G.sys.ADMIN_AppAddPath
    741         sandbox_env.ADMIN_StartPrompt = _G.sys.ADMIN_StartPrompt
    742         sandbox_env.ADMIN_FinishPrompt = _G.sys.ADMIN_FinishPrompt
    743         if osprint then
    744             osprint("Admin permission granted - admin functions available\n")
    745         end
    746     end
    747 
    748     -- Check for filesystem permission and setup SafeFS
    749     -- Also enable if manifest has allowedPaths (needs SafeFS for path access)
    750     local has_filesystem = false
    751     for _, perm in ipairs(permissions) do
    752         if perm == "filesystem" then
    753             has_filesystem = true
    754             break
    755         end
    756     end
    757     -- Enable SafeFS if manifest has allowedPaths defined
    758     if not has_filesystem and manifest and manifest.allowedPaths and #manifest.allowedPaths > 0 then
    759         has_filesystem = true
    760     end
    761 
    762     -- Load Application module (always load, not just for filesystem permission)
    763     local Application = nil
    764     local app_module_path = "/os/libs/Application.lua"
    765     local app_module_code = nil
    766 
    767     -- Try to load from fsRoot first, then fall back to CRamdisk
    768     if fsRoot then
    769         app_module_code = read_from_ramdisk(fsRoot, app_module_path)
    770     elseif CRamdiskOpen then
    771         -- Fall back to direct CRamdisk access when fsRoot is nil
    772         local handle = CRamdiskOpen(app_module_path, "r")
    773         if handle then
    774             app_module_code = CRamdiskRead(handle)
    775             CRamdiskClose(handle)
    776         end
    777     end
    778 
    779     if app_module_code then
    780         if osprint then
    781             osprint("[run] Application.lua loaded, size=" .. #app_module_code .. " bytes\n")
    782         end
    783 
    784         -- Initialize global sys table if it doesn't exist
    785         if not _G.sys then
    786                 _G.sys = {
    787                     used_pids = {},
    788                     fallback_pid = 1000,
    789                     app_timestamp = 0
    790                 }
    791             end
    792 
    793             -- Load Application module in a temporary environment
    794             local app_env = {
    795                 sys = _G.sys  -- Explicitly provide sys to Application.lua
    796             }
    797             setmetatable(app_env, {__index = _G, __metatable = false})
    798             local app_func, err = load(app_module_code, app_module_path, "t", app_env)
    799 
    800             if app_func then
    801                 local success, AppModule = pcall(app_func)
    802                 if success and AppModule then
    803                     Application = AppModule
    804                     if osprint then osprint("Application module loaded successfully!\n") end
    805                 else
    806                     if osprint then osprint("Warning: Failed to load Application module: " .. tostring(AppModule) .. "\n") end
    807                 end
    808             else
    809                 if osprint then osprint("Warning: Failed to compile Application module: " .. tostring(err) .. "\n") end
    810             end
    811     else
    812         if osprint then osprint("Warning: Application module not found at " .. app_module_path .. "\n") end
    813     end
    814 
    815     -- Create Application instance for this app
    816     if Application then
    817         if osprint then
    818             osprint("[run] Creating Application instance for " .. selected_app .. "\n")
    819         end
    820         app_instance = Application.new(app_dir, {
    821             name = selected_app
    822         })
    823         -- Set status to running
    824         app_instance:setStatus("running")
    825 
    826         -- Store permissions and allowed paths in app instance
    827         app_instance.permissions = permissions
    828         app_instance.allowedPaths = (manifest and manifest.allowedPaths) or {}
    829 
    830         -- Store manifest as read-only deep copy to prevent tampering
    831         local function deepCopyReadOnly(tbl)
    832             if type(tbl) ~= "table" then
    833                 return tbl
    834             end
    835 
    836             local copy = {}
    837             for k, v in pairs(tbl) do
    838                 copy[k] = deepCopyReadOnly(v)
    839             end
    840 
    841             -- Make table read-only with metatable
    842             return setmetatable({}, {
    843                 __index = copy,
    844                 __newindex = function()
    845                     error("Manifest is read-only and cannot be modified")
    846                 end,
    847                 __metatable = false  -- Hide metatable
    848             })
    849         end
    850 
    851         app_instance.manifest = manifest and deepCopyReadOnly(manifest) or {}
    852 
    853         -- Load application icon
    854         local iconLoaded = false
    855         local defaultIconPath = "/os/res/default.bmp"
    856 
    857         -- Try loading app-specific icon (PNG first, then JPEG, then BMP)
    858         local iconPaths = {
    859             app_dir .. "/icon.png",
    860             app_dir .. "/icon.jpg",
    861             app_dir .. "/icon.jpeg",
    862             app_dir .. "/icon.bmp"
    863         }
    864 
    865         for _, iconPath in ipairs(iconPaths) do
    866             if CRamdiskExists and CRamdiskExists(iconPath) then
    867                 local handle = CRamdiskOpen(iconPath, "r")
    868                 if handle then
    869                     local iconData = CRamdiskRead(handle)
    870                     CRamdiskClose(handle)
    871 
    872                     if iconData then
    873                         local img = nil
    874                         if iconPath:match("%.png$") and PNGLoad then
    875                             img = PNGLoad(iconData)
    876                         elseif (iconPath:match("%.jpg$") or iconPath:match("%.jpeg$")) and JPEGLoad then
    877                             img = JPEGLoad(iconData)
    878                         elseif iconPath:match("%.bmp$") and BMPLoad then
    879                             img = BMPLoad(iconData)
    880                         end
    881 
    882                         if img then
    883                             local info = ImageGetInfo and ImageGetInfo(img)
    884                             if info and info.width and info.height then
    885                                 app_instance.iconBuffer = img
    886                                 app_instance.iconWidth = info.width
    887                                 app_instance.iconHeight = info.height
    888                                 iconLoaded = true
    889                                 if osprint then
    890                                     osprint("[run] Loaded icon: " .. iconPath .. " (" .. info.width .. "x" .. info.height .. ")\n")
    891                                 end
    892                                 break
    893                             end
    894                         end
    895                     end
    896                 end
    897             end
    898         end
    899 
    900         -- Fall back to default icon if app icon not found
    901         if not iconLoaded and CRamdiskExists and CRamdiskExists(defaultIconPath) then
    902             local handle = CRamdiskOpen(defaultIconPath, "r")
    903             if handle then
    904                 local iconData = CRamdiskRead(handle)
    905                 CRamdiskClose(handle)
    906 
    907                 if iconData and BMPLoad then
    908                     local img = BMPLoad(iconData)
    909                     if img then
    910                         local info = ImageGetInfo and ImageGetInfo(img)
    911                         if info and info.width and info.height then
    912                             app_instance.iconBuffer = img
    913                             app_instance.iconWidth = info.width
    914                             app_instance.iconHeight = info.height
    915                             if osprint then
    916                                 osprint("[run] Loaded default icon for " .. selected_app .. "\n")
    917                             end
    918                         end
    919                     end
    920                 end
    921             end
    922         end
    923 
    924         -- Create /proc/$PID directory with process file
    925         if CRamdiskMkdir and CRamdiskWrite then
    926             local proc_dir = "/proc/" .. app_instance.pid
    927             local success, err = CRamdiskMkdir(proc_dir)
    928 
    929             if success or (err and err:match("already exists")) then
    930                 -- Create process file containing the app path
    931                 local process_file = proc_dir .. "/process"
    932                 local write_success = CRamdiskWrite(process_file, app_dir)
    933 
    934                 if write_success then
    935                     if osprint then
    936                         osprint("Created " .. process_file .. " -> " .. app_dir .. "\n")
    937                     end
    938                 else
    939                     if osprint then
    940                         osprint("WARNING: Failed to create " .. process_file .. "\n")
    941                     end
    942                 end
    943             else
    944                 if osprint then
    945                     osprint("WARNING: Failed to create " .. proc_dir .. ": " .. tostring(err) .. "\n")
    946                 end
    947             end
    948         end
    949 
    950         -- Register in sys.applications using PID as key
    951         if osprint then
    952             osprint("[run] Attempting to register app, _G.sys = " .. tostring(_G.sys) .. "\n")
    953             if _G.sys then
    954                 osprint("[run] _G.sys.registerApplication = " .. tostring(_G.sys.registerApplication) .. "\n")
    955             end
    956         end
    957 
    958         if _G.sys and _G.sys.registerApplication then
    959             if osprint then
    960                 osprint("[run] Calling _G.sys.registerApplication for PID " .. app_instance.pid .. "\n")
    961             end
    962             _G.sys.registerApplication(app_instance)
    963         else
    964             if osprint then
    965                 osprint("[run] ERROR: Cannot register app - sys or registerApplication missing!\n")
    966             end
    967         end
    968 
    969         -- Create a proxy for app that only exposes safe methods
    970         -- This prevents apps from directly modifying exports, windows, etc.
    971         local safe_app_methods = {
    972             "export", "call", "getExport", "listExports", "getInfo",
    973             "getExportCount", "getHelp", "printExports",
    974             "writeStdout", "getStdout", "clearStdout",
    975             "newWindow", "enterFullscreen", "exitFullscreen"
    976         }
    977         -- Also expose these read-only properties
    978         local safe_app_properties = {
    979             "appName", "pid", "startTime", "status", "path"
    980         }
    981         local app_proxy = {}
    982         for _, method in ipairs(safe_app_methods) do
    983             if app_instance[method] then
    984                 app_proxy[method] = function(self, ...)
    985                     return app_instance[method](app_instance, ...)
    986                 end
    987             end
    988         end
    989         setmetatable(app_proxy, {
    990             __index = function(t, k)
    991                 -- Allow read-only access to safe properties
    992                 for _, prop in ipairs(safe_app_properties) do
    993                     if k == prop then
    994                         return app_instance[k]
    995                     end
    996                 end
    997                 error("app." .. tostring(k) .. " is not accessible", 2)
    998             end,
    999             __newindex = function(t, k, v)
   1000                 -- Allow setting path if app has set-path permission
   1001                 if k == "path" and permissions["set-path"] then
   1002                     app_instance.path = v
   1003                     return
   1004                 end
   1005                 error("Cannot modify app." .. tostring(k), 2)
   1006             end,
   1007             __metatable = false
   1008         })
   1009 
   1010         -- Add to sandbox environment
   1011         sandbox_env.app = app_proxy
   1012 
   1013         -- Add Timer API scoped to this app
   1014         if _G.Timer then
   1015             sandbox_env.Timer = _G.Timer.createAPI(selected_app)
   1016         end
   1017 
   1018         -- Create print wrapper that captures output to app stdout
   1019         local sandbox_print = function(...)
   1020             -- Build output string
   1021             local n = select('#', ...)
   1022             local output = ""
   1023 
   1024             if n == 0 then
   1025                 output = "\n"
   1026             else
   1027                 local result = tostring(select(1, ...))
   1028                 for i = 2, n do
   1029                     result = result .. "\t" .. tostring(select(i, ...))
   1030                 end
   1031                 output = result .. "\n"
   1032             end
   1033 
   1034             -- Write to app stdout
   1035             app_instance:writeStdout(output)
   1036 
   1037             -- Also output to osprint if available (for debugging)
   1038             if osprint then
   1039                 osprint(output)
   1040             end
   1041         end
   1042 
   1043         -- Set the print function in sandbox
   1044         sandbox_env.print = sandbox_print
   1045 
   1046         if osprint then
   1047             osprint("Application instance created with PID: " .. app_instance.pid .. "\n")
   1048         end
   1049 
   1050         -- Check if parent CLI was passed (from run() function)
   1051         local parent_cli = _G.parentCli
   1052         local cli
   1053 
   1054         if parent_cli then
   1055             -- Create SafeCLI wrapper that only exposes write methods
   1056             -- DO NOT expose the parent's buffer directly
   1057             if osprint then
   1058                 osprint("Using parent CLI buffer (safe wrapper)\n")
   1059             end
   1060 
   1061             cli = {
   1062                 -- Safe write method - writes to parent buffer
   1063                 write = function(text)
   1064                     if parent_cli and parent_cli.write then
   1065                         parent_cli.write(text)
   1066                     end
   1067                 end,
   1068 
   1069                 -- Safe writeLine alias
   1070                 writeLine = function(text)
   1071                     if parent_cli and parent_cli.write then
   1072                         parent_cli.write(text)
   1073                     end
   1074                 end,
   1075 
   1076                 -- Safe getText method - read-only access to parent's buffer
   1077                 getText = function()
   1078                     if parent_cli and parent_cli.getText then
   1079                         return parent_cli.getText()
   1080                     end
   1081                     return ""
   1082                 end
   1083 
   1084                 -- DO NOT expose: buffer, clear
   1085                 -- buffer - direct access could allow modification
   1086                 -- clear - would clear parent's buffer
   1087             }
   1088         else
   1089             -- Create new CLI buffer table (always available, not just for CLI mode)
   1090             local cli_buffer = {
   1091                 lines = {},
   1092                 max_lines = 100  -- Store last 100 lines
   1093             }
   1094 
   1095             -- Create CLI helper table (always available for all apps)
   1096             cli = {
   1097                 buffer = cli_buffer,
   1098 
   1099                 -- Add line to buffer
   1100                 write = function(text)
   1101                     table.insert(cli_buffer.lines, tostring(text))
   1102                     -- Trim buffer if it exceeds max lines
   1103                     while #cli_buffer.lines > cli_buffer.max_lines do
   1104                         table.remove(cli_buffer.lines, 1)
   1105                     end
   1106                 end,
   1107 
   1108                 -- Clear buffer
   1109                 clear = function()
   1110                     cli_buffer.lines = {}
   1111                 end,
   1112 
   1113                 -- Get all buffer content as string
   1114                 getText = function()
   1115                     return table.concat(cli_buffer.lines, "\n")
   1116                 end
   1117             }
   1118 
   1119             -- Add writeLine as an alias for write
   1120             cli.writeLine = cli.write
   1121         end
   1122 
   1123         -- Add safe cli table to sandbox environment (for all apps)
   1124         sandbox_env.cli = cli
   1125 
   1126         -- Parse command line arguments from _G.args.argStr
   1127         local parsed_args = {}
   1128         if _G.args and _G.args.argStr then
   1129             parsed_args = parse_args(_G.args.argStr)
   1130             if osprint then
   1131                 osprint("Parsed arguments from argStr: " .. _G.args.argStr .. "\n")
   1132             end
   1133         end
   1134 
   1135         -- Add parsed args to sandbox environment
   1136         sandbox_env.args = parsed_args
   1137 
   1138         -- Modify print to also call cli.write
   1139         local original_print = sandbox_print
   1140         local using_parent_cli = (_G.parentCli ~= nil)
   1141 
   1142         sandbox_env.print = function(...)
   1143             -- Only call original print if NOT using parent CLI
   1144             -- (to avoid duplicate output to stdout when running from shell)
   1145             if not using_parent_cli then
   1146                 original_print(...)
   1147             end
   1148 
   1149             -- Write to CLI buffer (either parent's or own)
   1150             local n = select('#', ...)
   1151             if n == 0 then
   1152                 cli.write("")
   1153             else
   1154                 local result = tostring(select(1, ...))
   1155                 for i = 2, n do
   1156                     result = result .. "\t" .. tostring(select(i, ...))
   1157                 end
   1158                 cli.write(result)
   1159             end
   1160         end
   1161 
   1162         -- Check if app is in CLI mode and set up default draw handler
   1163         if manifest and manifest.type == "cli" then
   1164 
   1165             -- If app has draw permission, set up default draw method
   1166             local has_draw = false
   1167             for _, perm in ipairs(permissions) do
   1168                 if perm == "draw" then
   1169                     has_draw = true
   1170                     break
   1171                 end
   1172             end
   1173 
   1174             if has_draw and app_instance then
   1175                 -- Get the window if it exists
   1176                 local window = app_instance.window
   1177                 if window then
   1178                     -- Set up default onDraw handler that draws the CLI buffer
   1179                     window:onDraw(function(gfx)
   1180                         -- Draw black background
   1181                         gfx:fillRect(0, 0, window.width, window.height, 0x000000)
   1182 
   1183                         -- Draw CLI buffer text
   1184                         local text = cli.getText()
   1185                         gfx:drawText(10, 10, text, 0x00FF00)  -- Green text on black
   1186                     end)
   1187 
   1188                     if osprint then
   1189                         osprint("CLI mode enabled with default draw handler\n")
   1190                     end
   1191                 end
   1192             end
   1193         end
   1194     else
   1195         -- No Application module, create basic print wrapper
   1196         local sandbox_print = osprint and function(...)
   1197             local n = select('#', ...)
   1198             if n == 0 then
   1199                 osprint("\n")
   1200                 return
   1201             end
   1202 
   1203             local result = tostring(select(1, ...))
   1204             for i = 2, n do
   1205                 result = result .. "\t" .. tostring(select(i, ...))
   1206             end
   1207             osprint(result .. "\n")
   1208         end or print
   1209 
   1210         sandbox_env.print = sandbox_print
   1211     end
   1212 
   1213     if has_filesystem and fsRoot then
   1214         -- Load SafeFS module
   1215         local safefs_path = "/os/libs/SafeFS.lua"
   1216         local safefs_code = read_from_ramdisk(fsRoot, safefs_path)
   1217 
   1218         if safefs_code then
   1219             -- Load SafeFS in a temporary environment
   1220             local safefs_env = {}
   1221             setmetatable(safefs_env, {__index = _G, __metatable = false})
   1222             local safefs_func, err = load(safefs_code, safefs_path, "t", safefs_env)
   1223 
   1224             if safefs_func then
   1225                 local success, SafeFS = pcall(safefs_func)
   1226 
   1227                 if success and SafeFS then
   1228                     -- Build list of allowed paths
   1229                     local allowed_paths = {data_dir .. "/*"}  -- Always allow data directory
   1230 
   1231                     -- Add additional paths from manifest.allowedPaths
   1232                     if manifest and manifest.allowedPaths and type(manifest.allowedPaths) == "table" then
   1233                         for _, path in ipairs(manifest.allowedPaths) do
   1234                             if type(path) == "string" then
   1235                                 -- Replace $ with app directory
   1236                                 local resolved_path = string.gsub(path, "^%$", app_dir)
   1237                                 table.insert(allowed_paths, resolved_path)
   1238                                 if osprint then osprint("Additional path allowed: " .. resolved_path .. "\n") end
   1239                             end
   1240                         end
   1241                     end
   1242 
   1243                     -- Ensure data directory exists using C ramdisk functions
   1244                     if osprint then osprint("DEBUG: Checking data directory: " .. data_dir .. "\n") end
   1245                     if osprint then osprint("DEBUG: CRamdiskExists=" .. tostring(CRamdiskExists ~= nil) .. ", CRamdiskMkdir=" .. tostring(CRamdiskMkdir ~= nil) .. "\n") end
   1246                     if CRamdiskExists and CRamdiskMkdir then
   1247                         local dir_exists = CRamdiskExists(data_dir)
   1248                         if osprint then osprint("DEBUG: Directory exists: " .. tostring(dir_exists) .. "\n") end
   1249                         if not dir_exists then
   1250                             if osprint then osprint("Creating data directory: " .. data_dir .. "\n") end
   1251                             -- Create directory structure
   1252                             local parts = split(data_dir, "/")
   1253                             local path = ""
   1254                             for _, part in ipairs(parts) do
   1255                                 if part ~= "" then
   1256                                     path = path .. "/" .. part
   1257                                     if not CRamdiskExists(path) then
   1258                                         -- Create this directory
   1259                                         if osprint then osprint("  Creating: " .. path .. "\n") end
   1260                                         local success, err = CRamdiskMkdir(path)
   1261                                         if not success then
   1262                                             if osprint then osprint("  Failed to create directory: " .. tostring(err) .. "\n") end
   1263                                         else
   1264                                             if osprint then osprint("  Directory created successfully\n") end
   1265                                             -- Check if we can now find it
   1266                                             local exists_after = CRamdiskExists(path)
   1267                                             if osprint then osprint("  Exists after creation: " .. tostring(exists_after) .. "\n") end
   1268 
   1269                                             -- Try to get the directory node to see what was created
   1270                                             if CRamdiskList then
   1271                                                 local items = CRamdiskList(path)
   1272                                                 if osprint then
   1273                                                     osprint("  Directory listing: " .. tostring(items ~= nil) .. "\n")
   1274                                                     if items then
   1275                                                         osprint("  Number of items in directory: " .. tostring(#items) .. "\n")
   1276                                                     end
   1277                                                 end
   1278                                             end
   1279                                         end
   1280                                     else
   1281                                         if osprint then osprint("  Already exists: " .. path .. "\n") end
   1282                                     end
   1283                                 end
   1284                             end
   1285                         end
   1286                     end
   1287 
   1288                     -- Create SafeFS instance with all allowed paths
   1289                     -- Pass selected_app as the app ID for file handler registration
   1290                     -- Pass app_dir as the app path for $ expansion
   1291                     local fs_instance = SafeFS.new(fsRoot, allowed_paths, "app", selected_app, app_dir)
   1292 
   1293                     if fs_instance then
   1294                         -- Set default CWD to $/data if it exists in allowed paths
   1295                         local data_path = "/apps/" .. app_name .. "/data"
   1296                         for _, path in ipairs(allowed_paths) do
   1297                             if path == data_path or path == data_path .. "/*" then
   1298                                 fs_instance:setCWD(data_path)
   1299                                 break
   1300                             end
   1301                         end
   1302 
   1303                         -- Mount disk drives if app has /mnt/* access
   1304                         for _, path in ipairs(allowed_paths) do
   1305                             if path == "/mnt/*" or path == "/*" or path:match("^/mnt/hd%d") then
   1306                                 if fs_instance.mountAllDiskDrives then
   1307                                     local ok, msg = fs_instance:mountAllDiskDrives()
   1308                                     if osprint then
   1309                                         osprint("[RUN] Disk mount result: " .. tostring(msg) .. "\n")
   1310                                     end
   1311                                 end
   1312                                 break
   1313                             end
   1314                         end
   1315 
   1316                         -- Create a proxy table that only exposes safe methods
   1317                         -- This prevents apps from accessing internal properties like allowedDirs
   1318                         -- Note: createDiskFSMount and mountAllDiskDrives are NOT exposed - they're internal only
   1319                         local safe_fs_methods = {
   1320                             "resolvePath", "getCWD", "setCWD", "read", "write", "open",
   1321                             "delete", "dirs", "files", "exists", "getType", "mkdir",
   1322                             "fileName", "parentDir", "join", "relativeTo", "copy", "move",
   1323                             "createPseudoFile", "createPseudoDir", "addFileHandler",
   1324                             "removeFileHandler"
   1325                         }
   1326                         local fs_proxy = {}
   1327                         for _, method in ipairs(safe_fs_methods) do
   1328                             if fs_instance[method] then
   1329                                 fs_proxy[method] = function(self, ...)
   1330                                     return fs_instance[method](fs_instance, ...)
   1331                                 end
   1332                             end
   1333                         end
   1334                         setmetatable(fs_proxy, {
   1335                             __index = function(t, k)
   1336                                 error("fs." .. tostring(k) .. " is not accessible", 2)
   1337                             end,
   1338                             __newindex = function(t, k, v)
   1339                                 error("Cannot modify fs object", 2)
   1340                             end,
   1341                             __metatable = false
   1342                         })
   1343 
   1344                         -- Add to sandbox environment
   1345                         sandbox_env.fs = fs_proxy
   1346                         if osprint then
   1347                             osprint("[RUN] Set sandbox_env.fs (proxied) for " .. app_name .. "\n")
   1348                         end
   1349                     else
   1350                         if osprint then
   1351                             osprint("Warning: Failed to create SafeFS instance for " .. app_name .. "\n")
   1352                         end
   1353                     end
   1354 
   1355                     if osprint then
   1356                         osprint("Filesystem access granted to " .. app_name .. ":\n")
   1357                         for _, path in ipairs(allowed_paths) do
   1358                             osprint("  " .. path .. "\n")
   1359                         end
   1360                     end
   1361                 else
   1362                     if osprint then osprint("Warning: Failed to load SafeFS module: " .. tostring(SafeFS) .. "\n") end
   1363                 end
   1364             else
   1365                 if osprint then osprint("Warning: Failed to compile SafeFS: " .. tostring(err) .. "\n") end
   1366             end
   1367         else
   1368             if osprint then osprint("Warning: SafeFS module not found at " .. safefs_path .. "\n") end
   1369         end
   1370     end
   1371 
   1372     -- Check for network permission and setup SafeHTTP
   1373     local has_network = false
   1374     for _, perm in ipairs(permissions) do
   1375         if perm == "network" then
   1376             has_network = true
   1377             break
   1378         end
   1379     end
   1380 
   1381     if has_network and fsRoot then
   1382         -- Load SafeHTTP module
   1383         local safehttp_path = "/os/libs/SafeHTTP.lua"
   1384         local safehttp_code = read_from_ramdisk(fsRoot, safehttp_path)
   1385 
   1386         if safehttp_code then
   1387             local safehttp_func, err = load(safehttp_code, safehttp_path, "t")
   1388 
   1389             if safehttp_func then
   1390                 local success, SafeHTTP = pcall(safehttp_func)
   1391 
   1392                 if success and SafeHTTP then
   1393                     -- Get allowed domains from manifest
   1394                     local allowed_domains = (manifest and manifest.allowedDomains) or {}
   1395 
   1396                     -- If no domains specified but network permission granted, allow all (backwards compat)
   1397                     if #allowed_domains == 0 then
   1398                         if osprint then
   1399                             osprint("Warning: Network permission granted but no allowedDomains specified - blocking all HTTP\n")
   1400                         end
   1401                         -- Don't create SafeHTTP if no domains allowed
   1402                     else
   1403                         -- Get NetworkStack if available
   1404                         if _G.NetworkStack then
   1405                             -- Create SafeHTTP instance with allowed domains
   1406                             local http_instance = SafeHTTP.new(_G.NetworkStack, allowed_domains, {
   1407                                 timeout = 30,
   1408                                 max_size = 10485760,  -- 10MB default
   1409                                 user_agent = (manifest and manifest.name or "Script") .. "/" .. (manifest and manifest.version or "1.0")
   1410                             })
   1411 
   1412                             -- Add to sandbox environment
   1413                             sandbox_env.http = http_instance
   1414 
   1415                             if osprint then
   1416                                 osprint("Network access granted to domains:\n")
   1417                                 for _, domain in ipairs(allowed_domains) do
   1418                                     osprint("  " .. domain .. "\n")
   1419                                 end
   1420                             end
   1421                         else
   1422                             if osprint then
   1423                                 osprint("Warning: NetworkStack not available - network permission ineffective\n")
   1424                             end
   1425                         end
   1426                     end
   1427                 else
   1428                     if osprint then osprint("Warning: Failed to load SafeHTTP module: " .. tostring(SafeHTTP) .. "\n") end
   1429                 end
   1430             else
   1431                 if osprint then osprint("Warning: Failed to compile SafeHTTP: " .. tostring(err) .. "\n") end
   1432             end
   1433         else
   1434             if osprint then osprint("Warning: SafeHTTP module not found at " .. safehttp_path .. "\n") end
   1435         end
   1436     end
   1437 
   1438     -- Check for import permission and setup apps table
   1439     local has_import = false
   1440     for _, perm in ipairs(permissions) do
   1441         if perm == "import" then
   1442             has_import = true
   1443             break
   1444         end
   1445     end
   1446 
   1447     if has_import then
   1448         -- Create apps table that provides access to Application instances
   1449         local apps_table = {}
   1450 
   1451         setmetatable(apps_table, {
   1452             __index = function(t, app_name)
   1453                 -- app_name is like "com.devname.appname"
   1454                 -- Find the Application instance in sys.applications
   1455                 local registry = get_app_registry()
   1456                 for pid, app_instance in pairs(registry) do
   1457                     if app_instance.appPath and app_instance.appPath:find(app_name, 1, true) then
   1458                         -- Return the Application instance (which has :call(), :export(), etc.)
   1459                         rawset(t, app_name, app_instance)
   1460                         return app_instance
   1461                     end
   1462                 end
   1463 
   1464                 -- App not found
   1465                 return nil
   1466             end,
   1467             __metatable = false  -- Prevent metatable access/modification
   1468         })
   1469 
   1470         sandbox_env.apps = apps_table
   1471 
   1472         if osprint then
   1473             osprint("Import permission granted - apps table available\n")
   1474         end
   1475     end
   1476 
   1477     -- Check for scheduling permission and setup os.schedule API
   1478     local has_schedule = false
   1479     for _, perm in ipairs(permissions) do
   1480         if perm == "scheduling" then
   1481             has_schedule = true
   1482             break
   1483         end
   1484     end
   1485 
   1486     if has_schedule then
   1487         -- Load scheduler module if not already loaded
   1488         if not scheduler and fsRoot then
   1489             local scheduler_path = "/os/libs/Scheduler.lua"
   1490             local scheduler_code = read_from_ramdisk(fsRoot, scheduler_path)
   1491 
   1492             if scheduler_code then
   1493                 -- Load scheduler in a temporary environment
   1494                 local scheduler_env = {}
   1495                 setmetatable(scheduler_env, {__index = _G, __metatable = false})
   1496                 local scheduler_func, err = load(scheduler_code, scheduler_path, "t", scheduler_env)
   1497 
   1498                 if scheduler_func then
   1499                     local success, Scheduler = pcall(scheduler_func)
   1500 
   1501                     if success and Scheduler then
   1502                         scheduler = Scheduler
   1503                         -- Initialize scheduler
   1504                         scheduler.init()
   1505                         if osprint then
   1506                             osprint("Scheduler module loaded and initialized\n")
   1507                         end
   1508                     else
   1509                         if osprint then
   1510                             osprint("Warning: Failed to load scheduler module: " .. tostring(Scheduler) .. "\n")
   1511                         end
   1512                     end
   1513                 else
   1514                     if osprint then
   1515                         osprint("Warning: Failed to compile scheduler: " .. tostring(err) .. "\n")
   1516                     end
   1517                 end
   1518             else
   1519                 if osprint then
   1520                     osprint("Warning: Scheduler module not found at " .. scheduler_path .. "\n")
   1521                 end
   1522             end
   1523         end
   1524 
   1525         -- Create os.schedule API if scheduler is available
   1526         if scheduler then
   1527             -- Create app context for scheduler API
   1528             local app_context = {
   1529                 _app_path = selected_app  -- Store the app path for onNextStartUp/onEveryStartUp
   1530             }
   1531 
   1532             -- Parse permissions into a table
   1533             local perm_table = {}
   1534             for _, perm in ipairs(permissions) do
   1535                 perm_table[perm] = true
   1536             end
   1537 
   1538             -- Create the os.schedule API
   1539             local schedule_api = scheduler.create_api(app_context, perm_table)
   1540 
   1541             if schedule_api then
   1542                 -- Create os table if it doesn't exist
   1543                 if not sandbox_env.os then
   1544                     sandbox_env.os = {}
   1545                 end
   1546 
   1547                 -- Add schedule API to os table
   1548                 sandbox_env.os.schedule = schedule_api
   1549 
   1550                 if osprint then
   1551                     osprint("Schedule permission granted - os.schedule API available\n")
   1552                 end
   1553             end
   1554         end
   1555     end
   1556 
   1557     -- Check for ramdisk permission and add ramdisk functions
   1558     local has_ramdisk = false
   1559     for _, perm in ipairs(permissions) do
   1560         if perm == "ramdisk" then
   1561             has_ramdisk = true
   1562             break
   1563         end
   1564     end
   1565 
   1566     if has_ramdisk then
   1567         -- Add ramdisk functions
   1568         sandbox_env.CRamdiskOpen = _G.CRamdiskOpen
   1569         sandbox_env.CRamdiskRead = _G.CRamdiskRead
   1570         sandbox_env.CRamdiskWrite = _G.CRamdiskWrite
   1571         sandbox_env.CRamdiskClose = _G.CRamdiskClose
   1572         sandbox_env.CRamdiskList = _G.CRamdiskList
   1573         sandbox_env.CRamdiskExists = _G.CRamdiskExists
   1574         sandbox_env.CRamdiskMkdir = _G.CRamdiskMkdir
   1575 
   1576         -- Add GetManifest helper function (requires ramdisk access)
   1577         sandbox_env.GetManifest = function(appId)
   1578             if type(appId) ~= "string" or appId == "" then
   1579                 return nil, "Invalid app ID"
   1580             end
   1581 
   1582             local manifestPath = "/apps/" .. appId .. "/manifest.lua"
   1583 
   1584             if not _G.CRamdiskExists or not _G.CRamdiskExists(manifestPath) then
   1585                 return nil, "Manifest not found"
   1586             end
   1587 
   1588             local handle = _G.CRamdiskOpen(manifestPath, "r")
   1589             if not handle then
   1590                 return nil, "Failed to open manifest"
   1591             end
   1592 
   1593             local manifestCode = _G.CRamdiskRead(handle)
   1594             _G.CRamdiskClose(handle)
   1595 
   1596             if not manifestCode or manifestCode == "" then
   1597                 return nil, "Failed to read manifest"
   1598             end
   1599 
   1600             -- Load and execute manifest using loadstring (available in Run.lua context)
   1601             local loadFunc = loadstring or load
   1602             if not loadFunc then
   1603                 return nil, "No load function available"
   1604             end
   1605 
   1606             local manifestFunc, loadErr = loadFunc(manifestCode, "manifest_" .. appId)
   1607             if not manifestFunc then
   1608                 return nil, "Failed to load manifest"
   1609             end
   1610 
   1611             local success, manifest = pcall(manifestFunc)
   1612             if not success then
   1613                 return nil, "Failed to execute manifest"
   1614             end
   1615 
   1616             if type(manifest) ~= "table" then
   1617                 return nil, "Manifest did not return a table"
   1618             end
   1619 
   1620             return manifest
   1621         end
   1622 
   1623         if osprint then
   1624             osprint("Ramdisk permission granted - ramdisk functions available\n")
   1625         end
   1626     end
   1627 
   1628     -- Check for draw permission and add graphics/image functions
   1629     local has_draw = false
   1630     for _, perm in ipairs(permissions) do
   1631         if perm == "draw" then
   1632             has_draw = true
   1633             break
   1634         end
   1635     end
   1636 
   1637     if has_draw then
   1638         -- Add VESA graphics functions
   1639         sandbox_env.VESAInit = _G.VESAInit
   1640         sandbox_env.VESASetMode = _G.VESASetMode
   1641         sandbox_env.VESAClearScreen = _G.VESAClearScreen
   1642         sandbox_env.VESADrawPixel = _G.VESADrawPixel
   1643         sandbox_env.VESAFillRect = _G.VESAFillRect
   1644         sandbox_env.VESADrawRect = _G.VESADrawRect
   1645         sandbox_env.VESADrawChar = _G.VESADrawChar
   1646         sandbox_env.VESADrawString = _G.VESADrawString
   1647         sandbox_env.VESAGetInfo = _G.VESAGetInfo
   1648         sandbox_env.VESAInspectBuffer = _G.VESAInspectBuffer
   1649         sandbox_env.VESASetRenderTarget = _G.VESASetRenderTarget
   1650         sandbox_env.VESAProcessBufferedDrawOps = _G.VESAProcessBufferedDrawOps
   1651         sandbox_env.VESABlitWindowBufferRegion = _G.VESABlitWindowBufferRegion
   1652 
   1653         -- Add image loading/drawing functions
   1654         sandbox_env.PNGLoad = _G.PNGLoad
   1655         sandbox_env.BMPLoad = _G.BMPLoad
   1656         sandbox_env.JPEGLoad = _G.JPEGLoad
   1657         sandbox_env.ImageDraw = _G.ImageDraw
   1658         sandbox_env.ImageDrawScaled = _G.ImageDrawScaled
   1659         sandbox_env.ImageGetInfo = _G.ImageGetInfo
   1660         sandbox_env.ImageGetWidth = _G.ImageGetWidth
   1661         sandbox_env.ImageGetHeight = _G.ImageGetHeight
   1662         sandbox_env.ImageGetPixel = _G.ImageGetPixel
   1663         sandbox_env.ImageGetBufferBGRA = _G.ImageGetBufferBGRA
   1664         sandbox_env.ImageDestroy = _G.ImageDestroy
   1665 
   1666         -- Add partial window update function for efficient drawing (e.g., paint apps)
   1667         sandbox_env.updateWindowRegion = _G.updateWindowRegion
   1668 
   1669         if osprint then
   1670             osprint("Draw permission granted - graphics functions available\n")
   1671             osprint("  BMPLoad in _G: " .. tostring(_G.BMPLoad ~= nil) .. "\n")
   1672             osprint("  BMPLoad in sandbox: " .. tostring(sandbox_env.BMPLoad ~= nil) .. "\n")
   1673             osprint("  JPEGLoad in _G: " .. tostring(_G.JPEGLoad ~= nil) .. "\n")
   1674             osprint("  JPEGLoad in sandbox: " .. tostring(sandbox_env.JPEGLoad ~= nil) .. "\n")
   1675             osprint("  ImageGetWidth in _G: " .. tostring(_G.ImageGetWidth ~= nil) .. "\n")
   1676             osprint("  ImageGetHeight in _G: " .. tostring(_G.ImageGetHeight ~= nil) .. "\n")
   1677         end
   1678     end
   1679 
   1680     -- Check for imaging permission and add Image library
   1681     local has_imaging = false
   1682     for _, perm in ipairs(permissions) do
   1683         if perm == "imaging" then
   1684             has_imaging = true
   1685             break
   1686         end
   1687     end
   1688 
   1689     if has_imaging and _G.Image then
   1690         sandbox_env.Image = _G.Image
   1691         if osprint then
   1692             osprint("Imaging permission granted - Image library available\n")
   1693         end
   1694     end
   1695 
   1696     -- Check for run permission and setup run function
   1697     local has_run = false
   1698     for _, perm in ipairs(permissions) do
   1699         if perm == "run" then
   1700             has_run = true
   1701             break
   1702         end
   1703     end
   1704 
   1705     if has_run then
   1706         -- Create run function that accepts app name and arguments
   1707         -- Supports both: run("progname", "arg1 -v") and run("progname", "arg1", "-v")
   1708         sandbox_env.run = function(prog_name, ...)
   1709             local args = {...}
   1710             local argStr = ""
   1711 
   1712             -- Build argument string
   1713             if #args == 0 then
   1714                 argStr = ""
   1715             elseif #args == 1 and type(args[1]) == "string" then
   1716                 -- Single string argument: run("prog", "arg1 -v")
   1717                 argStr = args[1]
   1718             else
   1719                 -- Multiple arguments: run("prog", "arg1", "-v")
   1720                 local parts = {}
   1721                 for i, arg in ipairs(args) do
   1722                     table.insert(parts, tostring(arg))
   1723                 end
   1724                 argStr = table.concat(parts, " ")
   1725             end
   1726 
   1727             -- Set global args for the program being run
   1728             if not _G.args then
   1729                 _G.args = {}
   1730             end
   1731             _G.args.argStr = argStr
   1732 
   1733             -- Pass parent's CLI buffer to child process
   1734             _G.parentCli = sandbox_env.cli
   1735 
   1736             -- Run the program - use _G.fsRoot to ensure we have the correct filesystem root
   1737             local success, app = run.execute(prog_name, _G.fsRoot or fsRoot)
   1738 
   1739             -- Clear args and parentCli after running
   1740             _G.args.argStr = nil
   1741             _G.parentCli = nil
   1742 
   1743             return success, app
   1744         end
   1745 
   1746         if osprint then
   1747             osprint("Run permission granted - run() function available\n")
   1748         end
   1749     end
   1750 
   1751     -- Check for load permission and add load function
   1752     local has_load = false
   1753     for _, perm in ipairs(permissions) do
   1754         if perm == "load" then
   1755             has_load = true
   1756             break
   1757         end
   1758     end
   1759 
   1760     if has_load then
   1761         -- Grant access to load() function for Lua interpreter
   1762         -- Create a safe loadstring wrapper that sets the environment
   1763         -- Optionally accepts a custom environment table as third parameter
   1764         sandbox_env.loadstring = function(code, chunkname, env)
   1765             local func, err = _G.loadstring(code, chunkname)
   1766             if func then
   1767                 -- Use provided environment or default to sandbox_env
   1768                 local ok, setenv_err = pcall(_G.setfenv, func, env or sandbox_env)
   1769                 if not ok then
   1770                     return nil, "setfenv failed: " .. tostring(setenv_err)
   1771                 end
   1772             end
   1773             return func, err
   1774         end
   1775 
   1776         if osprint then
   1777             osprint("Load permission granted - loadstring() function available\n")
   1778         end
   1779     end
   1780 
   1781     -- Check for setfenv permission
   1782     local has_setfenv = false
   1783     for _, perm in ipairs(permissions) do
   1784         if perm == "setfenv" then
   1785             has_setfenv = true
   1786             break
   1787         end
   1788     end
   1789 
   1790     if has_setfenv then
   1791         -- Grant access to setfenv for environment manipulation
   1792         -- Use direct reference (available in Run.lua context) rather than _G
   1793         sandbox_env.setfenv = setfenv
   1794         sandbox_env.getfenv = getfenv
   1795 
   1796         if osprint then
   1797             osprint("Setfenv permission granted - setfenv/getfenv functions available\n")
   1798         end
   1799     end
   1800 
   1801     -- Check for system permission and setup system information API
   1802     local has_system = false
   1803     for _, perm in ipairs(permissions) do
   1804         if perm == "system" then
   1805             has_system = true
   1806             break
   1807         end
   1808     end
   1809 
   1810     if has_system then
   1811         -- Create system API with read-only access to system information
   1812         sandbox_env.system = {
   1813             -- Get list of running applications
   1814             getApplications = function()
   1815                 if _G.sys and _G.sys.getAllApplications then
   1816                     return _G.sys.getAllApplications()
   1817                 end
   1818                 return {}
   1819             end
   1820         }
   1821 
   1822         if osprint then
   1823             osprint("System permission granted - system information API available\n")
   1824         end
   1825     end
   1826 
   1827     -- Check for global-environment permission (gives full _G access)
   1828     local has_global_env = false
   1829     for _, perm in ipairs(permissions) do
   1830         if perm == "global-environment" then
   1831             has_global_env = true
   1832             break
   1833         end
   1834     end
   1835 
   1836     -- SECURITY: Scan and remove dangerous globals that should never be in sandbox
   1837     local dangerous_globals = {
   1838         "_G",           -- Global environment access
   1839         "fsRoot",       -- Direct filesystem access
   1840         "gfx",          -- Raw graphics access
   1841         "gfx_api",      -- Raw graphics API
   1842         "run",          -- Run module internals (except run() function if granted)
   1843         "sys",          -- System internals (except system permission API)
   1844         "ADMIN_AppAddPermission",  -- Admin-only functions
   1845         "ADMIN_AppAddPath",
   1846         "ADMIN_StartPrompt",
   1847         "ADMIN_FinishPrompt",
   1848         "VESAInit",     -- Raw VESA (only if draw permission not granted)
   1849         "VESASetMode",
   1850         "VESAClearScreen",
   1851         "VESADrawPixel",
   1852         "VESADrawRect",
   1853         "VESADrawText",
   1854         "VESASetPixel",
   1855         "VESAGetInfo",
   1856         "CRamdiskOpen", -- Raw ramdisk (only if ramdisk permission not granted)
   1857         "CRamdiskRead",
   1858         "CRamdiskWrite",
   1859         "CRamdiskClose",
   1860         "CRamdiskList",
   1861         "CRamdiskExists",
   1862         "CRamdiskMkdir",
   1863         "getfenv",      -- Environment manipulation
   1864         "setfenv",
   1865         "rawget",       -- Raw access functions (keep rawset for sandbox internals)
   1866         "rawequal",
   1867         "dofile",       -- File execution
   1868         "loadfile",
   1869         "load"          -- Dynamic code loading (unless explicitly added)
   1870     }
   1871 
   1872     -- Remove dangerous globals (except those explicitly granted by permissions)
   1873     -- Skip this for global-environment permission (full _G access)
   1874     if not has_global_env then
   1875         for _, key in ipairs(dangerous_globals) do
   1876             -- Only remove if it wasn't explicitly set by permission system above
   1877             -- Check if it exists and wasn't added intentionally
   1878             if sandbox_env[key] ~= nil then
   1879                 -- Special cases: these are OK if granted by permissions
   1880                 local is_permitted = false
   1881 
   1882                 -- Check if it's a permitted VESA function (draw permission)
   1883                 if key:match("^VESA") and has_draw then
   1884                     is_permitted = true
   1885                 end
   1886 
   1887                 -- Check if it's a permitted ramdisk function (ramdisk permission)
   1888                 if key:match("^CRamdisk") and has_ramdisk then
   1889                     is_permitted = true
   1890                 end
   1891 
   1892                 -- Check if it's the run() function (run permission)
   1893                 if key == "run" and has_run then
   1894                     is_permitted = true
   1895                 end
   1896 
   1897                 -- Check if it's the load() function (load permission)
   1898                 if key == "load" and has_load then
   1899                     is_permitted = true
   1900                 end
   1901 
   1902                 -- Check if it's the sys object (system-all permission)
   1903                 if key == "sys" and has_system_all then
   1904                     is_permitted = true
   1905                 end
   1906 
   1907                 -- Check if it's an admin function (admin permission)
   1908                 if (key == "ADMIN_AppAddPermission" or key == "ADMIN_AppAddPath" or
   1909                     key == "ADMIN_StartPrompt" or key == "ADMIN_FinishPrompt") and has_admin then
   1910                     is_permitted = true
   1911                 end
   1912 
   1913                 -- Check if it's setfenv/getfenv (setfenv permission)
   1914                 if (key == "setfenv" or key == "getfenv") and has_setfenv then
   1915                     is_permitted = true
   1916                 end
   1917 
   1918                 -- Remove if not permitted
   1919                 if not is_permitted then
   1920                     sandbox_env[key] = nil
   1921                     if osprint then
   1922                         osprint("SECURITY: Removed dangerous global from sandbox: " .. key .. "\n")
   1923                     end
   1924                 end
   1925             end
   1926         end
   1927     else
   1928         if osprint then
   1929             osprint("SECURITY: Skipping dangerous globals removal (global-environment permission)\n")
   1930         end
   1931     end
   1932 
   1933     -- Add _G reference to sandbox itself (or real _G for global-environment)
   1934     if has_global_env then
   1935         sandbox_env._G = _G  -- Full global access
   1936     else
   1937         sandbox_env._G = sandbox_env
   1938     end
   1939 
   1940     -- Create require function that searches app's src directory first
   1941     local app_src_dir = app_dir .. "/src"
   1942     local require_cache = {}
   1943 
   1944     -- Track modules currently being loaded to detect circular requires
   1945     local loading_modules = {}
   1946 
   1947     sandbox_env.require = function(module_name, use_bytecode)
   1948         if type(module_name) ~= "string" or module_name == "" then
   1949             error("require: module name must be a non-empty string", 2)
   1950         end
   1951 
   1952         if osprint then
   1953             osprint("[require] Loading: " .. module_name .. (use_bytecode and " (bytecode)" or "") .. "\n")
   1954         end
   1955 
   1956         -- Check cache first
   1957         if require_cache[module_name] then
   1958             if osprint then
   1959                 osprint("[require] Cache hit: " .. module_name .. "\n")
   1960             end
   1961             return require_cache[module_name]
   1962         end
   1963 
   1964         -- Check if module is pre-loaded in sandbox (e.g., Dialog, Application)
   1965         -- Use rawget to avoid metatable strict checking
   1966         local preloaded = rawget(sandbox_env, module_name)
   1967         if preloaded ~= nil then
   1968             if osprint then
   1969                 osprint("[require] Using pre-loaded module: " .. module_name .. "\n")
   1970             end
   1971             require_cache[module_name] = preloaded
   1972             return preloaded
   1973         end
   1974 
   1975         -- Check for circular require
   1976         if loading_modules[module_name] then
   1977             error("require: circular dependency detected for '" .. module_name .. "'", 2)
   1978         end
   1979 
   1980         -- Mark as loading
   1981         loading_modules[module_name] = true
   1982 
   1983         -- Convert module name to path (replace . with /)
   1984         local module_path = module_name:gsub("%.", "/")
   1985 
   1986         -- Search paths in order:
   1987         -- If use_bytecode is true, check .luac first
   1988         -- Otherwise only check .lua files
   1989         local search_paths
   1990         if use_bytecode then
   1991             search_paths = {
   1992                 app_src_dir .. "/" .. module_path .. ".luac",
   1993                 app_src_dir .. "/" .. module_path .. ".lua",
   1994                 "/os/libs/" .. module_path .. ".luac",
   1995                 "/os/libs/" .. module_path .. ".lua",
   1996             }
   1997         else
   1998             search_paths = {
   1999                 app_src_dir .. "/" .. module_path .. ".lua",
   2000                 "/os/libs/" .. module_path .. ".lua",
   2001             }
   2002         end
   2003 
   2004         local module_content = nil
   2005         local found_path = nil
   2006 
   2007         for _, path in ipairs(search_paths) do
   2008             if osprint then
   2009                 osprint("[require] Trying: " .. path .. "\n")
   2010             end
   2011             module_content = read_from_ramdisk(fsRoot, path)
   2012             if module_content then
   2013                 found_path = path
   2014                 if osprint then
   2015                     osprint("[require] Found: " .. path .. " (" .. #module_content .. " bytes)\n")
   2016                 end
   2017                 break
   2018             end
   2019         end
   2020 
   2021         if not module_content then
   2022             loading_modules[module_name] = nil
   2023             error("require: module '" .. module_name .. "' not found\n  searched:\n    " ..
   2024                   table.concat(search_paths, "\n    "), 2)
   2025         end
   2026 
   2027         -- Load the module in sandbox environment
   2028         local loadFunc = loadstring or load
   2029         local module_func, load_err
   2030 
   2031         if osprint then
   2032             osprint("[require] Compiling: " .. module_name .. " (" .. #module_content .. " bytes)\n")
   2033         end
   2034 
   2035         if loadFunc == loadstring then
   2036             if osprint then
   2037                 osprint("[require] Calling loadstring...\n")
   2038             end
   2039             local compile_ok, compile_result, compile_err = pcall(function()
   2040                 return loadFunc(module_content, found_path)
   2041             end)
   2042             if osprint then
   2043                 osprint("[require] loadstring pcall returned: " .. tostring(compile_ok) .. "\n")
   2044             end
   2045             if compile_ok then
   2046                 module_func = compile_result
   2047                 load_err = compile_err
   2048             else
   2049                 module_func = nil
   2050                 load_err = compile_result
   2051             end
   2052             if module_func then
   2053                 if osprint then
   2054                     osprint("[require] Setting environment...\n")
   2055                 end
   2056                 setfenv(module_func, sandbox_env)
   2057                 if osprint then
   2058                     osprint("[require] Environment set\n")
   2059                 end
   2060             end
   2061         else
   2062             module_func, load_err = loadFunc(module_content, found_path, "t", sandbox_env)
   2063         end
   2064 
   2065         if not module_func then
   2066             loading_modules[module_name] = nil
   2067             error("require: failed to load module '" .. module_name .. "': " .. tostring(load_err), 2)
   2068         end
   2069 
   2070         if osprint then
   2071             osprint("[require] Executing: " .. module_name .. "\n")
   2072         end
   2073 
   2074         -- Execute the module
   2075         local success, result = pcall(module_func)
   2076 
   2077         -- Clear loading flag
   2078         loading_modules[module_name] = nil
   2079 
   2080         if not success then
   2081             error("require: failed to execute module '" .. module_name .. "': " .. tostring(result), 2)
   2082         end
   2083 
   2084         if osprint then
   2085             osprint("[require] Completed: " .. module_name .. "\n")
   2086         end
   2087 
   2088         -- Cache the result (use true if module returns nil)
   2089         local cached_value = result
   2090         if cached_value == nil then
   2091             cached_value = true
   2092         end
   2093         require_cache[module_name] = cached_value
   2094 
   2095         return result
   2096     end
   2097 
   2098     if osprint then
   2099         osprint("require() function available (searches " .. app_src_dir .. " first)\n")
   2100     end
   2101 
   2102     -- Set up metatable based on permissions
   2103     if has_global_env then
   2104         -- Global environment permission: give direct access to _G
   2105         -- This is for system-level apps like the installer that need full access
   2106         setmetatable(sandbox_env, {
   2107             __index = _G,
   2108             __newindex = function(t, k, v)
   2109                 rawset(t, k, v)  -- Allow setting new globals within sandbox
   2110             end,
   2111             __metatable = _G  -- As requested, metatable = _G
   2112         })
   2113         if osprint then
   2114             osprint("Global-environment permission granted - full _G access available\n")
   2115         end
   2116     else
   2117         -- Standard sandbox: prevent access to undefined globals
   2118         -- Track which keys are allowed (even if nil) vs truly undefined
   2119         local allowed_keys = {}
   2120         for k, _ in pairs(sandbox_env) do
   2121             allowed_keys[k] = true
   2122         end
   2123         -- Also allow keys that were explicitly set to nil or may be nil
   2124         allowed_keys.fs = true
   2125         allowed_keys.crypto = true
   2126         allowed_keys.sys = true
   2127         allowed_keys.window = true
   2128         allowed_keys.JPEGLoad = true
   2129         allowed_keys.PNGLoad = true
   2130         allowed_keys.BMPLoad = true
   2131         allowed_keys.ImageGetWidth = true
   2132         allowed_keys.ImageGetHeight = true
   2133         allowed_keys.ImageGetPixel = true
   2134         allowed_keys.ImageGetBufferBGRA = true
   2135         allowed_keys.ImageDraw = true
   2136         allowed_keys.ImageDrawScaled = true
   2137         allowed_keys.ImageDestroy = true
   2138         allowed_keys.ImageGetInfo = true
   2139         allowed_keys.setfenv = true
   2140         allowed_keys.getfenv = true
   2141         allowed_keys.loadstring = true
   2142 
   2143         setmetatable(sandbox_env, {
   2144             __index = function(t, k)
   2145                 if allowed_keys[k] then
   2146                     return nil  -- Key is allowed but not set, return nil
   2147                 end
   2148                 error("Attempt to access undefined global: " .. tostring(k), 2)
   2149             end,
   2150             __newindex = function(t, k, v)
   2151                 allowed_keys[k] = true  -- Mark as allowed when set
   2152                 rawset(t, k, v)
   2153             end,
   2154             __metatable = false  -- Prevent metatable access/modification
   2155         })
   2156     end
   2157 
   2158     -- Register sandbox environment with sys (after sandbox is fully set up)
   2159     -- Note: app_instance is already defined above, don't redefine it from the proxy
   2160     if app_instance and app_instance.pid and _G.sys and _G.sys.registerEnvironment then
   2161         if osprint then
   2162             osprint("[run] Registering sandbox environment for PID " .. app_instance.pid .. "\n")
   2163         end
   2164         _G.sys.registerEnvironment(app_instance.pid, sandbox_env)
   2165     end
   2166 
   2167     -- Initialize global proc table if needed
   2168     if not _G.proc then
   2169         _G.proc = {}
   2170     end
   2171 
   2172     -- Populate global proc table with sandbox instances
   2173     local app = rawget(sandbox_env, "app")
   2174     if app and app.pid then
   2175         local pid = app.pid
   2176         _G.proc[pid] = {
   2177             id = pid,
   2178             fs = rawget(sandbox_env, "fs"),      -- SafeFS instance (may be nil)
   2179             gfx = rawget(sandbox_env, "gfx"),    -- SafeGfx instance (may be nil)
   2180             http = rawget(sandbox_env, "http"),  -- SafeHTTP instance (may be nil)
   2181             app = app                            -- Application instance
   2182         }
   2183 
   2184         if osprint then
   2185             osprint("Registered process " .. pid .. " in global proc table\n")
   2186             osprint("[DEBUG] About to load app code with load()...\n")
   2187         end
   2188     end
   2189 
   2190     -- Check if this is an HTML type app
   2191     if manifest and manifest.type == "html" then
   2192         if osprint then
   2193             osprint("[DEBUG] HTML type app detected, creating HTML window\n")
   2194         end
   2195 
   2196         -- Extract window dimensions from HTML meta tags
   2197         -- Supports: <meta name="window-width" content="500"> or value="500"
   2198         local html_width = manifest.width or 800
   2199         local html_height = manifest.height or 600
   2200         -- Try content= first, then value=
   2201         local meta_width = string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+content=["\'](%d+)["\']')
   2202             or string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+value=["\'](%d+)["\']')
   2203         local meta_height = string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+content=["\'](%d+)["\']')
   2204             or string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+value=["\'](%d+)["\']')
   2205         if meta_width then html_width = tonumber(meta_width) or html_width end
   2206         if meta_height then html_height = tonumber(meta_height) or html_height end
   2207 
   2208         -- For HTML apps, init_content is HTML, not Lua
   2209         local htmlWindow, htmlErr
   2210 
   2211         if _G.sys and _G.sys.browser then
   2212             htmlWindow, htmlErr = _G.sys.browser:newHTMLWindow({
   2213                 app = sandbox_env.app,
   2214                 html = init_content,
   2215                 file = init_file,
   2216                 fs = rawget(sandbox_env, "fs"),  -- Use rawget to avoid metatable errors
   2217                 width = html_width,
   2218                 height = html_height,
   2219                 title = manifest.name or "HTML App",
   2220                 Dialog = rawget(sandbox_env, "Dialog"),
   2221                 loadstring = rawget(sandbox_env, "loadstring"),
   2222                 showStatusBar = manifest.showStatusBar or false,
   2223                 onSubmit = function(formData)
   2224                     if osprint then
   2225                         osprint("Form submitted: " .. tostring(formData) .. "\n")
   2226                     end
   2227                 end,
   2228             })
   2229         else
   2230             htmlErr = "sys.browser not available"
   2231         end
   2232 
   2233         if not htmlWindow then
   2234             if osprint then
   2235                 osprint("ERROR: Failed to create HTML window: " .. tostring(htmlErr) .. "\n")
   2236             end
   2237             local err_msg = "Failed to create HTML window: " .. tostring(htmlErr)
   2238             print("Error: " .. err_msg)
   2239             return false, err_msg
   2240         end
   2241 
   2242         if osprint then
   2243             osprint("HTML window created successfully\n")
   2244         end
   2245 
   2246         -- Store HTML window reference
   2247         sandbox_env._htmlWindow = htmlWindow
   2248 
   2249         -- Trigger ApplicationStart hook
   2250         if _G.sys and _G.sys.hook and sandbox_env.app then
   2251             _G.sys.hook:run("ApplicationStart", sandbox_env.app)
   2252         end
   2253 
   2254         -- Attach CLI buffer to app instance for proper output handling
   2255         if app_instance and app_instance.attachCLI then
   2256             app_instance:attachCLI(cli)
   2257             if osprint then
   2258                 osprint("[run] Attached CLI to HTML app instance\n")
   2259             end
   2260         end
   2261 
   2262         return true, app_instance
   2263     end
   2264 
   2265     -- Load and execute the app in the sandbox (Lua apps)
   2266     if osprint then
   2267         osprint("[DEBUG] Calling load() on init file: " .. init_file .. "\n")
   2268         osprint("[DEBUG] init_content length: " .. #init_content .. " bytes\n")
   2269         osprint("[DEBUG] sandbox_env type: " .. type(sandbox_env) .. "\n")
   2270         osprint("[DEBUG] Testing with small chunk first...\n")
   2271     end
   2272 
   2273     -- Test: Try loading a simple string first to verify loadstring works
   2274     local testFunc = loadstring("return 1")
   2275     if osprint then
   2276         if testFunc then
   2277             osprint("[DEBUG] Test loadstring succeeded\n")
   2278         else
   2279             osprint("[DEBUG] Test loadstring FAILED\n")
   2280         end
   2281     end
   2282 
   2283     if osprint then
   2284         osprint("[DEBUG] About to call loadstring on actual content...\n")
   2285         osprint("[DEBUG] First 100 chars: " .. init_content:sub(1, 100) .. "\n")
   2286         osprint("[DEBUG] Last 100 chars: " .. init_content:sub(-100) .. "\n")
   2287     end
   2288 
   2289     -- Try loadstring first (LuaJIT), fallback to load
   2290     local loadFunc = loadstring or load
   2291     local app_func, err
   2292 
   2293     if loadFunc == loadstring then
   2294         if osprint then
   2295             osprint("[DEBUG] Using loadstring (LuaJIT)\n")
   2296             osprint("[DEBUG] Calling loadstring on full content...\n")
   2297         end
   2298 
   2299         -- LuaJIT loadstring doesn't take mode parameter
   2300         -- Wrap in pcall to catch any crashes
   2301         local success, result1, result2 = pcall(function()
   2302             return loadFunc(init_content, init_file)
   2303         end)
   2304 
   2305         if osprint then
   2306             osprint("[DEBUG] pcall returned, success=" .. tostring(success) .. "\n")
   2307         end
   2308 
   2309         if success then
   2310             app_func = result1
   2311             err = result2
   2312             if osprint then
   2313                 if app_func then
   2314                     osprint("[DEBUG] loadstring succeeded, app_func=" .. type(app_func) .. "\n")
   2315                 else
   2316                     osprint("[DEBUG] loadstring FAILED: " .. tostring(err) .. "\n")
   2317                 end
   2318             end
   2319         else
   2320             if osprint then
   2321                 osprint("[DEBUG] loadstring CRASHED: " .. tostring(result1) .. "\n")
   2322             end
   2323             err = "loadstring crashed: " .. tostring(result1)
   2324         end
   2325         if app_func then
   2326             -- Set environment after loading
   2327             if osprint then
   2328                 osprint("[DEBUG] Setting environment with setfenv...\n")
   2329             end
   2330             setfenv(app_func, sandbox_env)
   2331             if osprint then
   2332                 osprint("[DEBUG] setfenv completed\n")
   2333             end
   2334         end
   2335     else
   2336         if osprint then
   2337             osprint("[DEBUG] Using load (Lua 5.2+)\n")
   2338         end
   2339         -- Standard Lua 5.2+ load function
   2340         app_func, err = loadFunc(init_content, init_file, "t", sandbox_env)
   2341     end
   2342 
   2343     if osprint then
   2344         osprint("[DEBUG] load/loadstring phase completed\n")
   2345     end
   2346 
   2347     if not app_func then
   2348         if osprint then
   2349             osprint("ERROR: Failed to load app: " .. err .. "\n")
   2350         end
   2351         print("Error: " .. tostring(err))
   2352         return false, tostring(err)
   2353     end
   2354 
   2355     if osprint then
   2356         osprint("[DEBUG] load() succeeded, app_func created\n")
   2357         osprint("[run] Executing app code...\n")
   2358     end
   2359 
   2360     local success, result = pcall(app_func)
   2361 
   2362     if osprint then
   2363         osprint("[DEBUG] pcall completed, success=" .. tostring(success) .. "\n")
   2364     end
   2365 
   2366     -- Trigger ApplicationStart hook (requires admin permission)
   2367     if success and sys and sys.hook and sandbox_env.app then
   2368         sys.hook:run("ApplicationStart", sandbox_env.app)
   2369     end
   2370 
   2371     if not success then
   2372         if osprint then
   2373             osprint("[run] ERROR: App execution failed: " .. tostring(result) .. "\n")
   2374         end
   2375         print("Error: App execution failed: " .. result)
   2376 
   2377         -- Mark app as stopped if it exists
   2378         if Application and app_instance then
   2379             app_instance:setStatus("stopped", "execution_error: " .. tostring(result))
   2380 
   2381             -- Clean up /proc/$PID directory
   2382             local pid = app_instance.pid
   2383             if pid and CRamdiskRemove then
   2384                 local proc_dir = "/proc/" .. pid
   2385                 CRamdiskRemove(proc_dir .. "/process")
   2386                 CRamdiskRemove(proc_dir)
   2387                 if osprint then
   2388                     osprint("Cleaned up " .. proc_dir .. "\n")
   2389                 end
   2390             end
   2391 
   2392             -- Clean up global proc table
   2393             if pid and _G.proc then
   2394                 _G.proc[pid] = nil
   2395                 if osprint then
   2396                     osprint("Removed process " .. pid .. " from proc table\n")
   2397                 end
   2398             end
   2399 
   2400             -- Unregister from sys.applications
   2401             if _G.sys and _G.sys.unregisterApplication then
   2402                 _G.sys.unregisterApplication(pid)
   2403             end
   2404         end
   2405 
   2406         return false, tostring(result)
   2407     end
   2408 
   2409     print("App completed successfully")
   2410 
   2411     -- Check if app should keep running (has windows or callbacks)
   2412     local keepRunning = false
   2413     if Application and app_instance then
   2414         if app_instance.windows and #app_instance.windows > 0 then
   2415             keepRunning = true
   2416             if osprint then
   2417                 osprint("App has " .. #app_instance.windows .. " windows, keeping it running\n")
   2418             end
   2419         end
   2420     end
   2421 
   2422     -- Only mark app as stopped and clean up if it has no windows
   2423     if not keepRunning and Application and app_instance then
   2424         app_instance:setStatus("stopped", "no_windows")
   2425 
   2426         -- Clean up /proc/$PID directory
   2427         local pid = app_instance.pid
   2428         if pid and CRamdiskRemove then
   2429             local proc_dir = "/proc/" .. pid
   2430             CRamdiskRemove(proc_dir .. "/process")
   2431             CRamdiskRemove(proc_dir)
   2432             if osprint then
   2433                 osprint("Cleaned up " .. proc_dir .. "\n")
   2434             end
   2435         end
   2436 
   2437         -- Clean up global proc table
   2438         if pid and _G.proc then
   2439             _G.proc[pid] = nil
   2440             if osprint then
   2441                 osprint("Removed process " .. pid .. " from proc table\n")
   2442             end
   2443         end
   2444 
   2445         -- Unregister from sys.applications
   2446         if _G.sys and _G.sys.unregisterApplication then
   2447             _G.sys.unregisterApplication(pid)
   2448         end
   2449     end
   2450 
   2451     -- Attach CLI buffer to app instance for easy access
   2452     if app_instance and sandbox_env.cli then
   2453         app_instance.cli = sandbox_env.cli
   2454         if osprint then
   2455             osprint("[run] Attached CLI to app instance\n")
   2456             osprint("[run] CLI has getText: " .. tostring(sandbox_env.cli.getText ~= nil) .. "\n")
   2457             osprint("[run] CLI has buffer: " .. tostring(sandbox_env.cli.buffer ~= nil) .. "\n")
   2458             if sandbox_env.cli.getText then
   2459                 local text = sandbox_env.cli.getText()
   2460                 osprint("[run] CLI getText() returns: " .. tostring(text) .. "\n")
   2461             end
   2462         end
   2463     end
   2464 
   2465     -- Return success and the app instance (so caller can get PID, stdout, etc)
   2466     return true, app_instance
   2467 end
   2468 
   2469 -- Export to global namespace for easy access
   2470 _G.run = run
   2471 
   2472 -- Export module functions
   2473 return run