# Sandbox & Code Execution

## gipity sandbox run
Run JavaScript, Python, or Bash in a sandboxed Docker container. No network access.

### "No network" is the SANDBOX only — you (the agent) can fetch the web
The 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."

- 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 ...`).
- Example — grab a short CC0 dig sound, trim it, keep it in the app:
  ```
  curl -sL -o src/audio/dig.ogg "https://<cc0-pack>/dig.ogg"          # agent has network
  gipity sandbox run bash --input src/audio/dig.ogg \
    "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"
  ```
- **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.

## File I/O Contract - "sandbox = your project"
- `/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`.
- 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`.
- 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.
- **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/`.
- **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:
  ```
  gipity generate image "monster sprite" -o src/images/monster.png       # staged at a synced path
  gipity sandbox run bash --input src/images/monster.png \
    "convert src/images/monster.png -resize 128x128 src/images/monster.webp"
  rm src/images/monster.png                                              # drop the intermediate; the delete syncs
  ```
  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.
- 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.
- Unchanged input files are NOT round-tripped - we compare sha256, so you only pay the file write when you actually changed a file.
- **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)`.
- 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.
- `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.

## Running it (`gipity sandbox run`)
Code 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.

`--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.

To 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):
```
gipity sandbox run bash "pdftoppm -png -r 80 docs/report.pdf tmp/preview"   # pages land in local tmp/, never persisted
```

**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.
```
gipity sandbox run --language python "print(2 ** 10)"
gipity sandbox run --language python --timeout 120 "$(cat build_report.py)"   # multi-line code, no shell-quoting pain
gipity sandbox run --language bash --input clip.mp4 "ffmpeg -y -i clip.mp4 clip.webm"
gipity sandbox run bash "echo hi; ffmpeg -version"   # interpreter shorthand pins the language
gipity sandbox run --file build_report.py            # language inferred from .py
```

## Deterministic text questions - use `text_analyze` / `gipity text`, not the sandbox
For 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`.
```
gipity text analyze "strawberry" --count r        # "r" (case-insensitive): 3 non-overlapping ...
gipity text analyze --file notes.md               # full profile: counts, words, frequency, palindrome
```

## Pre-installed Runtimes
- **Node.js 20** - full standard library
- **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
- **Perl** 5.36 - full install
- **GCC/G++**, **mingw-w64** (cross-compile to Windows .exe via `x86_64-w64-mingw32-gcc` / `-g++` / `-windres`)

## CLI Tools (prefer one-liners over scripts)
- **FFmpeg** - transcode, trim, extract frames/audio, generate thumbnails (`ffmpeg`, `ffprobe`, `ffplay`)
- **ImageMagick** (`convert`/`mogrify`/`identify`/`composite`) - resize, crop, composite, format-convert
- **Graphviz** (`dot`, `neato`, `sfdp`, `fdp`, `twopi`, `circo`) - diagrams from DOT
- **gnuplot** - CLI plotting
- **sox** - audio processing (convert, trim, mix, effects); `play`/`rec` also available
- **jq** - JSON query/transform
- **sqlite3** - ad-hoc queries on .sqlite/.db
- **PDF toolkit** - `pdftotext`, `pdfimages`, `pdfinfo`, `pdfhtml`, `pdftoppm` (PDF→PNG/PPM), `pdftocairo` (PDF→PNG/SVG), `pdfunite` (merge), `pdfseparate` (split), `pdfattach`/`pdfdetach`, `pdfsig`, `pdffonts`
- **ghostscript** (`gs`), `ps2pdf`/`pdf2ps`/`ps2epsi`/`eps2eps`/`dvipdf` - PostScript ↔ PDF
- **qpdf** - merge, split, encrypt, linearize PDFs
- **pandoc** - convert markdown/HTML/docx/epub/rst/latex
- **LibreOffice** (`soffice --headless`, `lowriter`, `localc`, `loimpress`, `lodraw`, `loweb`) - docx/xlsx/pptx → PDF or HTML
- **wkhtmltopdf** / **wkhtmltoimage** - render HTML files to PDF or PNG
- **exiftool**, **mediainfo** - read/write file metadata
- **potrace**, **mkbitmap** - bitmap → SVG (`mkbitmap` preprocesses for `potrace`)
- **optipng**, **gifsicle** - image optimization; **gifdiff**, **gifview**
- **WebP toolkit** - `cwebp` (encode), `dwebp` (decode), `gif2webp`, `img2webp`, `webpmux`, `webpinfo`
- **diffimg** - pixel-diff two images
- **xmlstarlet** - query/edit XML
- **datamash**, **miller** (`mlr`) - tabular stats and CSV/JSON transforms
- **tesseract** - OCR (Python: `pytesseract.image_to_string`)
- **qrencode** - generate QR code images
- **fc-list**, **fc-match** - fontconfig: list/match fonts on the system
- **lp_solve** - linear programming solver (LP/MIP)
- **openssl** - hashes, x509, keygen, encrypt/decrypt
- **gpg** (+ `gpgsm`) - sign, encrypt, verify
- **bc** - arbitrary-precision calculator for bash
- **tree** - recursive directory listing
- **p7zip** (`7z`), **unzip/zip** (`zipinfo`, `zipsplit`, `zipcloak`), **unrar**
- **hunspell**, **figlet/toilet**, **dos2unix/unix2dos**, **sed/awk/mawk/perl**

## Discovering tools
The 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):
- CLI: `command -v ffmpeg` (exits 0 with path, 1 if missing)
- Python: `python3 -c "import foo; print(foo.__version__)"` or `pip show foo`
- Node: `node -e "console.log(require.resolve('foo'))"`

## Worked Examples

### 1. FFmpeg - transcode MP4 → WebM
```
input_files: ["videos/clip.mp4"]
language: bash
code: ffmpeg -y -i videos/clip.mp4 -c:v libvpx-vp9 -b:v 1M -c:a libopus videos/clip.webm
```
Returns: project gains `videos/clip.webm`. The `.mp4` is unchanged, so it is not re-written.

### 2. FFmpeg - extract 10 frames from a video
```
input_files: ["videos/clip.mp4"]
language: bash
code: |
  mkdir -p frames
  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
```
Returns: `frames/frame_001.jpg` … `frames/frame_010.jpg`.

### 3. ImageMagick - resize + convert format
```
input_files: ["images/photo.jpg"]
language: bash
code: |
  mkdir -p thumbnails
  convert images/photo.jpg -resize 400x400 -quality 85 thumbnails/photo.webp
```
Returns: `thumbnails/photo.webp`.

### 4. Python pandas - filter/aggregate a CSV
```
input_files: ["data/sales.csv"]
language: python
code: |
  import pandas as pd
  df = pd.read_csv('data/sales.csv')
  q4 = df[df['quarter'] == 'Q4'].groupby('region')['amount'].sum().reset_index()
  q4.to_csv('data/sales_q4.csv', index=False)
  print(q4.to_string(index=False))
```
Returns: `data/sales_q4.csv` plus a printed preview on stdout.

### 5. Python matplotlib - chart a JSON metric series → PNG
```
input_files: ["data/metrics.json"]
language: python
code: |
  import json, os
  import matplotlib
  matplotlib.use('Agg')
  import matplotlib.pyplot as plt
  d = json.load(open('data/metrics.json'))
  os.makedirs('charts', exist_ok=True)
  plt.plot(d['labels'], d['values'])
  plt.xticks(rotation=45); plt.tight_layout()
  plt.savefig('charts/metrics.png', dpi=120)
```
Returns: `charts/metrics.png` (inline in the reply).

### 6. Bash + poppler - PDF → plain text
```
input_files: ["docs/manual.pdf"]
language: bash
code: |
  mkdir -p docs
  pdftotext -layout docs/manual.pdf docs/manual.txt
  wc -l docs/manual.txt
```
Returns: `docs/manual.txt` plus the line count on stdout.

### 7. qrencode - QR code for a deployed URL → PNG in the app
When 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.
```
language: bash
code: |
  mkdir -p src/images
  qrencode -o src/images/qr.png -s 10 -m 2 "https://app.gipity.ai/acme/front-desk/"
```
Returns: `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.)

## Audio/Music Generation - use the generators
Do NOT use the sandbox with sox/ffmpeg to generate music or sound effects. Use:
- `gipity generate music` - AI-generated music
- `gipity generate sound` - AI-generated sound effects
- `gipity generate speech` - AI-generated voice narration

These produce higher-quality results and handle file saving automatically.
