[
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 1.0.1 — 2026-04-22\n\n### Security\n\n* Fix **CVE-2026-5752** (CVSS 9.3, critical): sandbox escape via JavaScript\n  prototype chain traversal in `src/services/python-interpreter/service.ts`.\n  Mock `document` / `ImageData` / DOM stub objects exposed to Pyodide via\n  `jsglobals` were plain object literals that inherited from\n  `Object.prototype`, allowing sandboxed Python to walk\n  `.constructor.constructor` to the host `Function` constructor, obtain\n  host `globalThis`, and reach `require` for arbitrary code execution as\n  root. Every exposed object is now built with `Object.create(null)`;\n  read-only mocks are additionally frozen. See `SECURITY.md` and\n  [VU#414811](https://kb.cert.org/vuls/id/414811).\n* Add regression test\n  `tests/security/cve_2026_5752_proto_escape.py`.\n\n### Notes\n\nThis project remains unmaintained beyond this security release. Users are\nencouraged to migrate to a maintained sandbox.\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "# Explicit All\n* @cohere-ai/rag\n\n## Security can approve changes to CODEOWNERS:\n* @cohere-ai/security\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:21-alpine3.18\nWORKDIR /usr/src/app\nCOPY package*.json ./\nRUN apk --no-cache add curl\nRUN npm install\nRUN npm i -g typescript ts-node\nRUN npm prune --production\nCOPY . .\nEXPOSE 8080\nENV ENV_RUN_AS \"docker\"\nHEALTHCHECK --interval=1s --timeout=10s --retries=2 \\\n  CMD curl -m 10 -f http://localhost:8080/health  || kill 1 \nENTRYPOINT [ \"ts-node\" , \"src/index.ts\"] "
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Cohere\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ⚠️ No longer maintained: This repository is archived and no longer supported. If you wish to continue development, please fork the project.\n\n# Terrarium - A Simple Python Sandbox\n\nTerrarium is a relatively low latency, easy to use, and economical Python sandbox - to be used as a docker deployed container, for example in GCP Cloud Run - for executing untrusted user or LLM generated ``python`` code.\n\n- **Terrarium is fast:** 900ms runtime to generate a 200 dpi png with a simple matplotlib barchart - 500 ms for a svg version. (hosted on GCP Cloud Run)\n- **Terrarium is cheap:** We spent less than $30 a month hosting terrarium on GCP during internal annotations (2GB mem + 1vCPU and at least 1 alive instance + autoscale on demand) \n- **Terrarium is fully compartmentalized:** The sandbox gets completely recycled after every invocation. No state whatsoever is carried over between calls. *Cohere does not give any guarantees for the sandbox integrity.*\n- **Terrarium supports native input & output files:** You can send any number & type of files as part of the request and we put it them in the python filesystem. After the code execution we gather up all generated files and return them with the response.\n- **Terrarium supports many common packages:** Terrarium runs on [Pyodide](https://pyodide.org/en/stable/index.html), therefore it supports numpy, pandas, matplotlib, sympy, and other standard python packages.\n\n## Using Terrarium\n\nUsing the deployed Cloud Run is super easy - just call it with the `code` to run & authorization bearer (if so configured) as follows:\n\n```bash\ncurl -X POST --url <name of your deployed gcp cloud run> \\\n-H \"Authorization: bearer $(gcloud auth print-identity-token)\" \\\n-H \"Content-Type: application/json\" \\\n--no-buffer \\\n--data-raw '{\"code\": \"1 + 1\"}'\n```\n\nwhich returns:\n```json\n{\"output_files\":[],\"final_expression\":2,\"success\":true,\"std_out\":\"\",\"std_err\":\"\",\"code_runtime\":16}\n```\n\nThe authentication `gcloud auth print-identity-token` needs to be renewed every hour.\n\nSee `terrarium_client.py` for an easy-to-use python function to call the service - including file input & output functionality via base64 encoded files.\n\n## Sandbox Design\n\nThe sandbox is composed of multiple layers: \n\n1. Parse, compile, & execute python code inside a node.js process - via CPython compiled to webassembly, not running natively - with https://pyodide.org/en/stable/index.html. This approach restricts the untrusted code's abilities: \n    - NO access to the filesystem (pyodide provides a compartmentalized memory only guest filesystem)\n    - NO threading & multiprocessing\n    - NO ability to call a subprocess \n    - NO access to any of our hosts memory\n    - NO access to other call states: we recycle the full pyodide environment (including the virtual file system, global state, loaded libs ... the works) after every call\n    - NO network nor internet access (this is a current design choice and could be changed in the future)\n\n2. Deploy the node.js host into a GCP Cloud Run container, which restricts:\n    - runtime\n    - decouples the node.js host (in case of a breakout) from the rest of our network\n\n---\n\nThe following packages are supported out of the box:\nhttps://pyodide.org/en/stable/usage/packages-in-pyodide.html including, but not limited to:\n\n- numpy\n- pandas\n- sympy\n- beautifulsoup4\n- matplotlib (plt.show() is not supported, but plt.savefig() works like a charm - most of the time)\n- python-sat\n- scikit-learn\n- scipy\n- sqlite3 (not enabled by default, but we could load it as well)\n\n## Development\n\nYou need node.js installed on your system. To install dependencies run:\n\n```bash\nnpm install\nmkdir pyodide_cache\n```\n\nrun the server & function locally:\n```bash\nnpm run dev\n```\n\nexecute code in the terrarium:\n```bash\ncurl -X POST -H \"Content-Type: application/json\" \\\n--url http://localhost:8080 \\\n--data-raw '{\"code\": \"1 + 1\"}' \\\n--no-buffer\n```\n\nrun a set of test files (all .py files in ``/test``) through the endpoint with: \n```bash\npython terrarium_client.py http://localhost:8080\n```\n\n## Deployment\n\n### Deploy as Docker container\n\nTo run in docker:\n\n**Build:**\n\n```bash\ndocker build -t terrarium .\n```\n\n**Run:**\n```bash\ndocker run -p 8080:8080 terrarium\n```\n\n**Stop:**\n```bash\ndocker ps\n```\nto get the container id and then\n```bash\ndocker stop {container_id}\n```\n\n### Deploy to GCP Cloud Run \n\nAllocating more resources to speed up run time as well as limiting concurrency from Cloud Run:\n\n```bash\ngcloud run deploy <insert name of your deployment here> \\\n--region=us-central1 \\\n--source . \\\n--concurrency=1 \\\n--min-instances=3 \\\n--max-instances=100 \\\n--cpu=2 \\\n--memory=4Gi \\\n--no-cpu-throttling \\\n--cpu-boost \\\n--timeout=100\n```\n\n### Handling timeouts\nPyodide today runs on the node.js main process, and can block node.js from responding. Pyodide recommends using a Worker if we need to interrupt. However the interface with pyodide would be through message passing, and it doesn't support matplotlib amongst other libraries.\n\nExample code that would trigger a timeout.\n\n```bash\ncurl -m 110 -X POST <insert name of your deployment here> \\\n-H \"Authorization: bearer $(gcloud auth print-identity-token)\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"code\": \"import time\\ntime.sleep(200)\"\n}'\n```\n\nCloud Run doesn't support Dockerfile healthcheck. Once the service is deployed for the first time, you need to grab the service.yaml file and add the liveness probe.\n\n`gcloud run services describe <insert name of your deployment here> --format export > service.yaml`\n\nAdd [livenessProbe](https://cloud.google.com/run/docs/configuring/healthchecks#yaml_3) after the `image` definition \n\n```\nlivenessProbe:\n  failureThreshold: 1\n  httpGet:\n    path: /health\n    port: 8080\n  periodSeconds: 100\n  timeoutSeconds: 1\n```\nRun `gcloud run services replace service.yaml `\n\nThis is only needed once per new Cloud Run service deployed.\n\nDocker itself doesn't support auto-restarts based on HEALTHCHECK (it seems). Process with pid `1` seems protected, and can't be killed. Would need to spin up a separate service like so: https://github.com/willfarrell/docker-autoheal\n\n\n## Limitations\n\n### Ability to install packages\n\n\n\n### Network access\n\n\n\n### Complex operations\n\nFor large & complex computations we sometimes observe untraceble \"RangeError: Maximum call stack size exceeded\" exceptions in Pyodide.\n\n- This increasingly happens when we set a too high dpi parameter on png saves for matplotlib figures\n- Or highly complex pandas operations\n\nSee also: https://blog.pyodide.org/posts/function-pointer-cast-handling/\n"
  },
  {
    "path": "default_python_home/README.md",
    "content": "Hey there 👋 If you found this file via the sandbox - why don't apply at https://cohere.com/careers 🤷?"
  },
  {
    "path": "default_python_home/matplotlibrc",
    "content": "#\n# cohere default style for matplotlib\n#\nfigure.figsize:     6.4, 4.0 # make it a bit more portrait mode vs default\n\n\n# general figure styles\naxes.spines.top:    False\naxes.spines.right:  False\naxes.edgecolor:     9a9a9a\naxes.linewidth:     1\nxtick.color:        9a9a9a\nxtick.labelcolor:   black\nytick.color:        9a9a9a\nytick.labelcolor:   black\nxtick.major.width:  1\nytick.major.width:  1\n\nlines.linewidth:        2\nlines.dash_capstyle:    round\nlines.solid_capstyle:   round\n\n# not sure if we should activate this by default, it leads to wide margins in some cases\n# axes.autolimit_mode: round_numbers \n# axes.xmargin:   0.05  # x margin. \n# axes.ymargin:   0.05  # y margin.\n\n# green, orange, violet from cohere logo + default matplotlib colors\naxes.prop_cycle: cycler('color', ['39594d','ff7759','d18ee2','1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b', 'e377c2']) + cycler('linestyle', ['-',':',(0, (5, 7)), '-.', '-', '--', '-.','-', '--', '-.']) \n# we could also add default markers, but that can look too messy\n# + cycler('marker', ['o', 's', '^','o', 's', '^','o', 's', '^','o'])\n\n# this is a fiddly number - setting it too high and the sandbox rendering of a png breaks with a call stack error :shrug: when we also use the constrained layout\nfigure.dpi: 128\n\n# auto set the constrained layout - this is important to make sure the text is in bounds\n# alternative: autolayout can be a bit thorny, but could be activated with #figure.autolayout: True \nfigure.constrained_layout.use: True\n\n# by default remove the background\nsavefig.transparent: True\n# svg.fonttype: none # activate if frontends support font overrides\n\n# show faded grid\naxes.grid: True\ngrid.linestyle: dotted\ngrid.alpha: 0.5\n# make sure to put the grid lowest on the z-axis\naxes.axisbelow: True        \n"
  },
  {
    "path": "example-clients/python/requirements.txt",
    "content": "requests\ntyping_extensions\ngoogle-auth"
  },
  {
    "path": "example-clients/python/terrarium_client.py",
    "content": "import glob\nfrom sys import argv\nfrom typing import List\nfrom typing_extensions import TypedDict\nimport requests\nimport json\nimport time\nimport google.auth\nimport google.auth.transport.requests\n\n#\n# credentials needed if connecting to a gcp cloud run / function deployment\n#\ncreds, project = google.auth.default()\n\ndef get_bearer():\n    auth_req = google.auth.transport.requests.Request()\n    if creds.expired == True or creds.valid == False:\n        print(\"refreshing creds\")\n        creds.refresh(auth_req)    \n    return creds.id_token.strip()\n\n\nclass B64_FileData(TypedDict):\n    b64_data: str\n    filename: str\n\n\ndef run_terrarium(server_url:str, code:str, file_data:List[B64_FileData] = None):\n    \"\"\"\n    Executes the given code in the terrarium environment and returns the result.\n\n    Args:\n        server_url (str): The URL of the terrarium server.\n        code (str): The code to be executed in the terrarium environment.\n        file_data (dict, optional): Additional file data to be passed to the terrarium server. Defaults to None.\n\n    Returns:\n        dict: The result of executing the code in the terrarium environment.\n        The result is a dictionary with the following:\n        - success: A boolean indicating whether the code was executed successfully.\n        - error: An error object containing the type and message of the error, if any.\n        - std_out: The standard output stream as single string of the code execution.\n        - std_err: The standard error stream as single string of the code execution.\n        - code_runtime: The inner runtime of the code in milliseconds (excluding networking, auth, et al.).\n\n\n    Raises:\n        RuntimeError: If there is an error when parsing the response content.\n\n    \"\"\"\n    \n    headers = {\"Content-Type\": \"application/json\",\n               \"Authorization\":\"bearer \" + get_bearer()}\n    \n    data = {\"code\": code}\n    if file_data is not None:\n        data[\"files\"] = file_data\n\n    result = requests.post(server_url, headers=headers, json=data, stream=True)\n    \n    if result.status_code != 200:\n        return {\"success\": False,\n                \"error\": {\n                  \"type\": \"HTTPError\",\n                  \"message\": \"Error: {result.status_code} - {result.text}\"\n                },\n                \"std_out\": \"\",\n                \"std_err\": \"\",\n                \"code_runtime\": 0}\n\n    #\n    # Explanation for this contorted parsing (made possbile by stream=True):\n    #\n    # The terrarium server needs to recycle the python interpreter environment either before or after each request. \n    # We are doing it after to save on latency for the next request.\n    # BUT the annoying thing is that gcp cloud functions and optionally cloud run terminate all CPU cycles as soon as the response content is closed !!\n    # With this trick we can parse the response content, return from this function, but crucially don't have to close the connection,\n    # and then the server can recycle the python interpreter.\n    #\n    res_string = \"\"\n    \n    try:\n        for c in result.iter_content(decode_unicode=True):\n            if c == \"\\n\":\n                break\n            res_string+=c\n        return json.loads(res_string)\n    except json.decoder.JSONDecodeError as e:\n        raise RuntimeError(\"Error when parsing: \"+ res_string, e)\n\nimport base64\nimport os\n\ndef file_to_base64(file_path):\n    try:\n        # Read the file in binary mode\n        with open(file_path, 'rb') as file:\n            # Read the content of the file\n            file_content = file.read()\n\n            # Convert the binary content to base64 encoding\n            base64_content = base64.b64encode(file_content)\n\n            # Decode the base64 bytes to a UTF-8 string\n            base64_string = base64_content.decode('utf-8')\n\n            return base64_string\n\n    except FileNotFoundError:\n        print(f\"Error: File not found - {file_path}\")\n    except Exception as e:\n        print(f\"Error: {e}\")\n\n\nif __name__ == \"__main__\":\n    # get url from command line argument\n    if len(argv) < 2:\n        print(\"Usage: python terrarium_client.py <server_url>\")\n        exit(1)\n    server_url = argv[1]\n\n    current_directory = os.path.dirname(os.path.realpath(__file__))\n    test_files = glob.glob(os.path.join(current_directory, \"../../tests/**/*.py\"),recursive=True)\n    print(\"Testing files:\",test_files)\n    for file in test_files:\n        file_data = None\n        if \"file_io\" in file:\n            # load all test_file_input* files\n            input_files = glob.glob(os.path.join(current_directory, \"../../tests/file_io/test_file_*\"))\n            file_data = []\n            for f in input_files:\n                file_data.append({\"filename\": os.path.basename(f), \"b64_data\": file_to_base64(f)})\n\n        print(file)\n        print(\"---------\")\n        with open(file) as f:\n            code = \"\".join(f.readlines())\n        print(code)\n        print(\"---------\")\n        start = time.time()\n        \n        #\n        # run the code in the terrarium environment\n        #\n        result = run_terrarium(server_url, code, file_data)\n        \n        if \"output_files\" in result:\n            os.makedirs(\"tests/file_io/_outputs\",exist_ok=True)\n            for of in result[\"output_files\"]:\n                print(of[\"filename\"],of[\"b64_data\"][:20]+\"...\")\n                with open(os.path.join(\"tests/file_io/_outputs\",of[\"filename\"]),mode=\"wb\") as f2:\n                    f2.write(base64.b64decode(of[\"b64_data\"]))\n\n            del result[\"output_files\"]\n\n        print(json.dumps(result,indent=2,ensure_ascii=False))\n        print(\"response parsed after:\",time.time() - start)\n        print(\"\\n***********************\\n\")\n\n        # let the server recycle the python interpreter (useful for local testing to see true speed)\n        # disable this for load testing / testing scalability\n        time.sleep(15)"
  },
  {
    "path": "nodemon.json",
    "content": "{\n    \"watch\": [\"src\"],\n    \"ext\": \"ts\",\n    \"exec\": \"ts-node ./src/index.ts\"\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"cohere-terrarium\",\n  \"version\": \"1.0.1\",\n  \"description\": \"A simple Python sandbox for helpful LLM data agents\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/cohere-ai/cohere-terrarium.git\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"dependencies\": {\n    \"@types/express\": \"^4.17.21\",\n    \"@types/node\": \"^20.11.30\",\n    \"clean\": \"^4.0.2\",\n    \"express\": \"^4.19.2\",\n    \"pyodide\": \"^0.24.1\",\n    \"typescript\": \"^5.4.3\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"start\": \"ts-node ./src/index.ts\",\n    \"gcp-build\": \"npm run build\",\n    \"gcp-start\": \"npm run start\",\n    \"dev\": \"nodemon src/index.ts\"\n  },\n  \"devDependencies\": {\n    \"node-fetch\": \"^3.3.2\",\n    \"nodemon\": \"^3.1.0\",\n    \"ts-node\": \"^10.9.2\"\n  }\n}"
  },
  {
    "path": "src/index.ts",
    "content": "import { PyodideInterface, loadPyodide } from 'pyodide';\nimport express, { Express, Request, Response } from \"express\";\nimport { PyodidePythonEnvironment } from '../src/services/python-interpreter/service';\nimport { PythonEnvironment } from './services/python-interpreter/types';\nimport { doWithLock } from './utils/async-utils';\n\n\nconst pythonEnvironment: PythonEnvironment = new PyodidePythonEnvironment();\n// prepare python env before a request comes in\npythonEnvironment.init()\n\n//\n// The main http endpoint \n//\n// Can create more express apps if we need multiple services.\nconst terrariumApp: Express = express();\nterrariumApp.use(express.json({ limit: '100mb' }));\n\nasync function runRequest(req: any, res: any): Promise<void> {\n    res.setHeader(\"Content-Type\", \"application/json\");\n\n    // make sure pyodide is loaded\n    await pythonEnvironment.waitForReady();\n\n    //\n    // parse the request body (code & files)\n    //\n    const code = req.body.code\n    if (code == undefined || code.trim() == \"\") {\n        res.send(JSON.stringify({ \"success\": false, \"error\": { \"type\": \"parsing\", \"message\": \"no code provided\" } }) + \"\\n\");\n        return\n    }\n    let files: any[] = [] // { \"filename\": \"file.txt\", \"b64_data\": \"dGhlc...\" }]\n    if (req.body.files != undefined) {\n        files = req.body.files\n        console.log(\"Got \" + files.length + \" input files\")\n        console.log(files.map(f => f.filename + \" \" + f.b64_data.slice(0, 10) + \"... \" + f.b64_data.length))\n    }\n\n    const result = await pythonEnvironment.runCode(code, files);\n\n    // write out the answer, but do not close the response yet - otherwise gcp cloud functions terminate the cpu cycles and hibernate the recycling\n    res.write(JSON.stringify(result) + \"\\n\");\n\n    console.log(\"Reloading pyodide\");\n\n    // run the recycle background process'\n    // see https://cloud.google.com/functions/docs/bestpractices/tips#do_not_start_background_activities\n\n    await pythonEnvironment.terminate();\n    await pythonEnvironment.cleanup();\n\n    // to make gcp run it until the promise resolves & only now close the response connection\n    res.end()\n}\n\nterrariumApp.post('', async (req, res) => {\n    // queue 1 request at a time - might be better in express.js middleware probably if we run into issues (example: https://www.npmjs.com/package/express-queue though not maintained)\n    await doWithLock('python-request', () => runRequest(req, res));\n});\n\nterrariumApp.get('/health', (req, res) => {\n    res.send(\"hi!\");\n});\n\n\nconst server = terrariumApp.listen(8080, () => {\n    console.log(\"Server is running on port 8080\");\n});\n"
  },
  {
    "path": "src/services/python-interpreter/service.ts",
    "content": "import { PyodideInterface, loadPyodide } from \"pyodide\";\nimport { waitFor } from \"../../utils/async-utils\";\nimport { promises as fs } from 'fs';\nimport * as path from 'path';\nimport { CodeExecutionResponse, FileData, PythonEnvironment } from \"./types\";\n\n\nconst pythonEnvironmentHomeDir = \"/home/earth\";\nconst defaultDirectoryOuterPath = 'default_python_home';\n\n// CVE-2026-5752 hardening:\n// Plain `{}` literals inherit from Object.prototype, which lets sandboxed\n// Pyodide code walk the prototype chain (e.g. `({}).constructor.constructor`)\n// to reach the host Function constructor, obtain the host `globalThis`, and\n// from there reach Node.js internals such as `require`. Building every object\n// exposed to the sandbox with a NULL prototype (`Object.create(null)`)\n// removes the chain so `.constructor` resolves to undefined.\n//\n// Some mocks must remain writable because matplotlib-pyodide assigns to them\n// during figure init (e.g. `style_element.id = \"...\"`,\n// `el.style.display = \"none\"`). Freezing those objects causes Pyodide to\n// throw a TypeError mid-`plt.subplots()` and breaks all matplotlib output.\n// `nullProto` keeps the mock writable; `sealed` also freezes for objects\n// that are only read or only have their methods called.\nfunction nullProto<T extends object>(props: T): T {\n    return Object.assign(Object.create(null) as T, props);\n}\nfunction sealed<T extends object>(props: T): Readonly<T> {\n    return Object.freeze(nullProto(props));\n}\nconst noop = () => { /* no-op DOM stub */ };\n// `elementStub` and its `style` are NOT frozen: matplotlib-pyodide writes\n// `.id`, `.textContent`, `.style.display`, etc. on returned elements.\nconst elementStub = () => nullProto({\n    addEventListener: noop,\n    style: nullProto({}),\n    classList: sealed({ add: noop, remove: noop }),\n    setAttribute: noop,\n    appendChild: noop,\n    remove: noop,\n});\n\nexport class PyodidePythonEnvironment implements PythonEnvironment {\n    out_string = \"\"\n    err_string = \"\"\n    default_files: any[] = []\n    default_file_names = new Set()\n\n    pyodide?: PyodideInterface;\n    interruptBufferPyodide = new SharedArrayBuffer(4);\n    interrupt = new Uint8Array(this.interruptBufferPyodide);\n\n    async prepareEnvironment() {\n        console.log(\"Preparing Pyodide environment\");\n        const files = await fs.readdir(defaultDirectoryOuterPath);\n        const filePromises = files.map(file => {\n            const filePath = path.join(defaultDirectoryOuterPath, file);\n            return this.readHostFileAsync(filePath);\n        });\n        const filesData = await Promise.all(filePromises);\n        filesData.forEach(({ filename, data }) => {\n            this.default_files.push({ \"filename\": filename, \"byte_data\": new Uint8Array(data) })\n            this.default_file_names.add(filename)\n        });\n    }\n\n    async loadEnvironment(): Promise<void> {\n        console.log(\"Loading Pyodide environment\");\n        this.interrupt[0] = 0;\n        this.out_string = \"\"\n        this.err_string = \"\"\n        this.pyodide = await loadPyodide({\n            packageCacheDir: \"pyodide_cache\", // allows us to cache the packages in the cloud function deployment\n            stdout: msg => { this.out_string += msg + \"\\n\" },\n            stderr: msg => { this.err_string += msg + \"\\n\" },\n            // we need to provide fake ImageData & document objects to pyodide, because matplotlib-pyodide polyfills try to access them when initializing\n            // BUT luckily for us matplotlib-pyodide does not actually use them for .savefig rendering (only for .show()), so we can just provide empty objects\n            //\n            // SECURITY (CVE-2026-5752): every object **the Python sandbox can\n            // reach via `import js`** MUST be built with `Object.create(null)`\n            // (via `sealed`) so the sandbox cannot walk\n            // Object.prototype -> Function -> globalThis -> require.\n            //\n            // The outer `jsglobals` container is intentionally a plain object:\n            // Pyodide writes its own bookkeeping into the globals at runtime,\n            // and freezing the container silently drops those writes (which\n            // later manifests as `'hiwire_call_bound' in undefined` when\n            // Pyodide tries to walk a JS error stack). It is the *values*\n            // exposed to the sandbox that need null prototypes, not the\n            // container Pyodide owns.\n            jsglobals: {\n                clearInterval, clearTimeout, setInterval, setTimeout,\n                ImageData: Object.freeze(Object.create(null)),\n                document: sealed({\n                    getElementById: (id: any) => {\n                        if (id.includes(\"canvas\")) return null; // lol don't ask ... this is needed! https://github.com/pyodide/matplotlib-pyodide/blob/61935f72718c0754a9b94e1569a685ad3c50ae91/matplotlib_pyodide/wasm_backend.py#L48\n                        return elementStub();\n                    },\n                    createElement: () => elementStub(),\n                    createTextNode: () => elementStub(),\n                    body: sealed({ appendChild: noop }),\n                }),\n            },\n            env: { \"HOME\": pythonEnvironmentHomeDir } // using a non-descriptive home dir\n        });\n\n        let pyodide = this.pyodide!;\n        // write the default files from default_python_home to the pyodide file system\n        this.default_files.forEach((f) => {\n            pyodide.FS.writeFile(pyodide?.PATH.join2(pythonEnvironmentHomeDir, f.filename), f.byte_data);\n        })\n        // load the packages we commonly use to avoid the latency hit during the user req\n        await pyodide.loadPackage([\"numpy\", \"matplotlib\", \"pandas\"])\n\n        // set interrupt buffer to allow for termination\n        pyodide.setInterruptBuffer(this.interrupt);\n\n        // second part of the import (also takes a latency hit), its ok to re-import packages\n        await pyodide.runPythonAsync(\"import matplotlib.pyplot as plt\\nimport pandas as pd\\nimport numpy as np\")\n        console.log(\"Pyodide is loaded with packages imported\")\n        return Promise.resolve();\n    }\n\n    async init(): Promise<void> {\n        await this.prepareEnvironment();\n        await this.loadEnvironment();\n    }\n\n    async waitForReady(): Promise<void> {\n        //TODO won't need this in 2nd iteration\n        if (!this.pyodide) {\n            let max_tries = 0\n            while (max_tries < 100 && this.pyodide == null) {\n                await waitFor(100);\n                max_tries++;\n            }\n        }\n\n        if (this.pyodide == null) {\n            console.error(\"pyodide is still not loaded after waiting\")\n            return Promise.reject(\"pyodide is still not loaded after waiting\")\n        }\n\n        return Promise.resolve();\n    }\n\n    async terminate(): Promise<void> {\n        // terminating to avoid leak (noticed packages are loaded twice with loadEnvironment the second time)\n        this.interrupt[0] = 1;\n    }\n    async cleanup(): Promise<void> {\n        return this.loadEnvironment();\n    }\n\n\n    /**\n     * Simple helper function to read a file asynchronously.\n     * @param {string} filePath - The path of the file to be read.\n     * @returns {Promise<{ filename: string, data: Buffer }>} - A promise that resolves to an object containing the filename and the file data.\n     * @throws {Error} - If there is an error reading the file.\n     */\n    async readHostFileAsync(filePath: any): Promise<FileData> {\n        const buffer = await fs.readFile(filePath);\n        return { filename: path.basename(filePath), data: buffer };\n    }\n\n\n    /**\n     * Function to recursively list files in the pyodide file system from the given directory.\n     * @param {string} dir \n     * @returns list of file paths\n     */\n    listFilesRecursive(dir: string) {\n        var files: any[] = [];\n        var entries = this.pyodide?.FS.readdir(dir);\n\n        for (var i = 0; i < entries.length; i++) {\n            var entry = entries[i];\n            if (entry === '.' || entry === '..') {\n                // Skip entries that are themselves directories\n                continue;\n            }\n            if (this.default_file_names.has(entry)) {\n                // Skip default files\n                continue;\n            }\n            var fullPath = this.pyodide?.PATH.join2(dir, entry);\n\n            if (this.pyodide?.FS.isDir(this.pyodide.FS.stat(fullPath).mode)) {\n                // If it's a directory, recursively list files in that directory\n                files = files.concat(this.listFilesRecursive(fullPath));\n            } else {\n                // If it's a file, add it to the list\n                files.push(fullPath);\n            }\n        }\n\n        return files;\n    }\n\n    /**\n     * Reads a file from the pyodide file system from the given file path and returns its content as a base64 encoded string.\n     * @param {string} filePath - The path of the file to be read.\n     * @returns {string} - The base64 encoded content of the file.\n     */\n    readFileAsBase64(filePath: string) {\n        var fileData = this.pyodide!.FS.readFile(filePath, { encoding: 'binary' });\n        return this.bytesToBase64(fileData);\n    }\n    /**\n     * Transforms a byte array into a base64 encoded string.\n     * @param {Uint8Array} bytes the raw bytes to encode as base64\n     * @returns base64 encoded string\n     */\n    bytesToBase64(bytes: any) {\n        const binString = String.fromCodePoint(...bytes);\n        return btoa(binString);\n    }\n\n    /**\n     * transforms a base64 encoded string into a byte array.\n     * @param {string} base64 \n     * @returns Uint8Array of bytes\n     */\n    base64ToBytes(base64: any) {\n        const binString = atob(base64);\n        return (Uint8Array as any).from(binString, (m: any) => m.codePointAt(0));\n    }\n\n\n    async runCode(code: string, files: any[]): Promise<CodeExecutionResponse> {\n        const startCode = Date.now();\n        let pyodide = this.pyodide!;\n        let result: CodeExecutionResponse = { success: true };\n        try {\n            // load available and needed packages - only supports pyodide built-in packages\n            await pyodide.loadPackagesFromImports(code)\n\n            //\n            // write the input files to the pyodide file system\n            //\n            files.forEach((f) => {\n                if (f.filename == undefined || f.b64_data == undefined) {\n                    result.success = false;\n                    result.error = { type: \"parsing\", message: \"file data is missing for: \" + JSON.stringify(f) }\n                    return result;\n                }\n                // TODO make sure to create subdirectories if the file is in a subdirectory path\n                pyodide.FS.writeFile(pyodide?.PATH.join2(pythonEnvironmentHomeDir, f.filename), this.base64ToBytes(f.b64_data));\n            })\n\n\n            //\n            // !! here is where the code is actually executed !!\n            //\n            let interpreterResult = await pyodide.runPythonAsync(code);\n            //\n            // soak up newly created files and return them as output\n            //\n            var allFiles = this.listFilesRecursive(pythonEnvironmentHomeDir);\n\n            // get only the new files (not in the input files) and read as base64\n            let input_file_names = files.map(f => f.filename)\n            let new_files = allFiles\n                .filter(f => !input_file_names.includes(f.slice(pythonEnvironmentHomeDir.length + 1)))\n                .map(f => {\n                    return { \"filename\": f.slice(pythonEnvironmentHomeDir.length + 1), \"b64_data\": this.readFileAsBase64(f) } //\"content\": decodeBase64ToText(readFileAsBase64(f))\n                });\n\n            console.log(\"output files:\", new_files.map(f => f.filename + \" \" + f.b64_data.slice(0, 10) + \"... \" + f.b64_data.length));\n            result.output_files = new_files\n\n            let result_reporting = \"\"\n            if (interpreterResult != undefined) {\n                result_reporting = result.toString().replace(/\\n/g, '\\\\n');\n            }\n\n            console.log(\"[Success] Code:\", (code as any).replace(/\\n/g, '\\\\n'),\n                \"final_expression:\", result_reporting,\n                \"stdout:\", this.out_string.replace(/\\n/g, '\\\\n'),\n                \"stderr:\", this.err_string.replace(/\\n/g, '\\\\n'));\n\n\n            result.final_expression = interpreterResult;\n            result.success = true\n        }\n        catch (error: any) {\n            // enrich error message with more code context\n            let errorMsg = error.toString()\n            // check for File \"<exec>\", line N, in <module> and extract the line number\n            let lineMatch = errorMsg.match(/File \"<exec>\", line (\\d+)/)\n            console.log(\"lineMatch\", lineMatch)\n            if (lineMatch != null) {\n                let lineNum = parseInt(lineMatch[1])\n                let codeLines = code.split(\"\\n\")\n                let startLine = Math.max(1, lineNum - 4)\n                let endLine = Math.min(codeLines.length, lineNum + 4)\n                let codeContext = codeLines.slice(startLine - 1, endLine)\n                    .map((line, idx) => { return (startLine + idx) + \": \" + line })\n                    .join(\"\\n\")\n                errorMsg = errorMsg + \"\\n\\nCode context:\\n\" + codeContext\n            }\n\n            console.error(\"[Failure] Code:\", code.replace(/\\n/g, '\\\\n'),\n                \"Error:\", errorMsg.replace(/\\n/g, '\\\\n'));\n\n            result.error = { \"type\": error.type, \"message\": errorMsg };\n            result.success = false\n        }\n\n        result.std_out = this.out_string;\n        result.std_err = this.err_string;\n        result.code_runtime = (Date.now() - startCode)\n        return result;\n    }\n}"
  },
  {
    "path": "src/services/python-interpreter/types.ts",
    "content": "export interface CodeExecutionResponse {\n    success: boolean;\n    final_expression?: any;\n    output_files?: any[];\n    error?: {\n        type: string;\n        message: string;\n    };\n    std_out?: string;\n    std_err?: string;\n    code_runtime?: number;\n}\n\nexport interface FileData { \n    filename: string;\n    data: Buffer;\n}\n\nexport interface PythonEnvironment { \n    init(): Promise<void>;\n    waitForReady(): Promise<void>;\n    runCode(code: string, files: any[]): Promise<CodeExecutionResponse>;\n    cleanup(): Promise<void>;\n    terminate() : Promise<void>;\n}"
  },
  {
    "path": "src/utils/async-utils.ts",
    "content": "// From gist: https://gist.github.com/Justin-Credible/693529fa4672a0d97963b95a26897812#file-async-utils-ts\n/**\n * A wrapper around setTimeout which returns a promise. Useful for waiting for an amount of\n * time from an async function. e.g. await waitFor(1000);\n *\n * @param milliseconds The amount of time to wait.\n * @returns A promise that resolves once the given number of milliseconds has ellapsed.\n */\nexport function waitFor(milliseconds: number): Promise<void> {\n    return new Promise((resolve) => {\n      setTimeout(resolve, milliseconds);\n    });\n  }\n  \n  \n  /**\n   * Used by doWithLock() to keep track of each \"stack\" of locks for a given lock name.\n   */\n  const locksByName: Record<string, Promise<any>[]> = {};\n  \n  /**\n   * Used to ensure that only a single task for the given lock name can be executed at once.\n   * While JS is generally single threaded, this method can be useful when running asynchronous\n   * tasks which may interact with external systems (HTTP API calls, React Native plugins, etc)\n   * which will cause the main JS thread's event loop to become unblocked. By using the same\n   * lock name for a group of tasks you can ensure the only one task will ever be in progress\n   * at a given time.\n   *\n   * @param lockName The name of the lock to be obtained.\n   * @param task The task to execute.\n   * @returns The value returned by the task.\n   */\n  export async function doWithLock<T>(lockName: string, task: () => Promise<T>): Promise<T> {\n    // Ensure array present for the given lock name.\n    if (!locksByName[lockName]) {\n      locksByName[lockName] = [];\n    }\n  \n    // Obtain the stack (array) of locks (promises) for the given lock name.\n    // The lock at the bottom of the stack (index 0) is for the currently executing task.\n    const locks = locksByName[lockName];\n  \n    // Determine if this is the first/only task for the given lock name.\n    const isFirst = locks.length === 0;\n  \n    // Create the lock, which is simply a promise. Obtain the promise's resolve method which\n    // we can use to \"unlock\" the lock, which signals to the next task in line that it can start.\n  \n    let unlock = () => {};\n  \n    const newLock = new Promise<void>((resolve) => {\n      unlock = resolve;\n    });\n  \n    locks.push(newLock);\n  \n    // If this is the first task for a given lock, we can skip this. All other tasks need to wait\n    // for the immediately proceeding task to finish executing before continuing.\n    if (!isFirst) {\n      const predecessorLock = locks[locks.length - 2];\n      await predecessorLock;\n    }\n  \n    // Now that it's our turn, execute the task. We use a finally block here to ensure that we unlock\n    // the lock so the next task can start, even if our task throws an error.\n    try {\n      return await task();\n    } catch (error) {\n      throw error;\n    } finally {\n      // Ensure that our lock is removed from the stack.\n      locks.splice(0, 1);\n  \n      // Invoke unlock to signal to the next waiting task to start.\n      unlock();\n    }\n  }"
  },
  {
    "path": "tests/file_io/_outputs/test_file_output.json",
    "content": "{\n    \"company\": \"ABC Corporation\",\n    \"address\": \"123 Main Street\",\n    \"city\": \"New York\",\n    \"state\": \"NY\",\n    \"zipcode\": \"10001\",\n    \"employees\": [\n        {\n            \"name\": \"John Doe\",\n            \"position\": \"Manager\",\n            \"salary\": 50000\n        },\n        {\n            \"name\": \"Jane Smith ❤️‍🔥\",\n            \"position\": \"Developer\",\n            \"salary\": 60000\n        },\n        {\n            \"name\": \"Mike Johnson\",\n            \"position\": \"Sales Representative - äöüß\",\n            \"salary\": 40000\n        }\n    ]\n}"
  },
  {
    "path": "tests/file_io/replay_inputs.py",
    "content": "import os\ndirectory = os.path.expanduser(\"~\")\n\n# Get a list of all files in the directory\nfiles = os.listdir(directory)\n        \n# Print the list of files\nfor file in files:\n    print(file)\n    # check if the file is a directory (we are only interested in files)\n    if not os.path.isdir(file):\n        with open(file,mode=\"rb\") as f, open(file.replace(\"_input\",\"_output\"),mode=\"wb\") as f2:\n            f2.write(f.read())"
  },
  {
    "path": "tests/file_io/simple_matplotlib.py",
    "content": "import matplotlib.pyplot as plt\nimport numpy as np\n# Generate some sample data\nx = np.linspace(0, 10, 100)  # Create an array of 100 values from 0 to 10\ny = np.sin(x)  # Compute the sine values for each x\n\n# Create a line plot\nplt.plot(x, y, label='Sin(x)')\ny = np.cos(x)  # Compute the cos values for each x\n# Create a line plot\nplt.plot(x, y, label='Cos(x)')\nplt.plot(x, y+1, label='Cos(x)+1')\nplt.plot(x, y+2, label='Cos(x)+2')\n\n# Add labels and title\nplt.xlabel('X-axis')\nplt.ylabel('Y-axis')\nplt.title('Simple Matplotlib Example')\n\n# Add a legend\nplt.legend()\n\n# Show the plot\n#plt.tight_layout()\nplt.savefig(\"plot.png\")\nplt.savefig(\"plot.pdf\")\nplt.savefig(\"plot.svg\")"
  },
  {
    "path": "tests/file_io/simple_matplotlib_barchart.py",
    "content": "import matplotlib.pyplot as plt\n\n# Data for plot\ndata = {\n    'Olympus Mons': 72000,\n    'Ascraeus Mons': 11200,\n    'Arsia Mons': 10000\n}\n\n# Create horizontal bar chart\nwidth = 10\ncolors = ['#FFA500', '#FFC000', '#FFB74D']\n\nfor i, (mountain, height) in enumerate(data.items()):\n    plt.barh(mountain, height, color=colors[i], edgecolor='black')\n\nplt.xlabel('Height (feet)')\nplt.savefig('mars_mountains.png')"
  },
  {
    "path": "tests/file_io/test_file_input.json",
    "content": "{\n    \"company\": \"ABC Corporation\",\n    \"address\": \"123 Main Street\",\n    \"city\": \"New York\",\n    \"state\": \"NY\",\n    \"zipcode\": \"10001\",\n    \"employees\": [\n        {\n            \"name\": \"John Doe\",\n            \"position\": \"Manager\",\n            \"salary\": 50000\n        },\n        {\n            \"name\": \"Jane Smith ❤️‍🔥\",\n            \"position\": \"Developer\",\n            \"salary\": 60000\n        },\n        {\n            \"name\": \"Mike Johnson\",\n            \"position\": \"Sales Representative - äöüß\",\n            \"salary\": 40000\n        }\n    ]\n}"
  },
  {
    "path": "tests/functionality/error_missing_import.py",
    "content": "# disabled import\n#from sympy import Symbol\nv = Symbol('v')"
  },
  {
    "path": "tests/functionality/error_syntax_error.py",
    "content": "import matplotlib.pyplot as plt\nimport numpy as np\n# Generate some sample data\nx = np.linspace(0, 10, 100)  # Create an array of 100 values from 0 to 10\ny = np.sin(x)  # Compute the sine values for each x\n\n# Create a line plot\nplt.plot(x, y, label='Sin(x)')\ny = np.cos(x)  # Compute the cos values for each x\n# Create a line plot\nplt.plot(x, y, label='Cos(x)')\nplt.plot(x, y+1, label=Cos(x)+ one) # syntax error here\nplt.plot(x, y+2, label='Cos(x)+2')\n\n# Add labels and title\nplt.xlabel('X-axis')\nplt.ylabel('Y-axis')\nplt.title('Simple Matplotlib Example')\n\n# Add a legend\nplt.legend()\n\n# Show the plot\n#plt.tight_layout()\nplt.savefig(\"plot.png\")"
  },
  {
    "path": "tests/functionality/error_wrong_param.py",
    "content": "import matplotlib.pyplot as plt\nimport numpy as np\n# Generate some sample data\nx = np.linspace(0, 10, 100)  # Create an array of 100 values from 0 to 10\ny = np.sin(x)  # Compute the sine values for each x\n\n# Create a line plot\nplt.plot(x, y, label='Sin(x)')\ny = np.cos(x)  # Compute the cos values for each x\n# Create a line plot\nplt.plot(x, y, label='Cos(x)')\nplt.plotter(x, y+1, labels='Cos(x)+1') # param error here\nplt.plot(x, y+2, label='Cos(x)+2')\n\n# Add labels and title\nplt.xlabel('X-axis')\nplt.ylabel('Y-axis')\nplt.title('Simple Matplotlib Example')\n\n# Add a legend\nplt.legend()\n\n# Show the plot\nplt.savefig(\"plot.png\")"
  },
  {
    "path": "tests/functionality/numpy_simple.py",
    "content": "import numpy as np\n\na = np.arange(15).reshape(3, 5)\nprint(a)\n\nprint(a + 20)\n"
  },
  {
    "path": "tests/functionality/super_long_python_file.py",
    "content": "import pandas as pd\nfrom datetime import datetime\n\ndora_data = [\n\t{\n\t\t\"componentsperday\": 332,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 9.921686746987952,\n\t\t\"Date\": 1691433000000\n\t},\n\t{\n\t\t\"componentsperday\": 156,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.8333333333333337,\n\t\t\"Date\": 1691519400000\n\t},\n\t{\n\t\t\"componentsperday\": 179,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 7.033519553072626,\n\t\t\"Date\": 1691605800000\n\t},\n\t{\n\t\t\"componentsperday\": 135,\n\t\t\"meantimetoresolve\": 3,\n\t\t\"leadtimetochange\": 4.733333333333333,\n\t\t\"Date\": 1691692200000\n\t},\n\t{\n\t\t\"componentsperday\": 69,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 1.1014492753623189,\n\t\t\"Date\": 1691778600000\n\t},\n\t{\n\t\t\"componentsperday\": 17,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.0588235294117647,\n\t\t\"Date\": 1691865000000\n\t},\n\t{\n\t\t\"componentsperday\": 304,\n\t\t\"meantimetoresolve\": 443,\n\t\t\"leadtimetochange\": 4.434210526315789,\n\t\t\"Date\": 1691951400000\n\t},\n\t{\n\t\t\"componentsperday\": 208,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.264423076923077,\n\t\t\"Date\": 1692037800000\n\t},\n\t{\n\t\t\"componentsperday\": 271,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 5.409594095940959,\n\t\t\"Date\": 1692124200000\n\t},\n\t{\n\t\t\"componentsperday\": 431,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 17.08584686774942,\n\t\t\"Date\": 1692210600000\n\t},\n\t{\n\t\t\"componentsperday\": 433,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 9.030023094688222,\n\t\t\"Date\": 1692297000000\n\t},\n\t{\n\t\t\"componentsperday\": 162,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.4506172839506173,\n\t\t\"Date\": 1692383400000\n\t},\n\t{\n\t\t\"componentsperday\": 7,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1692469800000\n\t},\n\t{\n\t\t\"componentsperday\": 365,\n\t\t\"meantimetoresolve\": 1055,\n\t\t\"leadtimetochange\": 4.780821917808219,\n\t\t\"Date\": 1692556200000\n\t},\n\t{\n\t\t\"componentsperday\": 353,\n\t\t\"meantimetoresolve\": 216,\n\t\t\"leadtimetochange\": 4.9405099150141649,\n\t\t\"Date\": 1692642600000\n\t},\n\t{\n\t\t\"componentsperday\": 336,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 5.4523809523809529,\n\t\t\"Date\": 1692729000000\n\t},\n\t{\n\t\t\"componentsperday\": 157,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.515923566878981,\n\t\t\"Date\": 1692815400000\n\t},\n\t{\n\t\t\"componentsperday\": 227,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.788546255506608,\n\t\t\"Date\": 1692901800000\n\t},\n\t{\n\t\t\"componentsperday\": 40,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 15.925,\n\t\t\"Date\": 1692988200000\n\t},\n\t{\n\t\t\"componentsperday\": 21,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 1.8095238095238096,\n\t\t\"Date\": 1693074600000\n\t},\n\t{\n\t\t\"componentsperday\": 134,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 6.41044776119403,\n\t\t\"Date\": 1693161000000\n\t},\n\t{\n\t\t\"componentsperday\": 265,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 13.818867924528302,\n\t\t\"Date\": 1693247400000\n\t},\n\t{\n\t\t\"componentsperday\": 158,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 4.987341772151899,\n\t\t\"Date\": 1693333800000\n\t},\n\t{\n\t\t\"componentsperday\": 451,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 9.279379157427938,\n\t\t\"Date\": 1693420200000\n\t},\n\t{\n\t\t\"componentsperday\": 331,\n\t\t\"meantimetoresolve\": 24,\n\t\t\"leadtimetochange\": 7.637462235649547,\n\t\t\"Date\": 1693506600000\n\t},\n\t{\n\t\t\"componentsperday\": 144,\n\t\t\"meantimetoresolve\": 473,\n\t\t\"leadtimetochange\": 4.826388888888889,\n\t\t\"Date\": 1693593000000\n\t},\n\t{\n\t\t\"componentsperday\": 4,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.5,\n\t\t\"Date\": 1693679400000\n\t},\n\t{\n\t\t\"componentsperday\": 344,\n\t\t\"meantimetoresolve\": 744,\n\t\t\"leadtimetochange\": 5.77906976744186,\n\t\t\"Date\": 1693765800000\n\t},\n\t{\n\t\t\"componentsperday\": 367,\n\t\t\"meantimetoresolve\": 23,\n\t\t\"leadtimetochange\": 5.615803814713897,\n\t\t\"Date\": 1693852200000\n\t},\n\t{\n\t\t\"componentsperday\": 281,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 18.416370106761567,\n\t\t\"Date\": 1693938600000\n\t},\n\t{\n\t\t\"componentsperday\": 457,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 23.792122538293218,\n\t\t\"Date\": 1694025000000\n\t},\n\t{\n\t\t\"componentsperday\": 240,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 1.825,\n\t\t\"Date\": 1694111400000\n\t},\n\t{\n\t\t\"componentsperday\": 57,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.45614035087719298,\n\t\t\"Date\": 1694197800000\n\t},\n\t{\n\t\t\"componentsperday\": 60,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 27.25,\n\t\t\"Date\": 1694284200000\n\t},\n\t{\n\t\t\"componentsperday\": 389,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 4.29305912596401,\n\t\t\"Date\": 1694370600000\n\t},\n\t{\n\t\t\"componentsperday\": 206,\n\t\t\"meantimetoresolve\": 7,\n\t\t\"leadtimetochange\": 6.349514563106796,\n\t\t\"Date\": 1694457000000\n\t},\n\t{\n\t\t\"componentsperday\": 180,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 5.572222222222222,\n\t\t\"Date\": 1694543400000\n\t},\n\t{\n\t\t\"componentsperday\": 189,\n\t\t\"meantimetoresolve\": 38,\n\t\t\"leadtimetochange\": 2.4761904761904764,\n\t\t\"Date\": 1694629800000\n\t},\n\t{\n\t\t\"componentsperday\": 366,\n\t\t\"meantimetoresolve\": 8,\n\t\t\"leadtimetochange\": 9.295081967213115,\n\t\t\"Date\": 1694716200000\n\t},\n\t{\n\t\t\"componentsperday\": 25,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.76,\n\t\t\"Date\": 1694802600000\n\t},\n\t{\n\t\t\"componentsperday\": 0,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1694889000000\n\t},\n\t{\n\t\t\"componentsperday\": 148,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 6.952702702702703,\n\t\t\"Date\": 1694975400000\n\t},\n\t{\n\t\t\"componentsperday\": 163,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.950920245398773,\n\t\t\"Date\": 1695061800000\n\t},\n\t{\n\t\t\"componentsperday\": 112,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 18.535714285714286,\n\t\t\"Date\": 1695148200000\n\t},\n\t{\n\t\t\"componentsperday\": 146,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 5.773972602739726,\n\t\t\"Date\": 1695234600000\n\t},\n\t{\n\t\t\"componentsperday\": 613,\n\t\t\"meantimetoresolve\": 12,\n\t\t\"leadtimetochange\": 136.42088091353998,\n\t\t\"Date\": 1695321000000\n\t},\n\t{\n\t\t\"componentsperday\": 108,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.2685185185185188,\n\t\t\"Date\": 1695407400000\n\t},\n\t{\n\t\t\"componentsperday\": 1,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1695493800000\n\t},\n\t{\n\t\t\"componentsperday\": 271,\n\t\t\"meantimetoresolve\": 46,\n\t\t\"leadtimetochange\": 25.44649446494465,\n\t\t\"Date\": 1695580200000\n\t},\n\t{\n\t\t\"componentsperday\": 369,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 16.471544715447157,\n\t\t\"Date\": 1695666600000\n\t},\n\t{\n\t\t\"componentsperday\": 287,\n\t\t\"meantimetoresolve\": 63,\n\t\t\"leadtimetochange\": 9.508710801393729,\n\t\t\"Date\": 1695753000000\n\t},\n\t{\n\t\t\"componentsperday\": 359,\n\t\t\"meantimetoresolve\": 7,\n\t\t\"leadtimetochange\": 6.933147632311978,\n\t\t\"Date\": 1695839400000\n\t},\n\t{\n\t\t\"componentsperday\": 234,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 16.482905982905984,\n\t\t\"Date\": 1695925800000\n\t},\n\t{\n\t\t\"componentsperday\": 29,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.13793103448275863,\n\t\t\"Date\": 1696012200000\n\t},\n\t{\n\t\t\"componentsperday\": 34,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 24.235294117647059,\n\t\t\"Date\": 1696098600000\n\t},\n\t{\n\t\t\"componentsperday\": 74,\n\t\t\"meantimetoresolve\": 396,\n\t\t\"leadtimetochange\": 11.324324324324325,\n\t\t\"Date\": 1696185000000\n\t},\n\t{\n\t\t\"componentsperday\": 300,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 4.386666666666667,\n\t\t\"Date\": 1696271400000\n\t},\n\t{\n\t\t\"componentsperday\": 359,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 12,\n\t\t\"Date\": 1696357800000\n\t},\n\t{\n\t\t\"componentsperday\": 85,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 7.91764705882353,\n\t\t\"Date\": 1696444200000\n\t},\n\t{\n\t\t\"componentsperday\": 64,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.453125,\n\t\t\"Date\": 1696530600000\n\t},\n\t{\n\t\t\"componentsperday\": 46,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 4.456521739130435,\n\t\t\"Date\": 1696617000000\n\t},\n\t{\n\t\t\"componentsperday\": 9,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.2222222222222222,\n\t\t\"Date\": 1696703400000\n\t},\n\t{\n\t\t\"componentsperday\": 129,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 23.651162790697677,\n\t\t\"Date\": 1696789800000\n\t},\n\t{\n\t\t\"componentsperday\": 122,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 5.60655737704918,\n\t\t\"Date\": 1696876200000\n\t},\n\t{\n\t\t\"componentsperday\": 56,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 6.392857142857143,\n\t\t\"Date\": 1696962600000\n\t},\n\t{\n\t\t\"componentsperday\": 82,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 4.951219512195122,\n\t\t\"Date\": 1697049000000\n\t},\n\t{\n\t\t\"componentsperday\": 60,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.716666666666667,\n\t\t\"Date\": 1697135400000\n\t},\n\t{\n\t\t\"componentsperday\": 23,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.6086956521739131,\n\t\t\"Date\": 1697221800000\n\t},\n\t{\n\t\t\"componentsperday\": 4,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2,\n\t\t\"Date\": 1697308200000\n\t},\n\t{\n\t\t\"componentsperday\": 79,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.8734177215189877,\n\t\t\"Date\": 1697394600000\n\t},\n\t{\n\t\t\"componentsperday\": 148,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 12.891891891891892,\n\t\t\"Date\": 1697481000000\n\t},\n\t{\n\t\t\"componentsperday\": 119,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 11.663865546218487,\n\t\t\"Date\": 1697567400000\n\t},\n\t{\n\t\t\"componentsperday\": 225,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 25.56,\n\t\t\"Date\": 1697653800000\n\t},\n\t{\n\t\t\"componentsperday\": 84,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.2261904761904764,\n\t\t\"Date\": 1697740200000\n\t},\n\t{\n\t\t\"componentsperday\": 61,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0.21311475409836065,\n\t\t\"Date\": 1697826600000\n\t},\n\t{\n\t\t\"componentsperday\": 59,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.0338983050847458,\n\t\t\"Date\": 1697913000000\n\t},\n\t{\n\t\t\"componentsperday\": 198,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 16.525252525252527,\n\t\t\"Date\": 1697999400000\n\t},\n\t{\n\t\t\"componentsperday\": 136,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 2.2205882352941179,\n\t\t\"Date\": 1698085800000\n\t},\n\t{\n\t\t\"componentsperday\": 112,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 28.8125,\n\t\t\"Date\": 1698172200000\n\t},\n\t{\n\t\t\"componentsperday\": 111,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 18.117117117117119,\n\t\t\"Date\": 1698258600000\n\t},\n\t{\n\t\t\"componentsperday\": 147,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 3.061224489795918,\n\t\t\"Date\": 1698345000000\n\t},\n\t{\n\t\t\"componentsperday\": 1,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1698431400000\n\t},\n\t{\n\t\t\"componentsperday\": 0,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1698517800000\n\t},\n\t{\n\t\t\"componentsperday\": 54,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 17.203703703703704,\n\t\t\"Date\": 1698604200000\n\t},\n\t{\n\t\t\"componentsperday\": 89,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 11.146067415730338,\n\t\t\"Date\": 1698690600000\n\t},\n\t{\n\t\t\"componentsperday\": 104,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 53.875,\n\t\t\"Date\": 1698777000000\n\t},\n\t{\n\t\t\"componentsperday\": 163,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 32.94478527607362,\n\t\t\"Date\": 1698863400000\n\t},\n\t{\n\t\t\"componentsperday\": 212,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 55.424528301886798,\n\t\t\"Date\": 1698949800000\n\t},\n\t{\n\t\t\"componentsperday\": 0,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 0,\n\t\t\"Date\": 1699036200000\n\t},\n\t{\n\t\t\"componentsperday\": 4,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 85.5,\n\t\t\"Date\": 1699122600000\n\t},\n\t{\n\t\t\"componentsperday\": 87,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 26.344827586206898,\n\t\t\"Date\": 1699209000000\n\t},\n\t{\n\t\t\"componentsperday\": 40,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 9.5,\n\t\t\"Date\": 1699295400000\n\t},\n\t{\n\t\t\"componentsperday\": 15,\n\t\t\"meantimetoresolve\": 0,\n\t\t\"leadtimetochange\": 1.9333333333333334,\n\t\t\"Date\": 1699381800000\n\t}\n]\n\ndef metrics_analysis_html(data):\n\t''' Function to provide metrics analysis in html '''\n\tfor entry in data:\n\t\tentry['Date'] = datetime.utcfromtimestamp(entry['Date'] / 1000.0).strftime('%Y-%m-%d') # Convert timestamp to datetime\n\n\tdf = pd.DataFrame(dora_data) # Convert data to a Pandas DataFrame\n\tdescriptive_stats = df.describe() # Data summary\n\tsummary_in_html = descriptive_stats.to_html() # Present results in HTML\n\treturn summary_in_html\n\nmetrics_analysis_html(dora_data)\n\n"
  },
  {
    "path": "tests/functionality/sympy_simple.py",
    "content": "import sympy\nimport sympy.solvers\n\nfrom sympy import Symbol\n\n# Define the variables\nv = Symbol('v')\n\n# Define the equations\nA = -29*v + 2*v\nB = -13*v + 98\n\n# Solve the equations\nsoln = sympy.solvers.solve(A - B, v)\nprint(f'The solution to {A - B} is v = {soln}')"
  },
  {
    "path": "tests/security/create_dir.py",
    "content": "#\n# expected output in terrarium: allow to create dir in guest fs, but not show any other test dirs from previous runs (!)\n#\nimport os\nimport time\nos.makedirs(\"test_dir_\"+str(time.time()))\nprint(os.listdir())"
  },
  {
    "path": "tests/security/cve_2026_5752_proto_escape.py",
    "content": "#\n# expected output in terrarium: fail\n#\n# Regression test for CVE-2026-5752.\n#\n# Before the fix, every object exposed to the sandbox via `jsglobals` (e.g.\n# `document`, `ImageData`, the nested `style`/`classList` objects) inherited\n# from `Object.prototype`. That let sandboxed Python code reach `js.document`\n# from Pyodide, walk `.constructor.constructor` up to the host `Function`\n# constructor, and call it with `\"return globalThis\"` to obtain the host\n# Node.js global object -- from there `require(\"child_process\").execSync(...)`\n# gave arbitrary code execution as root inside the container.\n#\n# After the fix, every exposed object is built with `Object.create(null)` and\n# frozen, so `.constructor` is `undefined` and the prototype walk dead-ends.\n# This test attempts the escape; the request must fail (or at minimum return\n# an undefined `.constructor`) for the patch to be considered effective.\n#\nimport js\n\ndoc = js.document\n# .constructor must NOT resolve to a callable host Function on a patched build.\nctor = getattr(doc, \"constructor\", None)\nassert ctor is None or not callable(ctor), (\n    \"CVE-2026-5752 regression: js.document.constructor is reachable; \"\n    \"sandbox can walk the prototype chain to host globalThis.\"\n)\n\n# Belt-and-suspenders: try the full escape and make sure it raises.\ntry:\n    leak = doc.constructor.constructor(\"return globalThis\")()\n    raise AssertionError(\n        f\"CVE-2026-5752 regression: obtained host globalThis from sandbox: {leak}\"\n    )\nexcept (AttributeError, TypeError, Exception):\n    print(\"ok: prototype chain escape blocked\")\n"
  },
  {
    "path": "tests/security/list_dirs.py",
    "content": "#\n# expected output in terrarium: listing the guest root & files only, not of the root system!!\n#\nimport os\nfrom os.path import expanduser\nhome = expanduser(\"~\")\nprint(home)\nprint(home,\" files\",os.listdir(home+\"/\"))\nprint(\"root:\",os.listdir(\"/\"))"
  },
  {
    "path": "tests/security/subprocess.py",
    "content": "#\n# expected output in terrarium: fail\n#\nimport subprocess\nresult = subprocess.run('bash echo \"test\"',\n    shell=True, text=True)\n\nprint(result)"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2018\", \n    \"module\": \"commonjs\", \n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n\n  },\n  \"include\" : [\"src\"],\n  \"exclude\" : [\"node_modules\"]\n}\n"
  }
]