@@ -421,6 +421,208 @@ def escalate(img: Any, label: Any) -> str:
421421 returns = "Image" ,
422422 )
423423
424+ # -- Standard library: File I/O (V4-2a) ----------------------------------
425+ import os as _os
426+
427+ def _io_read (path : Any ) -> str :
428+ p = str (_unwrap (path ))
429+ # Path traversal defense: reject any path containing '..'
430+ if ".." in p .split (_os .sep ) or ".." in p .split ("/" ):
431+ raise ValueError (
432+ f"io.read: path traversal rejected — '..' not allowed in path: { p !r} "
433+ )
434+ _MAX_READ = 16 * 1024 * 1024 # 16 MiB
435+ try :
436+ size = _os .path .getsize (p )
437+ except OSError as exc :
438+ raise OSError (f"io.read: cannot stat { p !r} : { exc } " ) from exc
439+ if size > _MAX_READ :
440+ raise ValueError (
441+ f"io.read: file { p !r} is { size } bytes, exceeds 16 MiB limit"
442+ )
443+ try :
444+ with open (p , "r" , encoding = "utf-8" ) as fh :
445+ return fh .read (_MAX_READ + 1 )
446+ except OSError as exc :
447+ raise OSError (f"io.read: cannot read { p !r} : { exc } " ) from exc
448+
449+ def _io_write (path : Any , content : Any ) -> None :
450+ p = str (_unwrap (path ))
451+ if ".." in p .split (_os .sep ) or ".." in p .split ("/" ):
452+ raise ValueError (
453+ f"io.write: path traversal rejected — '..' not allowed in path: { p !r} "
454+ )
455+ c = str (_unwrap (content ))
456+ # AUD-STD-05: size limit consistent with io.read (16 MiB).
457+ _MAX_WRITE = 16 * 1024 * 1024
458+ if len (c .encode ("utf-8" )) > _MAX_WRITE :
459+ raise ValueError (
460+ f"io.write: content exceeds 16 MiB limit"
461+ )
462+ try :
463+ with open (p , "w" , encoding = "utf-8" ) as fh :
464+ fh .write (c )
465+ except OSError as exc :
466+ raise OSError (f"io.write: cannot write { p !r} : { exc } " ) from exc
467+
468+ def _io_exists (path : Any ) -> bool :
469+ p = str (_unwrap (path ))
470+ if ".." in p .split (_os .sep ) or ".." in p .split ("/" ):
471+ raise ValueError (
472+ f"io.exists: path traversal rejected — '..' not allowed in path: { p !r} "
473+ )
474+ return _os .path .exists (p )
475+
476+ reg .register ("io.read" , _io_read , effect = "io.read" , returns = "String" )
477+ reg .register ("io.write" , _io_write , effect = "io.write" , returns = "Unit" )
478+ reg .register ("io.exists" , _io_exists , effect = "io.read" , returns = "Bool" )
479+
480+ # -- Standard library: HTTP client (V4-2b) --------------------------------
481+ def _https_only_opener ():
482+ """Build a urllib opener that rejects HTTP redirects (AUD-STD-02).
483+
484+ urllib follows redirects by default. A server at https://example.com
485+ could redirect to http://evil.com, bypassing the HTTPS-only check.
486+ This opener intercepts redirects and raises if the destination is
487+ not HTTPS.
488+ """
489+ import urllib .request as _req
490+
491+ class _HTTPSOnlyRedirectHandler (_req .HTTPRedirectHandler ):
492+ def redirect_request (self , req , fp , code , msg , headers , newurl ):
493+ if not newurl .startswith ("https://" ):
494+ raise ValueError (
495+ f"http: redirect to non-HTTPS URL rejected: { newurl !r} . "
496+ f"Only HTTPS redirects are allowed."
497+ )
498+ return super ().redirect_request (req , fp , code , msg , headers , newurl )
499+
500+ return _req .build_opener (_HTTPSOnlyRedirectHandler ())
501+
502+ def _http_get (url : Any ) -> str :
503+ import urllib .error as _err
504+ u = str (_unwrap (url ))
505+ if not u .startswith ("https://" ):
506+ raise ValueError (
507+ f"http.get: only HTTPS URLs are allowed, got: { u !r} . "
508+ f"Use 'https://' instead of 'http://'."
509+ )
510+ _MAX_RESP = 16 * 1024 * 1024
511+ opener = _https_only_opener ()
512+ try :
513+ with opener .open (u , timeout = 30 ) as resp :
514+ data = resp .read (_MAX_RESP + 1 )
515+ if len (data ) > _MAX_RESP :
516+ raise ValueError (
517+ f"http.get: response from { u !r} exceeds 16 MiB limit"
518+ )
519+ return data .decode ("utf-8" , errors = "replace" )
520+ except _err .HTTPError as exc :
521+ raise ValueError (
522+ f"http.get: HTTP { exc .code } from { u !r} : { exc .reason } "
523+ ) from exc
524+ except _err .URLError as exc :
525+ raise ValueError (
526+ f"http.get: cannot reach { u !r} : { exc .reason } "
527+ ) from exc
528+
529+ def _http_post (url : Any , body : Any ) -> str :
530+ import urllib .request as _req
531+ import urllib .error as _err
532+ u = str (_unwrap (url ))
533+ b = str (_unwrap (body )).encode ("utf-8" )
534+ if not u .startswith ("https://" ):
535+ raise ValueError (
536+ f"http.post: only HTTPS URLs are allowed, got: { u !r} . "
537+ f"Use 'https://' instead of 'http://'."
538+ )
539+ _MAX_RESP = 16 * 1024 * 1024
540+ opener = _https_only_opener ()
541+ try :
542+ request = _req .Request (
543+ u , data = b , method = "POST" ,
544+ headers = {"Content-Type" : "text/plain; charset=utf-8" },
545+ )
546+ with opener .open (request , timeout = 30 ) as resp :
547+ data = resp .read (_MAX_RESP + 1 )
548+ if len (data ) > _MAX_RESP :
549+ raise ValueError (
550+ f"http.post: response from { u !r} exceeds 16 MiB limit"
551+ )
552+ return data .decode ("utf-8" , errors = "replace" )
553+ except _err .HTTPError as exc :
554+ raise ValueError (
555+ f"http.post: HTTP { exc .code } from { u !r} : { exc .reason } "
556+ ) from exc
557+ except _err .URLError as exc :
558+ raise ValueError (
559+ f"http.post: cannot reach { u !r} : { exc .reason } "
560+ ) from exc
561+
562+ reg .register ("http.get" , _http_get , effect = "http.fetch" , returns = "String" )
563+ reg .register ("http.post" , _http_post , effect = "http.fetch" , returns = "String" )
564+
565+ # -- Standard library: JSON (V4-2c) ---------------------------------------
566+ def _json_parse (s : Any ) -> Any :
567+ import json as _json
568+ text = str (_unwrap (s ))
569+ try :
570+ return _json .loads (text )
571+ except _json .JSONDecodeError as exc :
572+ raise ValueError (f"json.parse: invalid JSON: { exc } " ) from exc
573+ except RecursionError as exc :
574+ # AUD-STD-03: deeply nested JSON can cause RecursionError in
575+ # Python's JSON parser. Catch and surface as a typed PrimitiveError.
576+ raise ValueError (
577+ f"json.parse: JSON structure is too deeply nested"
578+ ) from exc
579+
580+ def _json_encode (v : Any ) -> str :
581+ import json as _json
582+ val = _unwrap (v )
583+ # Reject values that cannot be represented in JSON.
584+ if isinstance (val , _BottomType ):
585+ raise ValueError ("json.encode: cannot encode ⊥ (refusal) as JSON" )
586+ try :
587+ return _json .dumps (val , ensure_ascii = False )
588+ except (TypeError , ValueError ) as exc :
589+ raise ValueError (f"json.encode: cannot encode value: { exc } " ) from exc
590+
591+ reg .register ("json.parse" , _json_parse , effect = None , returns = "Any" )
592+ reg .register ("json.encode" , _json_encode , effect = None , returns = "String" )
593+
594+ # -- Standard library: Date arithmetic (V4-2d) ----------------------------
595+ def _clock_today () -> str :
596+ import datetime as _dt
597+ ts = time .time ()
598+ trace .clock_reads .append (ts )
599+ return _dt .date .today ().isoformat ()
600+
601+ def _clock_parse (s : Any ) -> int :
602+ import datetime as _dt
603+ text = str (_unwrap (s ))
604+ try :
605+ d = _dt .date .fromisoformat (text )
606+ return int (_dt .datetime (d .year , d .month , d .day ).timestamp ())
607+ except ValueError as exc :
608+ raise ValueError (
609+ f"clock.parse: expected YYYY-MM-DD, got { text !r} : { exc } "
610+ ) from exc
611+
612+ def _clock_add_days (ts : Any , n : Any ) -> int :
613+ return int (_num (ts )) + int (_num (n )) * 86400
614+
615+ def _clock_format (ts : Any , fmt : Any ) -> str :
616+ import datetime as _dt
617+ t = int (_num (ts ))
618+ f = str (_unwrap (fmt ))
619+ return _dt .datetime .fromtimestamp (t ).strftime (f )
620+
621+ reg .register ("clock.today" , _clock_today , effect = "clock.read" , returns = "String" )
622+ reg .register ("clock.parse" , _clock_parse , effect = None , returns = "Int" )
623+ reg .register ("clock.add_days" , _clock_add_days , effect = None , returns = "Int" )
624+ reg .register ("clock.format" , _clock_format , effect = None , returns = "String" )
625+
424626 return reg
425627
426628
0 commit comments