zexus 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -6
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +34 -2
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/blockchain/accelerator.py +27 -0
- package/src/zexus/blockchain/contract_vm.py +409 -3
- package/src/zexus/blockchain/rust_bridge.py +64 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +69 -14
- package/src/zexus/parser/strategy_context.py +228 -5
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +80 -20
- package/src/zexus/vm/fastops.c +1093 -2975
- package/src/zexus/vm/gas_metering.py +2 -2
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +527 -67
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +79 -12
- package/src/zexus.egg-info/SOURCES.txt +23 -1
- package/src/zexus.egg-info/requires.txt +26 -0
- package/src/zexus.egg-info/entry_points.txt +0 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src/zexus/evaluator/expressions.py
|
|
2
2
|
import os
|
|
3
|
+
import traceback as _tb
|
|
3
4
|
|
|
4
5
|
from ..zexus_ast import (
|
|
5
6
|
IntegerLiteral, FloatLiteral, StringLiteral, ListLiteral, MapLiteral,
|
|
@@ -139,7 +140,6 @@ class ExpressionEvaluatorMixin:
|
|
|
139
140
|
if hasattr(env, 'store'):
|
|
140
141
|
env_keys = list(env.store.keys())
|
|
141
142
|
# Use direct print to ensure visibility during debugging
|
|
142
|
-
import traceback as _tb
|
|
143
143
|
stack_snip = ''.join(_tb.format_stack(limit=5)[-3:])
|
|
144
144
|
# print(f"[DEBUG] Identifier not found: {node.value}; env_keys={env_keys}\nStack snippet:\n{stack_snip}")
|
|
145
145
|
except Exception:
|
|
@@ -367,13 +367,27 @@ class ExpressionEvaluatorMixin:
|
|
|
367
367
|
return self.eval_string_infix(operator, left, right)
|
|
368
368
|
|
|
369
369
|
# String repetition: "x" * 100 or 100 * "x"
|
|
370
|
+
# SECURITY (H3): Cap repetition to prevent memory exhaustion
|
|
370
371
|
elif operator == "*":
|
|
372
|
+
_MAX_STRING_REPEAT = 1_000_000 # 1 MB max
|
|
371
373
|
if isinstance(left, String) and isinstance(right, Integer):
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
n = right.value
|
|
375
|
+
if n < 0:
|
|
376
|
+
n = 0
|
|
377
|
+
if n > _MAX_STRING_REPEAT:
|
|
378
|
+
return EvaluationError(
|
|
379
|
+
f"String repetition count {n} exceeds maximum ({_MAX_STRING_REPEAT})"
|
|
380
|
+
)
|
|
381
|
+
return String(left.value * n)
|
|
374
382
|
elif isinstance(left, Integer) and isinstance(right, String):
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
n = left.value
|
|
384
|
+
if n < 0:
|
|
385
|
+
n = 0
|
|
386
|
+
if n > _MAX_STRING_REPEAT:
|
|
387
|
+
return EvaluationError(
|
|
388
|
+
f"String repetition count {n} exceeds maximum ({_MAX_STRING_REPEAT})"
|
|
389
|
+
)
|
|
390
|
+
return String(right.value * n)
|
|
377
391
|
|
|
378
392
|
# Array Concatenation
|
|
379
393
|
elif operator == "+" and isinstance(left, List) and isinstance(right, List):
|
|
@@ -844,23 +858,14 @@ class ExpressionEvaluatorMixin:
|
|
|
844
858
|
error_msg += f"\n Promise created at: {awaitable.stack_trace}"
|
|
845
859
|
return EvaluationError(error_msg)
|
|
846
860
|
else:
|
|
847
|
-
#
|
|
848
|
-
#
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if awaitable.is_resolved():
|
|
857
|
-
try:
|
|
858
|
-
result = awaitable.get_value()
|
|
859
|
-
return result if result is not None else NULL
|
|
860
|
-
except Exception as e:
|
|
861
|
-
return EvaluationError(f"Promise rejected: {e}")
|
|
862
|
-
else:
|
|
863
|
-
return EvaluationError("Await timeout: promise did not resolve")
|
|
861
|
+
# LI6: Avoid busy-waiting (sleep(0.001)) which burns CPU and
|
|
862
|
+
# makes await non-deterministic. In the current interpreter,
|
|
863
|
+
# unresolved promises are not awaited synchronously.
|
|
864
|
+
return EvaluationError(
|
|
865
|
+
"Awaited promise is still pending. "
|
|
866
|
+
"This runtime does not support blocking awaits; "
|
|
867
|
+
"ensure the promise resolves before awaiting it."
|
|
868
|
+
)
|
|
864
869
|
|
|
865
870
|
# Await a Coroutine
|
|
866
871
|
elif obj_type == "COROUTINE":
|
|
@@ -565,6 +565,8 @@ class FunctionEvaluatorMixin:
|
|
|
565
565
|
found = any(elem.value == target.value for elem in obj.elements
|
|
566
566
|
if hasattr(elem, 'value') and hasattr(target, 'value'))
|
|
567
567
|
return TRUE if found else FALSE
|
|
568
|
+
elif method_name == "is_empty":
|
|
569
|
+
return TRUE if len(obj.elements) == 0 else FALSE
|
|
568
570
|
|
|
569
571
|
# === Coroutine Methods ===
|
|
570
572
|
from ..object import Coroutine
|
|
@@ -593,11 +595,23 @@ class FunctionEvaluatorMixin:
|
|
|
593
595
|
|
|
594
596
|
if method_name == "has":
|
|
595
597
|
key = args[0].value if hasattr(args[0], 'value') else str(args[0])
|
|
596
|
-
|
|
598
|
+
# Try plain key first, then try String-wrapped key for normalization
|
|
599
|
+
if key in obj.pairs:
|
|
600
|
+
return TRUE
|
|
601
|
+
str_key = String(key) if isinstance(key, str) else key
|
|
602
|
+
if str_key in obj.pairs:
|
|
603
|
+
return TRUE
|
|
604
|
+
return FALSE
|
|
597
605
|
elif method_name == "get":
|
|
598
606
|
key = args[0].value if hasattr(args[0], 'value') else str(args[0])
|
|
599
607
|
default = args[1] if len(args) > 1 else NULL
|
|
600
|
-
|
|
608
|
+
# Try plain key first, then String-wrapped key for normalization
|
|
609
|
+
if key in obj.pairs:
|
|
610
|
+
return obj.pairs[key]
|
|
611
|
+
str_key = String(key) if isinstance(key, str) else key
|
|
612
|
+
if str_key in obj.pairs:
|
|
613
|
+
return obj.pairs[str_key]
|
|
614
|
+
return default
|
|
601
615
|
elif method_name == "keys":
|
|
602
616
|
# Return array of all keys
|
|
603
617
|
return List([String(k) if isinstance(k, str) else k for k in obj.pairs.keys()])
|
|
@@ -2634,103 +2648,26 @@ class FunctionEvaluatorMixin:
|
|
|
2634
2648
|
return EvaluationError(f"eval_file() zexus execution error: {str(e)}")
|
|
2635
2649
|
|
|
2636
2650
|
elif language == "py" or language == "python":
|
|
2637
|
-
#
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
if 'result' in exec_globals:
|
|
2643
|
-
result_val = exec_globals['result']
|
|
2644
|
-
# Convert Python types to Zexus types
|
|
2645
|
-
if isinstance(result_val, str):
|
|
2646
|
-
return String(result_val)
|
|
2647
|
-
elif isinstance(result_val, int):
|
|
2648
|
-
return Integer(result_val)
|
|
2649
|
-
elif isinstance(result_val, float):
|
|
2650
|
-
return Float(result_val)
|
|
2651
|
-
elif isinstance(result_val, bool):
|
|
2652
|
-
return Boolean(result_val)
|
|
2653
|
-
elif isinstance(result_val, list):
|
|
2654
|
-
return List([Integer(x) if isinstance(x, int) else String(str(x)) for x in result_val])
|
|
2655
|
-
return NULL
|
|
2656
|
-
except Exception as e:
|
|
2657
|
-
return EvaluationError(f"eval_file() python execution error: {str(e)}")
|
|
2651
|
+
# SECURITY (C1): exec() disabled — arbitrary Python execution is unsafe
|
|
2652
|
+
return EvaluationError(
|
|
2653
|
+
"eval_file() for Python is disabled for security reasons. "
|
|
2654
|
+
"Use Zexus native code or the FFI bridge instead."
|
|
2655
|
+
)
|
|
2658
2656
|
|
|
2659
2657
|
elif language in ["cpp", "c++", "c", "rs", "rust", "go"]:
|
|
2660
2658
|
# For compiled languages, try to compile and run
|
|
2661
2659
|
return EvaluationError(f"eval_file() for {language} requires compilation - not yet implemented")
|
|
2662
2660
|
|
|
2663
2661
|
elif language == "js" or language == "javascript":
|
|
2664
|
-
#
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
timeout=5)
|
|
2670
|
-
if result.returncode != 0:
|
|
2671
|
-
return EvaluationError(f"JavaScript error: {result.stderr}")
|
|
2672
|
-
return String(result.stdout.strip())
|
|
2673
|
-
except FileNotFoundError:
|
|
2674
|
-
return EvaluationError("Node.js not found - cannot execute JavaScript")
|
|
2675
|
-
except Exception as e:
|
|
2676
|
-
return EvaluationError(f"eval_file() js execution error: {str(e)}")
|
|
2662
|
+
# SECURITY (C2): subprocess.run(['node',...]) disabled — arbitrary JS execution is unsafe
|
|
2663
|
+
return EvaluationError(
|
|
2664
|
+
"eval_file() for JavaScript is disabled for security reasons. "
|
|
2665
|
+
"Use Zexus native code instead."
|
|
2666
|
+
)
|
|
2677
2667
|
|
|
2678
2668
|
else:
|
|
2679
2669
|
return EvaluationError(f"Unsupported language: {language}")
|
|
2680
2670
|
|
|
2681
|
-
# Contract Assertions
|
|
2682
|
-
def _require(*a):
|
|
2683
|
-
"""Assert a condition in smart contracts: require(condition, message)
|
|
2684
|
-
|
|
2685
|
-
Throws an error if condition is false. Essential for contract validation.
|
|
2686
|
-
|
|
2687
|
-
Example:
|
|
2688
|
-
require(balance >= amount, "Insufficient balance")
|
|
2689
|
-
require(sender == owner, "Not authorized")
|
|
2690
|
-
require(value > 0, "Amount must be positive")
|
|
2691
|
-
"""
|
|
2692
|
-
if len(a) < 1 or len(a) > 2:
|
|
2693
|
-
return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
|
|
2694
|
-
|
|
2695
|
-
condition = a[0]
|
|
2696
|
-
message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
|
|
2697
|
-
|
|
2698
|
-
# Check if condition is truthy
|
|
2699
|
-
from .utils import is_truthy
|
|
2700
|
-
if not is_truthy(condition):
|
|
2701
|
-
# Return error with contract-specific formatting
|
|
2702
|
-
return EvaluationError(f"Contract requirement failed: {message}")
|
|
2703
|
-
|
|
2704
|
-
# Condition passed, return NULL
|
|
2705
|
-
return NULL
|
|
2706
|
-
|
|
2707
|
-
# Contract Assertions
|
|
2708
|
-
def _require(*a):
|
|
2709
|
-
"""Assert a condition in smart contracts: require(condition, message)
|
|
2710
|
-
|
|
2711
|
-
Throws an error if condition is false. Essential for contract validation.
|
|
2712
|
-
Note: This is a fallback for contexts where the require statement isn't available.
|
|
2713
|
-
|
|
2714
|
-
Example:
|
|
2715
|
-
require(balance >= amount, "Insufficient balance")
|
|
2716
|
-
require(sender == owner, "Not authorized")
|
|
2717
|
-
require(value > 0, "Amount must be positive")
|
|
2718
|
-
"""
|
|
2719
|
-
if len(a) < 1 or len(a) > 2:
|
|
2720
|
-
return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
|
|
2721
|
-
|
|
2722
|
-
condition = a[0]
|
|
2723
|
-
message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
|
|
2724
|
-
|
|
2725
|
-
# Check if condition is truthy
|
|
2726
|
-
from .utils import is_truthy
|
|
2727
|
-
if not is_truthy(condition):
|
|
2728
|
-
# Return error with contract-specific formatting
|
|
2729
|
-
return EvaluationError(f"Contract requirement failed: {message}")
|
|
2730
|
-
|
|
2731
|
-
# Condition passed, return NULL
|
|
2732
|
-
return NULL
|
|
2733
|
-
|
|
2734
2671
|
# Map/Object helper functions
|
|
2735
2672
|
def _keys(*a):
|
|
2736
2673
|
"""Get all keys from a map: keys(map) -> [key1, key2, ...]"""
|
|
@@ -2783,8 +2720,6 @@ class FunctionEvaluatorMixin:
|
|
|
2783
2720
|
"to_hex": Builtin(_to_hex, "to_hex"),
|
|
2784
2721
|
"from_hex": Builtin(_from_hex, "from_hex"),
|
|
2785
2722
|
"sqrt": Builtin(_sqrt, "sqrt"),
|
|
2786
|
-
"require": Builtin(_require, "require"),
|
|
2787
|
-
"require": Builtin(_require, "require"),
|
|
2788
2723
|
"input": Builtin(_input, "input"),
|
|
2789
2724
|
"hash_password": Builtin(_hash_password, "hash_password"),
|
|
2790
2725
|
"verify_password": Builtin(_verify_password, "verify_password"),
|
|
@@ -2832,10 +2767,8 @@ class FunctionEvaluatorMixin:
|
|
|
2832
2767
|
"uppercase": Builtin(_uppercase, "uppercase"),
|
|
2833
2768
|
"lowercase": Builtin(_lowercase, "lowercase"),
|
|
2834
2769
|
"split": Builtin(_split, "split"),
|
|
2835
|
-
"random": Builtin(_random, "random"),
|
|
2836
2770
|
"persist_set": Builtin(_persist_set, "persist_set"),
|
|
2837
2771
|
"persist_get": Builtin(_persist_get, "persist_get"),
|
|
2838
|
-
"input": Builtin(_input, "input"),
|
|
2839
2772
|
"len": Builtin(_len, "len"),
|
|
2840
2773
|
"type": Builtin(_type, "type"),
|
|
2841
2774
|
"first": Builtin(_first, "first"),
|
|
@@ -3204,7 +3137,6 @@ class FunctionEvaluatorMixin:
|
|
|
3204
3137
|
"receive": Builtin(_receive, "receive"),
|
|
3205
3138
|
"close_channel": Builtin(_close_channel, "close_channel"),
|
|
3206
3139
|
"async": Builtin(_async, "async"),
|
|
3207
|
-
"sleep": Builtin(_sleep, "sleep"),
|
|
3208
3140
|
"spawn": Builtin(_spawn, "spawn"),
|
|
3209
3141
|
"wait_group": Builtin(_wait_group, "wait_group"),
|
|
3210
3142
|
"wg_add": Builtin(_wg_add, "wg_add"),
|
|
@@ -3622,8 +3554,10 @@ class FunctionEvaluatorMixin:
|
|
|
3622
3554
|
|
|
3623
3555
|
def _memory_stats(*a):
|
|
3624
3556
|
"""Get memory tracking statistics: memory_stats()"""
|
|
3625
|
-
import sys
|
|
3626
3557
|
import gc
|
|
3558
|
+
import os
|
|
3559
|
+
import sys
|
|
3560
|
+
import tracemalloc
|
|
3627
3561
|
|
|
3628
3562
|
# Get process memory usage
|
|
3629
3563
|
try:
|
|
@@ -3633,12 +3567,24 @@ class FunctionEvaluatorMixin:
|
|
|
3633
3567
|
current_bytes = mem_info.rss # Resident Set Size
|
|
3634
3568
|
peak_bytes = getattr(mem_info, 'peak_wset', mem_info.rss) # Windows has peak_wset
|
|
3635
3569
|
except (ImportError, AttributeError):
|
|
3636
|
-
#
|
|
3637
|
-
current_bytes =
|
|
3638
|
-
peak_bytes =
|
|
3570
|
+
# LI8: Avoid gc.get_objects() (very slow). Prefer OS ru_maxrss or tracemalloc.
|
|
3571
|
+
current_bytes = 0
|
|
3572
|
+
peak_bytes = 0
|
|
3573
|
+
try:
|
|
3574
|
+
import resource
|
|
3575
|
+
ru = resource.getrusage(resource.RUSAGE_SELF)
|
|
3576
|
+
# Linux: KB, macOS: bytes
|
|
3577
|
+
ru_maxrss = getattr(ru, 'ru_maxrss', 0) or 0
|
|
3578
|
+
if sys.platform == 'darwin':
|
|
3579
|
+
peak_bytes = int(ru_maxrss)
|
|
3580
|
+
else:
|
|
3581
|
+
peak_bytes = int(ru_maxrss) * 1024
|
|
3582
|
+
current_bytes = peak_bytes
|
|
3583
|
+
except Exception:
|
|
3584
|
+
if tracemalloc.is_tracing():
|
|
3585
|
+
current_bytes, peak_bytes = tracemalloc.get_traced_memory()
|
|
3639
3586
|
|
|
3640
|
-
# Get GC statistics
|
|
3641
|
-
gc_count = len(gc.get_objects())
|
|
3587
|
+
# Get GC statistics (fast)
|
|
3642
3588
|
gc_collections = sum(gc.get_count())
|
|
3643
3589
|
|
|
3644
3590
|
# Get environment-specific tracking if available
|
|
@@ -3652,7 +3598,7 @@ class FunctionEvaluatorMixin:
|
|
|
3652
3598
|
String("current"): Integer(current_bytes),
|
|
3653
3599
|
String("peak"): Integer(peak_bytes),
|
|
3654
3600
|
String("gc_count"): Integer(gc_collections),
|
|
3655
|
-
String("objects"): Integer(
|
|
3601
|
+
String("objects"): Integer(-1),
|
|
3656
3602
|
String("tracked_objects"): Integer(tracked_objects)
|
|
3657
3603
|
})
|
|
3658
3604
|
|
|
@@ -3829,6 +3775,94 @@ class FunctionEvaluatorMixin:
|
|
|
3829
3775
|
"clear_mocks": Builtin(_clear_mocks, "clear_mocks"),
|
|
3830
3776
|
"set_execution_mode": Builtin(_set_execution_mode, "set_execution_mode"),
|
|
3831
3777
|
})
|
|
3778
|
+
|
|
3779
|
+
# ----- INT-005 through INT-009: missing directive builtins -----
|
|
3780
|
+
self._register_missing_directive_builtins()
|
|
3781
|
+
|
|
3782
|
+
def _register_missing_directive_builtins(self):
|
|
3783
|
+
"""Register track_memory, cache, throttle, audit, verify builtins (INT-005..INT-009)."""
|
|
3784
|
+
|
|
3785
|
+
def _track_memory(*a):
|
|
3786
|
+
"""Enable memory tracking: track_memory() or track_memory(options_map)"""
|
|
3787
|
+
env = getattr(self, '_current_env', None)
|
|
3788
|
+
if env and hasattr(env, 'enable_memory_tracking'):
|
|
3789
|
+
env.enable_memory_tracking()
|
|
3790
|
+
return String("Memory tracking enabled")
|
|
3791
|
+
return String("Memory tracking enabled (no-op — persistence module not loaded)")
|
|
3792
|
+
|
|
3793
|
+
def _cache(*a):
|
|
3794
|
+
"""Declare a named cache: cache(name, options_map)
|
|
3795
|
+
Options: {ttl: seconds}
|
|
3796
|
+
Returns the cache handle (currently a lightweight Map stub).
|
|
3797
|
+
"""
|
|
3798
|
+
name = str(a[0].value) if a and hasattr(a[0], 'value') else "default"
|
|
3799
|
+
ttl = 300 # default 5 min
|
|
3800
|
+
if len(a) >= 2 and hasattr(a[1], 'pairs'):
|
|
3801
|
+
for k, v in a[1].pairs.items():
|
|
3802
|
+
key_str = k.value if hasattr(k, 'value') else str(k)
|
|
3803
|
+
if key_str == "ttl" and hasattr(v, 'value'):
|
|
3804
|
+
ttl = int(v.value)
|
|
3805
|
+
# Store cache metadata in environment
|
|
3806
|
+
env = getattr(self, '_current_env', None)
|
|
3807
|
+
if env:
|
|
3808
|
+
env.set(f"__cache_{name}_ttl__", Integer(ttl))
|
|
3809
|
+
return Map({String("name"): String(name), String("ttl"): Integer(ttl)})
|
|
3810
|
+
|
|
3811
|
+
def _throttle(*a):
|
|
3812
|
+
"""Set up rate-limiting: throttle(name, options_map)
|
|
3813
|
+
Options: {requests_per_minute: N}
|
|
3814
|
+
"""
|
|
3815
|
+
name = str(a[0].value) if a and hasattr(a[0], 'value') else "default"
|
|
3816
|
+
rpm = 60
|
|
3817
|
+
if len(a) >= 2 and hasattr(a[1], 'pairs'):
|
|
3818
|
+
for k, v in a[1].pairs.items():
|
|
3819
|
+
key_str = k.value if hasattr(k, 'value') else str(k)
|
|
3820
|
+
if key_str == "requests_per_minute" and hasattr(v, 'value'):
|
|
3821
|
+
rpm = int(v.value)
|
|
3822
|
+
env = getattr(self, '_current_env', None)
|
|
3823
|
+
if env:
|
|
3824
|
+
env.set(f"__throttle_{name}_rpm__", Integer(rpm))
|
|
3825
|
+
return Map({String("name"): String(name), String("requests_per_minute"): Integer(rpm)})
|
|
3826
|
+
|
|
3827
|
+
def _audit(*a):
|
|
3828
|
+
"""Log an audit event: audit(event_name, data_map)"""
|
|
3829
|
+
event_name = str(a[0].value) if a and hasattr(a[0], 'value') else "unknown"
|
|
3830
|
+
data = a[1] if len(a) >= 2 else NULL
|
|
3831
|
+
# Store in environment audit trail
|
|
3832
|
+
env = getattr(self, '_current_env', None)
|
|
3833
|
+
if env:
|
|
3834
|
+
trail = env.get("__audit_trail__")
|
|
3835
|
+
if trail is None or not isinstance(trail, List):
|
|
3836
|
+
trail = List([])
|
|
3837
|
+
env.set("__audit_trail__", trail)
|
|
3838
|
+
trail.elements.append(Map({String("event"): String(event_name), String("data"): data}))
|
|
3839
|
+
return String(f"Audit logged: {event_name}")
|
|
3840
|
+
|
|
3841
|
+
def _verify(*a):
|
|
3842
|
+
"""Alias for require(): verify(condition, message)
|
|
3843
|
+
Throws if condition is falsy.
|
|
3844
|
+
"""
|
|
3845
|
+
if len(a) < 1:
|
|
3846
|
+
return EvaluationError("verify() requires at least 1 argument: condition")
|
|
3847
|
+
condition = a[0]
|
|
3848
|
+
msg = str(a[1].value) if len(a) >= 2 and hasattr(a[1], 'value') else "Verification failed"
|
|
3849
|
+
# Truthy check
|
|
3850
|
+
is_truthy = True
|
|
3851
|
+
if hasattr(condition, 'value'):
|
|
3852
|
+
is_truthy = bool(condition.value)
|
|
3853
|
+
elif condition is NULL or condition is FALSE:
|
|
3854
|
+
is_truthy = False
|
|
3855
|
+
if not is_truthy:
|
|
3856
|
+
return EvaluationError(msg)
|
|
3857
|
+
return TRUE
|
|
3858
|
+
|
|
3859
|
+
self.builtins.update({
|
|
3860
|
+
"track_memory": Builtin(_track_memory, "track_memory"),
|
|
3861
|
+
"cache": Builtin(_cache, "cache"),
|
|
3862
|
+
"throttle": Builtin(_throttle, "throttle"),
|
|
3863
|
+
"audit": Builtin(_audit, "audit"),
|
|
3864
|
+
"verify": Builtin(_verify, "verify"),
|
|
3865
|
+
})
|
|
3832
3866
|
|
|
3833
3867
|
def _register_main_entry_point_builtins(self):
|
|
3834
3868
|
"""Register builtins for main entry point pattern and continuous execution"""
|
|
@@ -4004,7 +4038,9 @@ class FunctionEvaluatorMixin:
|
|
|
4004
4038
|
print(f"⚠️ on_exit hook error: {str(e)}")
|
|
4005
4039
|
|
|
4006
4040
|
print(f"👋 Exiting with code {exit_code}")
|
|
4007
|
-
sys.exit(
|
|
4041
|
+
# SECURITY (H4): Raise SystemExit instead of calling sys.exit()
|
|
4042
|
+
# so it can be caught by the interpreter's top-level handler
|
|
4043
|
+
raise SystemExit(exit_code)
|
|
4008
4044
|
|
|
4009
4045
|
def _on_start(*a):
|
|
4010
4046
|
"""
|
|
@@ -4167,23 +4203,19 @@ class FunctionEvaluatorMixin:
|
|
|
4167
4203
|
"""
|
|
4168
4204
|
Run the current process as a background daemon.
|
|
4169
4205
|
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
Usage:
|
|
4174
|
-
if is_main() {
|
|
4175
|
-
daemonize()
|
|
4176
|
-
# Now running as daemon
|
|
4177
|
-
run(my_server_task)
|
|
4178
|
-
}
|
|
4179
|
-
|
|
4180
|
-
Optional arguments:
|
|
4181
|
-
daemonize() # Use defaults
|
|
4182
|
-
daemonize(working_dir) # Set working directory
|
|
4206
|
+
SECURITY (C8): Requires ZEXUS_ALLOW_DAEMON=1 environment variable.
|
|
4207
|
+
Disabled by default to prevent untrusted scripts from forking.
|
|
4183
4208
|
"""
|
|
4184
4209
|
import os
|
|
4185
4210
|
import sys
|
|
4186
4211
|
|
|
4212
|
+
# Security gate: must be explicitly opted-in
|
|
4213
|
+
if os.environ.get('ZEXUS_ALLOW_DAEMON') != '1':
|
|
4214
|
+
return EvaluationError(
|
|
4215
|
+
"daemonize() is disabled for security. "
|
|
4216
|
+
"Set ZEXUS_ALLOW_DAEMON=1 environment variable to enable."
|
|
4217
|
+
)
|
|
4218
|
+
|
|
4187
4219
|
# Check if we're on a Unix-like system
|
|
4188
4220
|
if not hasattr(os, 'fork'):
|
|
4189
4221
|
return EvaluationError("daemonize() is only supported on Unix-like systems")
|
|
@@ -4826,7 +4858,13 @@ class FunctionEvaluatorMixin:
|
|
|
4826
4858
|
return String(value)
|
|
4827
4859
|
|
|
4828
4860
|
def _env_set(*a):
|
|
4829
|
-
"""Set environment variable: env_set("VAR_NAME", "value")
|
|
4861
|
+
"""Set environment variable: env_set("VAR_NAME", "value")
|
|
4862
|
+
|
|
4863
|
+
SECURITY (C9): Blocks modification of security-sensitive env vars
|
|
4864
|
+
(PATH, LD_PRELOAD, PYTHONPATH, etc.).
|
|
4865
|
+
"""
|
|
4866
|
+
from ..object import BLOCKED_ENV_VARS
|
|
4867
|
+
|
|
4830
4868
|
if len(a) != 2:
|
|
4831
4869
|
return EvaluationError("env_set() takes 2 arguments: var_name, value")
|
|
4832
4870
|
|
|
@@ -4836,6 +4874,12 @@ class FunctionEvaluatorMixin:
|
|
|
4836
4874
|
var_name = var_name_obj.value if isinstance(var_name_obj, String) else str(var_name_obj)
|
|
4837
4875
|
value = value_obj.value if isinstance(value_obj, String) else str(value_obj)
|
|
4838
4876
|
|
|
4877
|
+
# Security: block sensitive environment variables
|
|
4878
|
+
if var_name.upper() in BLOCKED_ENV_VARS:
|
|
4879
|
+
return EvaluationError(
|
|
4880
|
+
f"env_set() denied: '{var_name}' is a protected environment variable"
|
|
4881
|
+
)
|
|
4882
|
+
|
|
4839
4883
|
os.environ[var_name] = value
|
|
4840
4884
|
return TRUE
|
|
4841
4885
|
|
|
@@ -4885,22 +4929,23 @@ class FunctionEvaluatorMixin:
|
|
|
4885
4929
|
return String("strong")
|
|
4886
4930
|
|
|
4887
4931
|
def _sanitize_input(*a):
|
|
4888
|
-
"""Sanitize user input
|
|
4932
|
+
"""Sanitize user input without corrupting data.
|
|
4933
|
+
|
|
4934
|
+
LI9: This is *not* an SQL-injection defense and must not attempt to
|
|
4935
|
+
strip SQL keywords (bypassable + corrupts user data). Prefer
|
|
4936
|
+
parameterized queries for DB APIs.
|
|
4937
|
+
"""
|
|
4889
4938
|
if len(a) != 1:
|
|
4890
4939
|
return EvaluationError("sanitize_input() takes 1 argument")
|
|
4891
4940
|
|
|
4892
4941
|
val = a[0]
|
|
4893
4942
|
input_str = val.value if isinstance(val, String) else str(val)
|
|
4894
4943
|
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
# Remove SQL injection patterns
|
|
4901
|
-
sanitized = re.sub(r'(;|--|\'|\"|\bOR\b|\bAND\b)', '', sanitized, flags=re.IGNORECASE)
|
|
4902
|
-
|
|
4903
|
-
return String(sanitized)
|
|
4944
|
+
sanitized = input_str.replace("\x00", "")
|
|
4945
|
+
sanitized = sanitized.replace("\r\n", "\n").replace("\r", "\n")
|
|
4946
|
+
# Preserve original trust level if we received a String.
|
|
4947
|
+
is_trusted = val.is_trusted if isinstance(val, String) else False
|
|
4948
|
+
return String(sanitized, sanitized_for="generic", is_trusted=is_trusted)
|
|
4904
4949
|
|
|
4905
4950
|
def _validate_length(*a):
|
|
4906
4951
|
"""Validate string length: validate_length(value, min, max)"""
|