{
  "name": "sandbox-tools",
  "title": "Sandbox & Code Execution",
  "description": "How to use the code execution sandbox, pre-installed CLI tools, and language runtimes",
  "guid": "sk_plat_sbox",
  "category": "Agent Tools",
  "requiredTools": [
    "code_execute"
  ],
  "content": "# Sandbox & Code Execution\n\n## gipity sandbox run\nRun JavaScript, Python, or Bash in a sandboxed Docker container. No network access.\n\n### \"No network\" is the SANDBOX only — you (the agent) can fetch the web\nThe network isolation applies **inside the sandbox container**, not to you. Your own environment has full outbound network: `WebSearch`, `WebFetch`, and `curl`/`wget` in Bash all work. So the pattern for anything that needs an external asset or data is **fetch first, then feed the sandbox** — never conclude \"I can't get that, no network.\"\n\n- Downloading assets (sound effects, textures, sprite sheets, fonts, datasets, model weights): `curl`/`wget` them straight into the project tree. They **auto-sync to Gipity Storage/CDN** and ship with a `deploy`. Then, if they need processing (trim, transcode, resize), run the sandbox on the already-local files (`gipity sandbox run --input asset.ogg ...`).\n- Example — grab a short CC0 dig sound, trim it, keep it in the app:\n  ```\n  curl -sL -o src/audio/dig.ogg \"https://<cc0-pack>/dig.ogg\"          # agent has network\n  gipity sandbox run bash --input src/audio/dig.ogg \\\n    \"ffmpeg -y -i src/audio/dig.ogg -t 0.4 -af 'afade=t=out:st=0.3:d=0.1' src/audio/dig_trim.ogg\"\n  ```\n- **Licensing:** prefer CC0 / CC-BY / public-domain sources (e.g. Kenney.nl, freesound CC0, OpenGameArt, Minetest/Luanti's CC-licensed packs). Do **not** copy proprietary assets (real Minecraft/Mojang sounds, ripped game art). When no clean-licensed asset exists, synthesizing (WebAudio SFX, procedural textures) or generating with `gipity generate` is the right fallback — but reach for it because the license is bad, not because you think you can't fetch.\n\n## File I/O Contract - \"sandbox = your project\"\n- `/work/` mirrors your project files. By default the **whole project is auto-mirrored** into `/work/` (up to 1 GB); for a larger project, set `input_files` (CLI: `--input`) to narrow to just the files you need. Input paths are preserved: an `input_files` entry of `data/foo.csv` lands at `/work/data/foo.csv`.\n- Your code runs from the `/work/` root (entry is placed at `/work/_run.<ext>`, not at its repo path), so write imports/opens **relative to `/work/`**, not to the source file's original folder. A module mirrored at `src/js/engine.js` is `./src/js/engine.js` (or `/work/src/js/engine.js`) - a repo-relative `../src/js/engine.js` resolves outside `/work/` and fails with `ERR_MODULE_NOT_FOUND`.\n- Write files at **any relative path under /work/** (or the cwd, which is /work/). Anything you create or modify is written back to the project at the same path.\n- **Scratch that should NOT be saved goes in a scratch namespace: `.gipityscratch/`, `tmp/`, or any `*_tmp/` dir.** `tmp/`, `.tmp/`, and `.gipityscratch/` already exist in every run - write `tmp/chart.png` directly, no `mkdir` needed. Build/conversion intermediates *written by your code inside a run* are never persisted to your project, so they don't clutter your tree or show up as phantom server-only files on later syncs. (These are the same dirs `gipity sync`/deploy ignores, so scratch is treated as throwaway everywhere - sandbox, sync, and deploy.) You still get to LOOK at them: `gipity sandbox run` writes scratch outputs to your **local** `tmp/` etc. after the run (local-only, capped at 5MB per file) - so `tmp/chart.png` is inspectable on disk without ever entering the project. Well-known build dirs (`node_modules/`, `__pycache__/`, `.cache/`, `.venv/`, `.git/`) are auto-excluded too. Everything else you write **is** saved - so place real outputs deliberately: deployable assets at their app path (`src/images/logo.png`), reference material you want to keep but not ship in `docs/` (`docs/report.pdf`, diagrams, decks), and working intermediates in `tmp/` or `.gipityscratch/`.\n- **Scratch is write-only, never read-in: a scratch dir is one-way.** Because the mirror is built from the *synced* project and scratch dirs are never synced, a file you create **locally** at `tmp/frame.png` does **not** exist at `/work/tmp/frame.png` - the sandbox will fail with \"no such file\". Scratch works *inside* a run (create it and consume it in the same command), not as a place to stage inputs *for* a run. **Stage sandbox inputs at a real project path** (`src/…`, `assets/…`, `docs/…`), convert there, and delete the intermediate afterward if you don't want to keep it:\n  ```\n  gipity generate image \"monster sprite\" -o src/images/monster.png       # staged at a synced path\n  gipity sandbox run bash --input src/images/monster.png \\\n    \"convert src/images/monster.png -resize 128x128 src/images/monster.webp\"\n  rm src/images/monster.png                                              # drop the intermediate; the delete syncs\n  ```\n  The one non-input that DOES belong in scratch: the **driver script itself**. `$(cat tmp/build.py)` is read by your local shell, not the sandbox, so write one-off scripts to `tmp/` and pass them inline - a script at a real path (`docs/build.py`) syncs and costs a cleanup turn.\n- If you write to a path that already exists, the project file is **versioned** (not overwritten destructively). Users and agents can roll back with `file_version_restore`, so it's safe to transform files in place.\n- Unchanged input files are NOT round-tripped - we compare sha256, so you only pay the file write when you actually changed a file.\n- **Parent directories are never created for you** - not by the mirror, and not by the tools. Only the scratch namespaces (`tmp/`, `.tmp/`, `.gipityscratch/`) are pre-created; every other subdir must already exist before you write into it, or `ffmpeg`, `convert`, `pdftoppm` and a plain `>` redirect all fail with a bare `No such file or directory`. Create it in the same command - `mkdir -p out && convert ... out/receipt.png` - or write to `tmp/`, which is always there. Python: `os.makedirs('thumbnails', exist_ok=True)`.\n- When you set `input_files` explicitly (to narrow what's mirrored in): up to 5 GB per file, 6 GB total, 50 files per run - large media (video, audio) is fine. No network. 30s default / 600s max timeout.\n- `output_files` is an optional assertion: list paths you *expect* to produce, and you'll get a warning if any are missing. It does **not** filter - all new/modified files are returned regardless.\n\n## Running it (`gipity sandbox run`)\nCode is the final positional arg. `--language js|python|bash` (**required**), `--timeout <sec>` (default 30, max 600), `--input <path>` (repeatable; narrows the `/work/` mirror). The CLI pushes your local working tree up *before* running, so inputs staged any way - a Bash `cp`/`ffmpeg`/redirect, not just files written through an editor - are mirrored in; no manual `gipity sync` first. The one exception is the scratch namespaces (`tmp/`, `.gipityscratch/`, `*_tmp/`): they are never synced, so they are never mirrored in either - stage inputs at a real project path (see the File I/O Contract above). Files written under cwd (`/work/`) auto-sync back to the project at the same path. Add `--json` for machine output.\n\n`--discard-output <glob>` (repeatable) drops matching run outputs entirely - not saved, not returned; you cannot look at a discarded file. For byproducts you want to LOOK at but never keep, don't discard - write them under `tmp/` (comes back to local disk only, never enters the project). Globs support `*` (one segment), `**`, and `dir/` prefixes; the CLI lists what was dropped.\n\nTo inspect a produced PDF, `Read` the synced file directly - the Read tool renders PDF pages when the host has poppler. If it fails (\"pdftoppm is not installed\"), rasterize in the sandbox instead (it always has poppler):\n```\ngipity sandbox run bash \"pdftoppm -png -r 80 docs/report.pdf tmp/preview\"   # pages land in local tmp/, never persisted\n```\n\n**The language is always explicit.** There is no default: pin it with `--language python` (or `py`/`bash`/`js`), an interpreter token as the first arg (`gipity sandbox run bash \"<cmd>\"`, `gipity sandbox run python <file-or-code>`), or `--file script.py` (inferred from the extension). A bare `gipity sandbox run \"<code>\"` fails immediately, locally, and prints these three forms - it never guesses an interpreter for you.\n```\ngipity sandbox run --language python \"print(2 ** 10)\"\ngipity sandbox run --language python --timeout 120 \"$(cat build_report.py)\"   # multi-line code, no shell-quoting pain\ngipity sandbox run --language bash --input clip.mp4 \"ffmpeg -y -i clip.mp4 clip.webm\"\ngipity sandbox run bash \"echo hi; ffmpeg -version\"   # interpreter shorthand pins the language\ngipity sandbox run --file build_report.py            # language inferred from .py\n```\n\n## Deterministic text questions - use `text_analyze` / `gipity text`, not the sandbox\nFor letter/word counts, substring occurrences and positions, nth word/char, anagram checks, or letter frequency, don't spin up the sandbox (and never answer from an LLM's head): the `text_analyze` tool (agent surface) and `gipity text analyze \"<text>\"` (CLI) run the same exact-arithmetic engine and answer instantly. CLI: pipe via stdin or use `--file <path>` for longer text. Targeted flags short-circuit the full profile: `--count <substr>`, `--find <substr>` (1-indexed positions), `--word <n>` / `--char <n>` (negative counts from the end), `--anagram <other>`; modifiers `--whole-word`, `--case-sensitive`, `--json`.\n```\ngipity text analyze \"strawberry\" --count r        # \"r\" (case-insensitive): 3 non-overlapping ...\ngipity text analyze --file notes.md               # full profile: counts, words, frequency, palindrome\n```\n\n## Pre-installed Runtimes\n- **Node.js 20** - full standard library\n- **Python 3** - pandas, numpy, matplotlib, scipy, sympy, pillow, openpyxl, xlsxwriter, xlrd, python-docx, python-pptx, reportlab, cairosvg, seaborn, qrcode, python-barcode, requests, bs4, lxml, pyyaml, Jinja2, tabulate, csvkit (+ agate-dbf, agate-excel, agate-sql, dbfread), pyfiglet, pytesseract, python-dateutil, python-slugify, Pygments, faker, wordcloud, networkx, SQLAlchemy, Markdown, babel, fonttools\n- **Perl** 5.36 - full install\n- **GCC/G++**, **mingw-w64** (cross-compile to Windows .exe via `x86_64-w64-mingw32-gcc` / `-g++` / `-windres`)\n\n## CLI Tools (prefer one-liners over scripts)\n- **FFmpeg** - transcode, trim, extract frames/audio, generate thumbnails (`ffmpeg`, `ffprobe`, `ffplay`)\n- **ImageMagick** (`convert`/`mogrify`/`identify`/`composite`) - resize, crop, composite, format-convert\n- **Graphviz** (`dot`, `neato`, `sfdp`, `fdp`, `twopi`, `circo`) - diagrams from DOT\n- **gnuplot** - CLI plotting\n- **sox** - audio processing (convert, trim, mix, effects); `play`/`rec` also available\n- **jq** - JSON query/transform\n- **sqlite3** - ad-hoc queries on .sqlite/.db\n- **PDF toolkit** - `pdftotext`, `pdfimages`, `pdfinfo`, `pdfhtml`, `pdftoppm` (PDF→PNG/PPM), `pdftocairo` (PDF→PNG/SVG), `pdfunite` (merge), `pdfseparate` (split), `pdfattach`/`pdfdetach`, `pdfsig`, `pdffonts`\n- **ghostscript** (`gs`), `ps2pdf`/`pdf2ps`/`ps2epsi`/`eps2eps`/`dvipdf` - PostScript ↔ PDF\n- **qpdf** - merge, split, encrypt, linearize PDFs\n- **pandoc** - convert markdown/HTML/docx/epub/rst/latex\n- **LibreOffice** (`soffice --headless`, `lowriter`, `localc`, `loimpress`, `lodraw`, `loweb`) - docx/xlsx/pptx → PDF or HTML\n- **wkhtmltopdf** / **wkhtmltoimage** - render HTML files to PDF or PNG\n- **exiftool**, **mediainfo** - read/write file metadata\n- **potrace**, **mkbitmap** - bitmap → SVG (`mkbitmap` preprocesses for `potrace`)\n- **optipng**, **gifsicle** - image optimization; **gifdiff**, **gifview**\n- **WebP toolkit** - `cwebp` (encode), `dwebp` (decode), `gif2webp`, `img2webp`, `webpmux`, `webpinfo`\n- **diffimg** - pixel-diff two images\n- **xmlstarlet** - query/edit XML\n- **datamash**, **miller** (`mlr`) - tabular stats and CSV/JSON transforms\n- **tesseract** - OCR (Python: `pytesseract.image_to_string`)\n- **qrencode** - generate QR code images\n- **fc-list**, **fc-match** - fontconfig: list/match fonts on the system\n- **lp_solve** - linear programming solver (LP/MIP)\n- **openssl** - hashes, x509, keygen, encrypt/decrypt\n- **gpg** (+ `gpgsm`) - sign, encrypt, verify\n- **bc** - arbitrary-precision calculator for bash\n- **tree** - recursive directory listing\n- **p7zip** (`7z`), **unzip/zip** (`zipinfo`, `zipsplit`, `zipcloak`), **unrar**\n- **hunspell**, **figlet/toilet**, **dos2unix/unix2dos**, **sed/awk/mawk/perl**\n\n## Discovering tools\nThe toolset is **fixed** and there is **no network**, so `pip install` / `npm install` / `apt-get` will fail - never reach for them. Use only what's preinstalled, and if unsure whether a tool or package is available, probe before using it (a fast check is cheaper than a failed run):\n- CLI: `command -v ffmpeg` (exits 0 with path, 1 if missing)\n- Python: `python3 -c \"import foo; print(foo.__version__)\"` or `pip show foo`\n- Node: `node -e \"console.log(require.resolve('foo'))\"`\n\n## Worked Examples\n\n### 1. FFmpeg - transcode MP4 → WebM\n```\ninput_files: [\"videos/clip.mp4\"]\nlanguage: bash\ncode: ffmpeg -y -i videos/clip.mp4 -c:v libvpx-vp9 -b:v 1M -c:a libopus videos/clip.webm\n```\nReturns: project gains `videos/clip.webm`. The `.mp4` is unchanged, so it is not re-written.\n\n### 2. FFmpeg - extract 10 frames from a video\n```\ninput_files: [\"videos/clip.mp4\"]\nlanguage: bash\ncode: |\n  mkdir -p frames\n  ffmpeg -y -i videos/clip.mp4 -vf \"fps=10/$(ffprobe -v error -show_entries format=duration -of csv=p=0 videos/clip.mp4)\" frames/frame_%03d.jpg\n```\nReturns: `frames/frame_001.jpg` … `frames/frame_010.jpg`.\n\n### 3. ImageMagick - resize + convert format\n```\ninput_files: [\"images/photo.jpg\"]\nlanguage: bash\ncode: |\n  mkdir -p thumbnails\n  convert images/photo.jpg -resize 400x400 -quality 85 thumbnails/photo.webp\n```\nReturns: `thumbnails/photo.webp`.\n\n### 4. Python pandas - filter/aggregate a CSV\n```\ninput_files: [\"data/sales.csv\"]\nlanguage: python\ncode: |\n  import pandas as pd\n  df = pd.read_csv('data/sales.csv')\n  q4 = df[df['quarter'] == 'Q4'].groupby('region')['amount'].sum().reset_index()\n  q4.to_csv('data/sales_q4.csv', index=False)\n  print(q4.to_string(index=False))\n```\nReturns: `data/sales_q4.csv` plus a printed preview on stdout.\n\n### 5. Python matplotlib - chart a JSON metric series → PNG\n```\ninput_files: [\"data/metrics.json\"]\nlanguage: python\ncode: |\n  import json, os\n  import matplotlib\n  matplotlib.use('Agg')\n  import matplotlib.pyplot as plt\n  d = json.load(open('data/metrics.json'))\n  os.makedirs('charts', exist_ok=True)\n  plt.plot(d['labels'], d['values'])\n  plt.xticks(rotation=45); plt.tight_layout()\n  plt.savefig('charts/metrics.png', dpi=120)\n```\nReturns: `charts/metrics.png` (inline in the reply).\n\n### 6. Bash + poppler - PDF → plain text\n```\ninput_files: [\"docs/manual.pdf\"]\nlanguage: bash\ncode: |\n  mkdir -p docs\n  pdftotext -layout docs/manual.pdf docs/manual.txt\n  wc -l docs/manual.txt\n```\nReturns: `docs/manual.txt` plus the line count on stdout.\n\n### 7. qrencode - QR code for a deployed URL → PNG in the app\nWhen the user asks for a QR code to a deployed app/URL (e.g. \"stick a QR code on the front desk\"), **actually generate the image** - don't hand back the URL and tell them to make one themselves.\n```\nlanguage: bash\ncode: |\n  mkdir -p src/images\n  qrencode -o src/images/qr.png -s 10 -m 2 \"https://app.gipity.ai/acme/front-desk/\"\n```\nReturns: `src/images/qr.png`, saved into the app's `src/` so it deploys with the site and is ready to print. Embed it on the page (`<img src=\"images/qr.png\">`) if they want it shown in-app, and tell the user the file path. (`-s` sets pixel size per module, `-m` the quiet-zone margin.)\n\n## Audio/Music Generation - use the generators\nDo NOT use the sandbox with sox/ffmpeg to generate music or sound effects. Use:\n- `gipity generate music` - AI-generated music\n- `gipity generate sound` - AI-generated sound effects\n- `gipity generate speech` - AI-generated voice narration\n\nThese produce higher-quality results and handle file saving automatically."
}
