zexus 1.6.2 → 1.6.3
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 +165 -5
- package/package.json +1 -1
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/access_control_system/__init__.py +38 -0
- package/src/zexus/access_control_system/access_control.py +237 -0
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/debug_sanitizer.py +250 -0
- package/src/zexus/error_reporter.py +22 -2
- package/src/zexus/evaluator/core.py +7 -2
- package/src/zexus/evaluator/expressions.py +116 -57
- package/src/zexus/evaluator/functions.py +586 -170
- package/src/zexus/evaluator/resource_limiter.py +291 -0
- package/src/zexus/evaluator/statements.py +31 -3
- package/src/zexus/evaluator/utils.py +12 -6
- package/src/zexus/lsp/server.py +1 -1
- package/src/zexus/object.py +21 -2
- package/src/zexus/parser/parser.py +39 -1
- package/src/zexus/parser/strategy_context.py +29 -4
- package/src/zexus/parser/strategy_structural.py +12 -4
- package/src/zexus/persistence.py +105 -6
- package/src/zexus/security_enforcement.py +237 -0
- package/src/zexus/stdlib/fs.py +120 -22
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +499 -13
- package/src/zexus.egg-info/SOURCES.txt +242 -152
|
@@ -196,187 +196,208 @@ class FunctionEvaluatorMixin:
|
|
|
196
196
|
def apply_function(self, fn, args, env=None):
|
|
197
197
|
debug_log("apply_function", f"Calling {fn}")
|
|
198
198
|
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
if isinstance(fn, (Action, LambdaFunction)):
|
|
202
|
-
func_name = fn.name if hasattr(fn, 'name') else str(fn)
|
|
203
|
-
# Trigger before-call hook
|
|
204
|
-
self.integration_context.plugins.before_action_call(func_name, {})
|
|
205
|
-
|
|
206
|
-
# Check required capabilities
|
|
207
|
-
try:
|
|
208
|
-
self.integration_context.capabilities.require_capability("core.language")
|
|
209
|
-
except PermissionError:
|
|
210
|
-
return EvaluationError(f"Permission denied: insufficient capabilities for {func_name}")
|
|
211
|
-
|
|
199
|
+
# Resource limit: Track call depth (Security Fix #7)
|
|
200
|
+
func_name = None
|
|
212
201
|
if isinstance(fn, (Action, LambdaFunction)):
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
202
|
+
func_name = fn.name if hasattr(fn, 'name') else str(fn)
|
|
203
|
+
try:
|
|
204
|
+
self.resource_limiter.enter_call(func_name)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
# Convert ResourceError to EvaluationError
|
|
207
|
+
from .resource_limiter import ResourceError, TimeoutError
|
|
208
|
+
if isinstance(e, (ResourceError, TimeoutError)):
|
|
209
|
+
return EvaluationError(str(e))
|
|
210
|
+
raise # Re-raise if not a resource error
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# Phase 2 & 3: Trigger plugin hooks and check capabilities
|
|
214
|
+
if hasattr(self, 'integration_context'):
|
|
215
|
+
if isinstance(fn, (Action, LambdaFunction)):
|
|
216
|
+
# Trigger before-call hook
|
|
217
|
+
self.integration_context.plugins.before_action_call(func_name, {})
|
|
218
|
+
|
|
219
|
+
# Check required capabilities
|
|
220
|
+
try:
|
|
221
|
+
self.integration_context.capabilities.require_capability("core.language")
|
|
222
|
+
except PermissionError:
|
|
223
|
+
return EvaluationError(f"Permission denied: insufficient capabilities for {func_name}")
|
|
217
224
|
|
|
218
|
-
if
|
|
219
|
-
|
|
220
|
-
from ..object import Coroutine
|
|
221
|
-
import sys
|
|
225
|
+
if isinstance(fn, (Action, LambdaFunction)):
|
|
226
|
+
debug_log(" Calling user-defined function")
|
|
222
227
|
|
|
223
|
-
#
|
|
228
|
+
# Check if this is an async action
|
|
229
|
+
is_async = getattr(fn, 'is_async', False)
|
|
224
230
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# Bind parameters
|
|
230
|
-
for i, param in enumerate(fn.parameters):
|
|
231
|
-
if i < len(args):
|
|
232
|
-
param_name = param.value if hasattr(param, 'value') else str(param)
|
|
233
|
-
new_env.set(param_name, args[i])
|
|
231
|
+
if is_async:
|
|
232
|
+
# Create a coroutine that lazily executes the async action
|
|
233
|
+
from ..object import Coroutine
|
|
234
|
+
import sys
|
|
234
235
|
|
|
235
|
-
#
|
|
236
|
-
yield None
|
|
236
|
+
# print(f"[ASYNC CREATE] Creating coroutine for async action, fn.env has keys: {list(fn.env.store.keys()) if hasattr(fn.env, 'store') else 'N/A'}", file=sys.stderr)
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
def async_generator():
|
|
239
|
+
"""Generator that executes the async action body"""
|
|
240
|
+
new_env = Environment(outer=fn.env)
|
|
241
241
|
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
# Bind parameters
|
|
243
|
+
for i, param in enumerate(fn.parameters):
|
|
244
|
+
if i < len(args):
|
|
245
|
+
param_name = param.value if hasattr(param, 'value') else str(param)
|
|
246
|
+
new_env.set(param_name, args[i])
|
|
247
247
|
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
self._execute_deferred_cleanup(new_env, [])
|
|
248
|
+
# Yield control first (makes it a true generator)
|
|
249
|
+
yield None
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
try:
|
|
280
|
-
if param_names:
|
|
281
|
-
debug_log(" Function parameters bound", f"{param_names}")
|
|
282
|
-
except Exception:
|
|
283
|
-
pass
|
|
284
|
-
|
|
285
|
-
try:
|
|
286
|
-
res = self.eval_node(fn.body, new_env)
|
|
287
|
-
res = _resolve_awaitable(res)
|
|
251
|
+
try:
|
|
252
|
+
# Evaluate the function body
|
|
253
|
+
res = self.eval_node(fn.body, new_env)
|
|
254
|
+
|
|
255
|
+
# Unwrap ReturnValue if needed
|
|
256
|
+
if isinstance(res, ReturnValue):
|
|
257
|
+
result = res.value
|
|
258
|
+
else:
|
|
259
|
+
result = res
|
|
260
|
+
|
|
261
|
+
# Execute deferred cleanup
|
|
262
|
+
if hasattr(self, '_execute_deferred_cleanup'):
|
|
263
|
+
self._execute_deferred_cleanup(new_env, [])
|
|
264
|
+
|
|
265
|
+
# Return the result (will be caught by StopIteration)
|
|
266
|
+
return result
|
|
267
|
+
except Exception as e:
|
|
268
|
+
# Re-raise exception to be caught by coroutine
|
|
269
|
+
raise e
|
|
270
|
+
|
|
271
|
+
# Create and return coroutine
|
|
272
|
+
gen = async_generator()
|
|
273
|
+
coroutine = Coroutine(gen, fn)
|
|
274
|
+
return coroutine
|
|
288
275
|
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
result = res.value
|
|
292
|
-
else:
|
|
293
|
-
result = res
|
|
276
|
+
# Synchronous function execution
|
|
277
|
+
new_env = Environment(outer=fn.env)
|
|
294
278
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
elif isinstance(fn, Builtin):
|
|
308
|
-
debug_log(" Calling builtin function", f"{fn.name}")
|
|
309
|
-
# Sandbox enforcement: if current env is sandboxed, consult policy
|
|
310
|
-
try:
|
|
311
|
-
in_sandbox = False
|
|
312
|
-
policy_name = None
|
|
313
|
-
if env is not None:
|
|
314
|
-
try:
|
|
315
|
-
in_sandbox = bool(env.get('__in_sandbox__'))
|
|
316
|
-
policy_name = env.get('__sandbox_policy__')
|
|
317
|
-
except Exception:
|
|
318
|
-
in_sandbox = False
|
|
279
|
+
param_names = []
|
|
280
|
+
for i, param in enumerate(fn.parameters):
|
|
281
|
+
if i < len(args):
|
|
282
|
+
# Handle both Identifier objects and strings
|
|
283
|
+
param_name = param.value if hasattr(param, 'value') else str(param)
|
|
284
|
+
param_names.append(param_name)
|
|
285
|
+
new_env.set(param_name, args[i])
|
|
286
|
+
# Lightweight debug: show what is being bound
|
|
287
|
+
try:
|
|
288
|
+
debug_log(" Set parameter", f"{param_name} = {type(args[i]).__name__}")
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
319
291
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
292
|
+
try:
|
|
293
|
+
if param_names:
|
|
294
|
+
debug_log(" Function parameters bound", f"{param_names}")
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
res = self.eval_node(fn.body, new_env)
|
|
300
|
+
res = _resolve_awaitable(res)
|
|
301
|
+
|
|
302
|
+
# Unwrap ReturnValue if needed
|
|
303
|
+
if isinstance(res, ReturnValue):
|
|
304
|
+
result = res.value
|
|
305
|
+
else:
|
|
306
|
+
result = res
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
except Exception as e:
|
|
310
|
+
# Store result for after-call hook
|
|
311
|
+
result = EvaluationError(str(e))
|
|
312
|
+
raise
|
|
313
|
+
finally:
|
|
314
|
+
# CRITICAL: Execute deferred cleanup when function exits
|
|
315
|
+
# This happens in finally block to ensure cleanup runs even on errors
|
|
316
|
+
if hasattr(self, '_execute_deferred_cleanup'):
|
|
317
|
+
self._execute_deferred_cleanup(new_env, [])
|
|
318
|
+
|
|
319
|
+
# Phase 2: Trigger after-call hook
|
|
320
|
+
if hasattr(self, 'integration_context'):
|
|
321
|
+
func_name = fn.name if hasattr(fn, 'name') else str(fn)
|
|
322
|
+
self.integration_context.plugins.after_action_call(func_name, result)
|
|
323
|
+
|
|
324
|
+
elif isinstance(fn, Builtin):
|
|
325
|
+
debug_log(" Calling builtin function", f"{fn.name}")
|
|
326
|
+
# Sandbox enforcement: if current env is sandboxed, consult policy
|
|
327
|
+
try:
|
|
328
|
+
in_sandbox = False
|
|
329
|
+
policy_name = None
|
|
330
|
+
if env is not None:
|
|
331
|
+
try:
|
|
332
|
+
in_sandbox = bool(env.get('__in_sandbox__'))
|
|
333
|
+
policy_name = env.get('__sandbox_policy__')
|
|
334
|
+
except Exception:
|
|
335
|
+
in_sandbox = False
|
|
331
336
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
prop_names = [prop['name'] for prop in fn.properties]
|
|
350
|
-
|
|
351
|
-
for i, arg in enumerate(args):
|
|
352
|
-
if i < len(prop_names):
|
|
353
|
-
values[prop_names[i]] = arg
|
|
354
|
-
|
|
355
|
-
return EntityInstance(fn, values)
|
|
356
|
-
|
|
357
|
-
# Handle SecurityEntityDefinition (from security.py with methods support)
|
|
358
|
-
from ..security import EntityDefinition as SecurityEntityDef, EntityInstance as SecurityEntityInstance
|
|
359
|
-
if isinstance(fn, SecurityEntityDef):
|
|
360
|
-
debug_log(" Creating entity instance (with methods)")
|
|
361
|
-
|
|
362
|
-
values = {}
|
|
363
|
-
# Map positional arguments to property names, INCLUDING INHERITED PROPERTIES
|
|
364
|
-
# Use get_all_properties() to get the full property list in correct order
|
|
365
|
-
if hasattr(fn, 'get_all_properties'):
|
|
366
|
-
# Get all properties (parent + child) in correct order
|
|
367
|
-
all_props = fn.get_all_properties()
|
|
368
|
-
prop_names = list(all_props.keys())
|
|
369
|
-
else:
|
|
370
|
-
# Fallback for old-style properties
|
|
371
|
-
prop_names = list(fn.properties.keys()) if isinstance(fn.properties, dict) else [prop['name'] for prop in fn.properties]
|
|
337
|
+
if in_sandbox:
|
|
338
|
+
from ..security import get_security_context
|
|
339
|
+
ctx = get_security_context()
|
|
340
|
+
policy = ctx.get_sandbox_policy(policy_name or 'default')
|
|
341
|
+
allowed = None if policy is None else policy.get('allowed_builtins')
|
|
342
|
+
# If allowed set exists and builtin not in it -> block
|
|
343
|
+
if allowed is not None and fn.name not in allowed:
|
|
344
|
+
return EvaluationError(f"Builtin '{fn.name}' not allowed inside sandbox policy '{policy_name or 'default'}'")
|
|
345
|
+
except Exception:
|
|
346
|
+
# If enforcement fails unexpectedly, proceed to call but log nothing
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
res = fn.fn(*args)
|
|
351
|
+
return _resolve_awaitable(res)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
return EvaluationError(f"Builtin error: {str(e)}")
|
|
372
354
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
355
|
+
elif isinstance(fn, EntityDefinition):
|
|
356
|
+
debug_log(" Creating entity instance (old format)")
|
|
357
|
+
# Entity constructor: Person("Alice", 30)
|
|
358
|
+
# Create instance with positional arguments mapped to properties
|
|
359
|
+
from ..object import EntityInstance, String, Integer
|
|
360
|
+
|
|
361
|
+
values = {}
|
|
362
|
+
# Map positional arguments to property names
|
|
363
|
+
if isinstance(fn.properties, dict):
|
|
364
|
+
prop_names = list(fn.properties.keys())
|
|
365
|
+
else:
|
|
366
|
+
prop_names = [prop['name'] for prop in fn.properties]
|
|
367
|
+
|
|
368
|
+
for i, arg in enumerate(args):
|
|
369
|
+
if i < len(prop_names):
|
|
370
|
+
values[prop_names[i]] = arg
|
|
371
|
+
|
|
372
|
+
return EntityInstance(fn, values)
|
|
376
373
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
374
|
+
# Handle SecurityEntityDefinition (from security.py with methods support)
|
|
375
|
+
from ..security import EntityDefinition as SecurityEntityDef, EntityInstance as SecurityEntityInstance
|
|
376
|
+
if isinstance(fn, SecurityEntityDef):
|
|
377
|
+
debug_log(" Creating entity instance (with methods)")
|
|
378
|
+
|
|
379
|
+
values = {}
|
|
380
|
+
# Map positional arguments to property names, INCLUDING INHERITED PROPERTIES
|
|
381
|
+
# Use get_all_properties() to get the full property list in correct order
|
|
382
|
+
if hasattr(fn, 'get_all_properties'):
|
|
383
|
+
# Get all properties (parent + child) in correct order
|
|
384
|
+
all_props = fn.get_all_properties()
|
|
385
|
+
prop_names = list(all_props.keys())
|
|
386
|
+
else:
|
|
387
|
+
# Fallback for old-style properties
|
|
388
|
+
prop_names = list(fn.properties.keys()) if isinstance(fn.properties, dict) else [prop['name'] for prop in fn.properties]
|
|
389
|
+
|
|
390
|
+
for i, arg in enumerate(args):
|
|
391
|
+
if i < len(prop_names):
|
|
392
|
+
values[prop_names[i]] = arg
|
|
393
|
+
|
|
394
|
+
debug_log(f" Entity instance created with {len(values)} properties: {list(values.keys())}")
|
|
395
|
+
# Use create_instance to handle dependency injection
|
|
396
|
+
return fn.create_instance(values)
|
|
397
|
+
finally:
|
|
398
|
+
# Resource limit: Exit call depth tracking (Security Fix #7)
|
|
399
|
+
if isinstance(fn, (Action, LambdaFunction)):
|
|
400
|
+
self.resource_limiter.exit_call()
|
|
380
401
|
|
|
381
402
|
return EvaluationError(f"Not a function: {fn}")
|
|
382
403
|
|
|
@@ -651,6 +672,112 @@ class FunctionEvaluatorMixin:
|
|
|
651
672
|
return EvaluationError("sqrt() takes exactly 1 argument")
|
|
652
673
|
return Math.sqrt(a[0])
|
|
653
674
|
|
|
675
|
+
# User Input (SECURITY: Returns untrusted strings)
|
|
676
|
+
def _input(*a):
|
|
677
|
+
"""Read user input from stdin - automatically marked as untrusted"""
|
|
678
|
+
prompt = ""
|
|
679
|
+
if len(a) == 1:
|
|
680
|
+
if isinstance(a[0], String):
|
|
681
|
+
prompt = a[0].value
|
|
682
|
+
else:
|
|
683
|
+
prompt = str(a[0].inspect() if hasattr(a[0], 'inspect') else a[0])
|
|
684
|
+
elif len(a) > 1:
|
|
685
|
+
return EvaluationError("input() takes 0 or 1 argument (optional prompt)")
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
user_input = input(prompt)
|
|
689
|
+
# SECURITY: User input is ALWAYS untrusted - external data source
|
|
690
|
+
return String(user_input, is_trusted=False)
|
|
691
|
+
except Exception as e:
|
|
692
|
+
return EvaluationError(f"input() error: {str(e)}")
|
|
693
|
+
|
|
694
|
+
# Cryptographic Functions (SECURITY FIX #5)
|
|
695
|
+
def _hash_password(*a):
|
|
696
|
+
"""
|
|
697
|
+
Hash password using bcrypt (secure, industry-standard)
|
|
698
|
+
Usage: hash_password(password) -> hashed_string
|
|
699
|
+
"""
|
|
700
|
+
if len(a) != 1:
|
|
701
|
+
return EvaluationError("hash_password() takes exactly 1 argument")
|
|
702
|
+
|
|
703
|
+
password = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
704
|
+
|
|
705
|
+
try:
|
|
706
|
+
import bcrypt
|
|
707
|
+
# Generate salt and hash password
|
|
708
|
+
salt = bcrypt.gensalt()
|
|
709
|
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
|
710
|
+
# Return as trusted string (hash, not user input)
|
|
711
|
+
return String(hashed.decode('utf-8'), is_trusted=True)
|
|
712
|
+
except ImportError:
|
|
713
|
+
return EvaluationError("hash_password() requires bcrypt library. Install: pip install bcrypt")
|
|
714
|
+
except Exception as e:
|
|
715
|
+
return EvaluationError(f"hash_password() error: {str(e)}")
|
|
716
|
+
|
|
717
|
+
def _verify_password(*a):
|
|
718
|
+
"""
|
|
719
|
+
Verify password against bcrypt hash (constant-time comparison)
|
|
720
|
+
Usage: verify_password(password, hash) -> boolean
|
|
721
|
+
"""
|
|
722
|
+
if len(a) != 2:
|
|
723
|
+
return EvaluationError("verify_password() takes exactly 2 arguments: password, hash")
|
|
724
|
+
|
|
725
|
+
password = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
726
|
+
password_hash = a[1].value if isinstance(a[1], String) else str(a[1])
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
import bcrypt
|
|
730
|
+
# Constant-time comparison via bcrypt
|
|
731
|
+
result = bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
|
|
732
|
+
return BooleanObj(result)
|
|
733
|
+
except ImportError:
|
|
734
|
+
return EvaluationError("verify_password() requires bcrypt library. Install: pip install bcrypt")
|
|
735
|
+
except Exception as e:
|
|
736
|
+
return EvaluationError(f"verify_password() error: {str(e)}")
|
|
737
|
+
|
|
738
|
+
def _crypto_random(*a):
|
|
739
|
+
"""
|
|
740
|
+
Generate cryptographically secure random string
|
|
741
|
+
Usage: crypto_random(length?) -> hex_string (default 32 bytes = 64 hex chars)
|
|
742
|
+
"""
|
|
743
|
+
length = 32 # Default: 32 bytes
|
|
744
|
+
if len(a) >= 1:
|
|
745
|
+
if isinstance(a[0], Integer):
|
|
746
|
+
length = a[0].value
|
|
747
|
+
else:
|
|
748
|
+
return EvaluationError("crypto_random() length must be an integer")
|
|
749
|
+
|
|
750
|
+
if len(a) > 1:
|
|
751
|
+
return EvaluationError("crypto_random() takes 0 or 1 argument (optional length)")
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
import secrets
|
|
755
|
+
# Generate cryptographically secure random hex string
|
|
756
|
+
random_hex = secrets.token_hex(length)
|
|
757
|
+
# Return as trusted string (generated, not user input)
|
|
758
|
+
return String(random_hex, is_trusted=True)
|
|
759
|
+
except Exception as e:
|
|
760
|
+
return EvaluationError(f"crypto_random() error: {str(e)}")
|
|
761
|
+
|
|
762
|
+
def _constant_time_compare(*a):
|
|
763
|
+
"""
|
|
764
|
+
Constant-time string comparison (timing-attack resistant)
|
|
765
|
+
Usage: constant_time_compare(a, b) -> boolean
|
|
766
|
+
"""
|
|
767
|
+
if len(a) != 2:
|
|
768
|
+
return EvaluationError("constant_time_compare() takes exactly 2 arguments")
|
|
769
|
+
|
|
770
|
+
str_a = a[0].value if isinstance(a[0], String) else str(a[0])
|
|
771
|
+
str_b = a[1].value if isinstance(a[1], String) else str(a[1])
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
import secrets
|
|
775
|
+
# Use secrets.compare_digest for constant-time comparison
|
|
776
|
+
result = secrets.compare_digest(str_a, str_b)
|
|
777
|
+
return BooleanObj(result)
|
|
778
|
+
except Exception as e:
|
|
779
|
+
return EvaluationError(f"constant_time_compare() error: {str(e)}")
|
|
780
|
+
|
|
654
781
|
# File I/O
|
|
655
782
|
def _read_text(*a):
|
|
656
783
|
if len(a) != 1 or not isinstance(a[0], String):
|
|
@@ -1531,7 +1658,8 @@ class FunctionEvaluatorMixin:
|
|
|
1531
1658
|
try:
|
|
1532
1659
|
from ..stdlib.http import HttpModule
|
|
1533
1660
|
result = HttpModule.get(url, headers, timeout)
|
|
1534
|
-
|
|
1661
|
+
# HTTP responses are external data - mark as untrusted
|
|
1662
|
+
return _python_to_zexus(result, mark_untrusted=True)
|
|
1535
1663
|
except Exception as e:
|
|
1536
1664
|
return EvaluationError(f"HTTP GET error: {str(e)}")
|
|
1537
1665
|
|
|
@@ -1558,7 +1686,8 @@ class FunctionEvaluatorMixin:
|
|
|
1558
1686
|
# Determine if data should be sent as JSON
|
|
1559
1687
|
json_mode = isinstance(a[1], (Map, List))
|
|
1560
1688
|
result = HttpModule.post(url, data, headers, json=json_mode, timeout=timeout)
|
|
1561
|
-
|
|
1689
|
+
# HTTP responses are external data - mark as untrusted
|
|
1690
|
+
return _python_to_zexus(result, mark_untrusted=True)
|
|
1562
1691
|
except Exception as e:
|
|
1563
1692
|
return EvaluationError(f"HTTP POST error: {str(e)}")
|
|
1564
1693
|
|
|
@@ -1582,7 +1711,8 @@ class FunctionEvaluatorMixin:
|
|
|
1582
1711
|
from ..stdlib.http import HttpModule
|
|
1583
1712
|
json_mode = isinstance(a[1], (Map, List))
|
|
1584
1713
|
result = HttpModule.put(url, data, headers, json=json_mode, timeout=timeout)
|
|
1585
|
-
|
|
1714
|
+
# HTTP responses are external data - mark as untrusted
|
|
1715
|
+
return _python_to_zexus(result, mark_untrusted=True)
|
|
1586
1716
|
except Exception as e:
|
|
1587
1717
|
return EvaluationError(f"HTTP PUT error: {str(e)}")
|
|
1588
1718
|
|
|
@@ -1604,7 +1734,8 @@ class FunctionEvaluatorMixin:
|
|
|
1604
1734
|
try:
|
|
1605
1735
|
from ..stdlib.http import HttpModule
|
|
1606
1736
|
result = HttpModule.delete(url, headers, timeout)
|
|
1607
|
-
|
|
1737
|
+
# HTTP responses are external data - mark as untrusted
|
|
1738
|
+
return _python_to_zexus(result, mark_untrusted=True)
|
|
1608
1739
|
except Exception as e:
|
|
1609
1740
|
return EvaluationError(f"HTTP DELETE error: {str(e)}")
|
|
1610
1741
|
|
|
@@ -2095,6 +2226,59 @@ class FunctionEvaluatorMixin:
|
|
|
2095
2226
|
else:
|
|
2096
2227
|
return EvaluationError(f"Unsupported language: {language}")
|
|
2097
2228
|
|
|
2229
|
+
# Contract Assertions
|
|
2230
|
+
def _require(*a):
|
|
2231
|
+
"""Assert a condition in smart contracts: require(condition, message)
|
|
2232
|
+
|
|
2233
|
+
Throws an error if condition is false. Essential for contract validation.
|
|
2234
|
+
|
|
2235
|
+
Example:
|
|
2236
|
+
require(balance >= amount, "Insufficient balance")
|
|
2237
|
+
require(sender == owner, "Not authorized")
|
|
2238
|
+
require(value > 0, "Amount must be positive")
|
|
2239
|
+
"""
|
|
2240
|
+
if len(a) < 1 or len(a) > 2:
|
|
2241
|
+
return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
|
|
2242
|
+
|
|
2243
|
+
condition = a[0]
|
|
2244
|
+
message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
|
|
2245
|
+
|
|
2246
|
+
# Check if condition is truthy
|
|
2247
|
+
from .utils import is_truthy
|
|
2248
|
+
if not is_truthy(condition):
|
|
2249
|
+
# Return error with contract-specific formatting
|
|
2250
|
+
return EvaluationError(f"Contract requirement failed: {message}")
|
|
2251
|
+
|
|
2252
|
+
# Condition passed, return NULL
|
|
2253
|
+
return NULL
|
|
2254
|
+
|
|
2255
|
+
# Contract Assertions
|
|
2256
|
+
def _require(*a):
|
|
2257
|
+
"""Assert a condition in smart contracts: require(condition, message)
|
|
2258
|
+
|
|
2259
|
+
Throws an error if condition is false. Essential for contract validation.
|
|
2260
|
+
Note: This is a fallback for contexts where the require statement isn't available.
|
|
2261
|
+
|
|
2262
|
+
Example:
|
|
2263
|
+
require(balance >= amount, "Insufficient balance")
|
|
2264
|
+
require(sender == owner, "Not authorized")
|
|
2265
|
+
require(value > 0, "Amount must be positive")
|
|
2266
|
+
"""
|
|
2267
|
+
if len(a) < 1 or len(a) > 2:
|
|
2268
|
+
return EvaluationError("require() takes 1-2 arguments: require(condition, [message])")
|
|
2269
|
+
|
|
2270
|
+
condition = a[0]
|
|
2271
|
+
message = a[1].value if len(a) > 1 and isinstance(a[1], String) else "Requirement failed"
|
|
2272
|
+
|
|
2273
|
+
# Check if condition is truthy
|
|
2274
|
+
from .utils import is_truthy
|
|
2275
|
+
if not is_truthy(condition):
|
|
2276
|
+
# Return error with contract-specific formatting
|
|
2277
|
+
return EvaluationError(f"Contract requirement failed: {message}")
|
|
2278
|
+
|
|
2279
|
+
# Condition passed, return NULL
|
|
2280
|
+
return NULL
|
|
2281
|
+
|
|
2098
2282
|
# Register mappings
|
|
2099
2283
|
self.builtins.update({
|
|
2100
2284
|
"now": Builtin(_now, "now"),
|
|
@@ -2103,6 +2287,13 @@ class FunctionEvaluatorMixin:
|
|
|
2103
2287
|
"to_hex": Builtin(_to_hex, "to_hex"),
|
|
2104
2288
|
"from_hex": Builtin(_from_hex, "from_hex"),
|
|
2105
2289
|
"sqrt": Builtin(_sqrt, "sqrt"),
|
|
2290
|
+
"require": Builtin(_require, "require"),
|
|
2291
|
+
"require": Builtin(_require, "require"),
|
|
2292
|
+
"input": Builtin(_input, "input"),
|
|
2293
|
+
"hash_password": Builtin(_hash_password, "hash_password"),
|
|
2294
|
+
"verify_password": Builtin(_verify_password, "verify_password"),
|
|
2295
|
+
"crypto_random": Builtin(_crypto_random, "crypto_random"),
|
|
2296
|
+
"constant_time_compare": Builtin(_constant_time_compare, "constant_time_compare"),
|
|
2106
2297
|
"file": Builtin(_file, "file"),
|
|
2107
2298
|
"file_read_text": Builtin(_read_text, "file_read_text"),
|
|
2108
2299
|
"file_write_text": Builtin(_write_text, "file_write_text"),
|
|
@@ -2142,6 +2333,7 @@ class FunctionEvaluatorMixin:
|
|
|
2142
2333
|
"random": Builtin(_random, "random"),
|
|
2143
2334
|
"persist_set": Builtin(_persist_set, "persist_set"),
|
|
2144
2335
|
"persist_get": Builtin(_persist_get, "persist_get"),
|
|
2336
|
+
"input": Builtin(_input, "input"),
|
|
2145
2337
|
"len": Builtin(_len, "len"),
|
|
2146
2338
|
"type": Builtin(_type, "type"),
|
|
2147
2339
|
"first": Builtin(_first, "first"),
|
|
@@ -2154,6 +2346,9 @@ class FunctionEvaluatorMixin:
|
|
|
2154
2346
|
"filter": Builtin(_filter, "filter"),
|
|
2155
2347
|
})
|
|
2156
2348
|
|
|
2349
|
+
# Register access control builtins
|
|
2350
|
+
self._register_access_control_builtins()
|
|
2351
|
+
|
|
2157
2352
|
# Register concurrency builtins
|
|
2158
2353
|
self._register_concurrency_builtins()
|
|
2159
2354
|
|
|
@@ -2512,6 +2707,227 @@ class FunctionEvaluatorMixin:
|
|
|
2512
2707
|
"barrier_reset": Builtin(_barrier_reset, "barrier_reset"),
|
|
2513
2708
|
})
|
|
2514
2709
|
|
|
2710
|
+
def _register_access_control_builtins(self):
|
|
2711
|
+
"""Register access control functions for contracts"""
|
|
2712
|
+
from ..access_control_system import get_access_control
|
|
2713
|
+
from ..blockchain.transaction import get_current_tx
|
|
2714
|
+
|
|
2715
|
+
def _set_owner(*a):
|
|
2716
|
+
"""Set owner of current contract: set_owner(contract_id, owner_address)"""
|
|
2717
|
+
if len(a) != 2:
|
|
2718
|
+
return EvaluationError("set_owner() requires 2 arguments: contract_id, owner_address")
|
|
2719
|
+
|
|
2720
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2721
|
+
owner = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2722
|
+
|
|
2723
|
+
ac = get_access_control()
|
|
2724
|
+
ac.set_owner(contract_id, owner)
|
|
2725
|
+
return NULL
|
|
2726
|
+
|
|
2727
|
+
def _get_owner(*a):
|
|
2728
|
+
"""Get owner of contract: get_owner(contract_id)"""
|
|
2729
|
+
if len(a) != 1:
|
|
2730
|
+
return EvaluationError("get_owner() requires 1 argument: contract_id")
|
|
2731
|
+
|
|
2732
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2733
|
+
|
|
2734
|
+
ac = get_access_control()
|
|
2735
|
+
owner = ac.get_owner(contract_id)
|
|
2736
|
+
return String(owner) if owner else NULL
|
|
2737
|
+
|
|
2738
|
+
def _is_owner(*a):
|
|
2739
|
+
"""Check if address is owner: is_owner(contract_id, address)"""
|
|
2740
|
+
if len(a) != 2:
|
|
2741
|
+
return EvaluationError("is_owner() requires 2 arguments: contract_id, address")
|
|
2742
|
+
|
|
2743
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2744
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2745
|
+
|
|
2746
|
+
ac = get_access_control()
|
|
2747
|
+
return TRUE if ac.is_owner(contract_id, address) else FALSE
|
|
2748
|
+
|
|
2749
|
+
def _grant_role(*a):
|
|
2750
|
+
"""Grant role to address: grant_role(contract_id, address, role)"""
|
|
2751
|
+
if len(a) != 3:
|
|
2752
|
+
return EvaluationError("grant_role() requires 3 arguments: contract_id, address, role")
|
|
2753
|
+
|
|
2754
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2755
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2756
|
+
role = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2757
|
+
|
|
2758
|
+
ac = get_access_control()
|
|
2759
|
+
ac.grant_role(contract_id, address, role)
|
|
2760
|
+
return NULL
|
|
2761
|
+
|
|
2762
|
+
def _revoke_role(*a):
|
|
2763
|
+
"""Revoke role from address: revoke_role(contract_id, address, role)"""
|
|
2764
|
+
if len(a) != 3:
|
|
2765
|
+
return EvaluationError("revoke_role() requires 3 arguments: contract_id, address, role")
|
|
2766
|
+
|
|
2767
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2768
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2769
|
+
role = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2770
|
+
|
|
2771
|
+
ac = get_access_control()
|
|
2772
|
+
ac.revoke_role(contract_id, address, role)
|
|
2773
|
+
return NULL
|
|
2774
|
+
|
|
2775
|
+
def _has_role(*a):
|
|
2776
|
+
"""Check if address has role: has_role(contract_id, address, role)"""
|
|
2777
|
+
if len(a) != 3:
|
|
2778
|
+
return EvaluationError("has_role() requires 3 arguments: contract_id, address, role")
|
|
2779
|
+
|
|
2780
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2781
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2782
|
+
role = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2783
|
+
|
|
2784
|
+
ac = get_access_control()
|
|
2785
|
+
return TRUE if ac.has_role(contract_id, address, role) else FALSE
|
|
2786
|
+
|
|
2787
|
+
def _get_roles(*a):
|
|
2788
|
+
"""Get all roles for address: get_roles(contract_id, address)"""
|
|
2789
|
+
if len(a) != 2:
|
|
2790
|
+
return EvaluationError("get_roles() requires 2 arguments: contract_id, address")
|
|
2791
|
+
|
|
2792
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2793
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2794
|
+
|
|
2795
|
+
ac = get_access_control()
|
|
2796
|
+
roles = ac.get_roles(contract_id, address)
|
|
2797
|
+
return List([String(role) for role in roles])
|
|
2798
|
+
|
|
2799
|
+
def _grant_permission(*a):
|
|
2800
|
+
"""Grant permission to address: grant_permission(contract_id, address, permission)"""
|
|
2801
|
+
if len(a) != 3:
|
|
2802
|
+
return EvaluationError("grant_permission() requires 3 arguments: contract_id, address, permission")
|
|
2803
|
+
|
|
2804
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2805
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2806
|
+
permission = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2807
|
+
|
|
2808
|
+
ac = get_access_control()
|
|
2809
|
+
ac.grant_permission(contract_id, address, permission)
|
|
2810
|
+
return NULL
|
|
2811
|
+
|
|
2812
|
+
def _revoke_permission(*a):
|
|
2813
|
+
"""Revoke permission from address: revoke_permission(contract_id, address, permission)"""
|
|
2814
|
+
if len(a) != 3:
|
|
2815
|
+
return EvaluationError("revoke_permission() requires 3 arguments: contract_id, address, permission")
|
|
2816
|
+
|
|
2817
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2818
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2819
|
+
permission = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2820
|
+
|
|
2821
|
+
ac = get_access_control()
|
|
2822
|
+
ac.revoke_permission(contract_id, address, permission)
|
|
2823
|
+
return NULL
|
|
2824
|
+
|
|
2825
|
+
def _has_permission(*a):
|
|
2826
|
+
"""Check if address has permission: has_permission(contract_id, address, permission)"""
|
|
2827
|
+
if len(a) != 3:
|
|
2828
|
+
return EvaluationError("has_permission() requires 3 arguments: contract_id, address, permission")
|
|
2829
|
+
|
|
2830
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2831
|
+
address = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2832
|
+
permission = a[2].value if hasattr(a[2], 'value') else str(a[2])
|
|
2833
|
+
|
|
2834
|
+
ac = get_access_control()
|
|
2835
|
+
return TRUE if ac.has_permission(contract_id, address, permission) else FALSE
|
|
2836
|
+
|
|
2837
|
+
def _require_owner(*a):
|
|
2838
|
+
"""Require caller is owner: require_owner(contract_id, message?)"""
|
|
2839
|
+
if len(a) < 1 or len(a) > 2:
|
|
2840
|
+
return EvaluationError("require_owner() requires 1 or 2 arguments: contract_id, [message]")
|
|
2841
|
+
|
|
2842
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2843
|
+
message = a[1].value if len(a) > 1 and hasattr(a[1], 'value') else None
|
|
2844
|
+
|
|
2845
|
+
# Get current transaction caller
|
|
2846
|
+
tx = get_current_tx()
|
|
2847
|
+
if not tx:
|
|
2848
|
+
return EvaluationError("require_owner() requires transaction context (TX.caller)")
|
|
2849
|
+
|
|
2850
|
+
caller = tx.caller
|
|
2851
|
+
|
|
2852
|
+
ac = get_access_control()
|
|
2853
|
+
try:
|
|
2854
|
+
if message:
|
|
2855
|
+
ac.require_owner(contract_id, caller, message)
|
|
2856
|
+
else:
|
|
2857
|
+
ac.require_owner(contract_id, caller)
|
|
2858
|
+
return NULL
|
|
2859
|
+
except Exception as e:
|
|
2860
|
+
return EvaluationError(str(e))
|
|
2861
|
+
|
|
2862
|
+
def _require_role(*a):
|
|
2863
|
+
"""Require caller has role: require_role(contract_id, role, message?)"""
|
|
2864
|
+
if len(a) < 2 or len(a) > 3:
|
|
2865
|
+
return EvaluationError("require_role() requires 2 or 3 arguments: contract_id, role, [message]")
|
|
2866
|
+
|
|
2867
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2868
|
+
role = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2869
|
+
message = a[2].value if len(a) > 2 and hasattr(a[2], 'value') else None
|
|
2870
|
+
|
|
2871
|
+
# Get current transaction caller
|
|
2872
|
+
tx = get_current_tx()
|
|
2873
|
+
if not tx:
|
|
2874
|
+
return EvaluationError("require_role() requires transaction context (TX.caller)")
|
|
2875
|
+
|
|
2876
|
+
caller = tx.caller
|
|
2877
|
+
|
|
2878
|
+
ac = get_access_control()
|
|
2879
|
+
try:
|
|
2880
|
+
if message:
|
|
2881
|
+
ac.require_role(contract_id, caller, role, message)
|
|
2882
|
+
else:
|
|
2883
|
+
ac.require_role(contract_id, caller, role)
|
|
2884
|
+
return NULL
|
|
2885
|
+
except Exception as e:
|
|
2886
|
+
return EvaluationError(str(e))
|
|
2887
|
+
|
|
2888
|
+
def _require_permission(*a):
|
|
2889
|
+
"""Require caller has permission: require_permission(contract_id, permission, message?)"""
|
|
2890
|
+
if len(a) < 2 or len(a) > 3:
|
|
2891
|
+
return EvaluationError("require_permission() requires 2 or 3 arguments: contract_id, permission, [message]")
|
|
2892
|
+
|
|
2893
|
+
contract_id = a[0].value if hasattr(a[0], 'value') else str(a[0])
|
|
2894
|
+
permission = a[1].value if hasattr(a[1], 'value') else str(a[1])
|
|
2895
|
+
message = a[2].value if len(a) > 2 and hasattr(a[2], 'value') else None
|
|
2896
|
+
|
|
2897
|
+
# Get current transaction caller
|
|
2898
|
+
tx = get_current_tx()
|
|
2899
|
+
if not tx:
|
|
2900
|
+
return EvaluationError("require_permission() requires transaction context (TX.caller)")
|
|
2901
|
+
|
|
2902
|
+
caller = tx.caller
|
|
2903
|
+
|
|
2904
|
+
ac = get_access_control()
|
|
2905
|
+
try:
|
|
2906
|
+
if message:
|
|
2907
|
+
ac.require_permission(contract_id, caller, permission, message)
|
|
2908
|
+
else:
|
|
2909
|
+
ac.require_permission(contract_id, caller, permission)
|
|
2910
|
+
return NULL
|
|
2911
|
+
except Exception as e:
|
|
2912
|
+
return EvaluationError(str(e))
|
|
2913
|
+
|
|
2914
|
+
# Register access control builtins
|
|
2915
|
+
self.builtins.update({
|
|
2916
|
+
"set_owner": Builtin(_set_owner, "set_owner"),
|
|
2917
|
+
"get_owner": Builtin(_get_owner, "get_owner"),
|
|
2918
|
+
"is_owner": Builtin(_is_owner, "is_owner"),
|
|
2919
|
+
"grant_role": Builtin(_grant_role, "grant_role"),
|
|
2920
|
+
"revoke_role": Builtin(_revoke_role, "revoke_role"),
|
|
2921
|
+
"has_role": Builtin(_has_role, "has_role"),
|
|
2922
|
+
"get_roles": Builtin(_get_roles, "get_roles"),
|
|
2923
|
+
"grant_permission": Builtin(_grant_permission, "grant_permission"),
|
|
2924
|
+
"revoke_permission": Builtin(_revoke_permission, "revoke_permission"),
|
|
2925
|
+
"has_permission": Builtin(_has_permission, "has_permission"),
|
|
2926
|
+
"require_owner": Builtin(_require_owner, "require_owner"),
|
|
2927
|
+
"require_role": Builtin(_require_role, "require_role"),
|
|
2928
|
+
"require_permission": Builtin(_require_permission, "require_permission"),
|
|
2929
|
+
})
|
|
2930
|
+
|
|
2515
2931
|
def _register_blockchain_builtins(self):
|
|
2516
2932
|
"""Register blockchain cryptographic and utility functions"""
|
|
2517
2933
|
from ..blockchain.crypto import CryptoPlugin
|