PHPackages                             eddmann/terrarium - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. eddmann/terrarium

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

eddmann/terrarium
=================

Typed PHP SDK for running untrusted JS, TypeScript, Python, or PHP sandboxed in WebAssembly

v1.0.0(1mo ago)00[1 issues](https://github.com/eddmann/terrarium/issues)MITPHPPHP &gt;=8.4CI passing

Since Jul 9Pushed 5d agoCompare

[ Source](https://github.com/eddmann/terrarium)[ Packagist](https://packagist.org/packages/eddmann/terrarium)[ Docs](https://github.com/eddmann/terrarium)[ RSS](/packages/eddmann-terrarium/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)DependenciesVersions (2)Used By (0)

 [![Terrarium](docs/logo.png)](docs/logo.png)

Terrarium
=========

[](#terrarium)

Run untrusted **JavaScript, TypeScript, Python, or PHP inside PHP** — sandboxed in WebAssembly, against a typed capability SDK you define in plain PHP.

You write capabilities as ordinary typed PHP functions and `register()` them; untrusted guest code runs inside a WebAssembly sandbox and calls them **by name**as a typed API. The guest reaches exactly what you registered — nothing else — and because its entire language engine runs *inside* the WASM boundary, even a memory-corruption bug in that engine cannot touch your process. **No containers, no microVMs: one PHP extension.**

The whole thing is **one uniform API** — a single `Terrarium` class. The guest's language is decided purely by which `*_guest.wasm` you load; nothing else changes.

Features
--------

[](#features)

- **Language-agnostic guests** — JavaScript ([QuickJS](guests/quickjs/README.md)or [Boa](guests/boa/README.md)), [Python](guests/rustpython/README.md), [PHP](guests/php/README.md), [TypeScript](guests/typescript/README.md); anything that targets WASM behind a tiny contract.
- **Capability allowlist** — the guest sees only the PHP functions you register, reached by their dotted names (no synthetic root). That allowlist is the entire trust boundary.
- **In-process memory isolation** — the engine runs *inside* the WASM sandbox, so an engine bug stays contained without an outer microVM/container.
- **TypeScript checked in-sandbox** — the real `tsc` runs in the guest and checks every eval against the `.d.ts` of your registered SDK, before it runs.
- **Typed SDK, three languages** — types inferred from your PHP signatures + PHPDoc and emitted as `.d.ts`, `.pyi`, or a `.php` stub.
- **Typed exceptions + captured output** — guest failures surface as a `Terrarium\Exception` family; `console.log`/`print` is captured separately and survives a throw.
- **Bounded and optionally deterministic** — memory, time, stack, and fuel limits; fuel gives reproducible runs.

Quick example
-------------

[](#quick-example)

```
require 'lib/Terrarium.php';   // or, via Composer: require 'vendor/autoload.php';

use Terrarium\Terrarium;

$wasm = new Terrarium('tests/wasm/quickjs_guest.wasm', timeoutMs: 500, memoryLimit: 32 register('user.fetch',
    /**
     * Fetch a user by ID.
     * @return array{name: string, roles: string[]}
     */
    fn (int $id): array => ['name' => 'Ada', 'roles' => ['admin', 'dev']]);

// Untrusted guest code runs sandboxed and calls the SDK by its registered name.
echo $wasm->eval('user.fetch(42).roles.length');   // => 2

echo $wasm->types('dts');   // a typed .d.ts of the SDK ('pyi' for Python, 'php' for a PHP guest)
```

Swap `quickjs_guest.wasm` for `boa_guest.wasm`, `rustpython_guest.wasm`, or `php_guest.wasm` and the host code is unchanged. Fuller programs — typed capabilities, four languages over one SDK — in [`examples/`](examples).

TypeScript, checked inside the sandbox
--------------------------------------

[](#typescript-checked-inside-the-sandbox)

Load `typescript_guest.wasm` and every eval is **type-checked against the `.d.ts`generated from your registered SDK** before it runs — the real TypeScript compiler executes inside the wasm guest:

```
use Terrarium\Terrarium;

$ts = new Terrarium('tests/wasm/typescript_guest.wasm');
$ts->register('user.fetch',
    /** @return array{name: string, roles: string[]} */
    fn (int $id): array => ['name' => 'Ada', 'roles' => ['admin', 'dev']]);

$ts->eval('user.fetch("42")');
// => Terrarium\GuestException: TS2345: Argument of type 'string' is not
//    assignable to parameter of type 'number'. (line 1) — nothing executed

$ts->eval('const u = user.fetch(42); `${u.name}: ${u.roles.join(", ")}`');
// => "Ada: admin, dev"
```

There's also a lint-style mode — `check()` validates **without running** and returns *every* diagnostic as data (`[]` = passed). It works on every guest at the depth its language allows (a full type-check here; a syntax/compile check on the JS, Python, and PHP guests). See [docs/api.md](docs/api.md).

`analyze()` is the same pass with more of the compiler's knowledge handed back. Name some callees and the TypeScript guest derives a **JSON Schema from each call's type argument** — the author writes the type, you get the contract:

```
$ts = new Terrarium('typescript_guest.wasm', typeArgumentSchemas: ['ctx.agent']);
$ts->analyze('const v = ctx.agent({ … }); v;')['schemas'];
// [['ordinal' => 0, 'callee' => 'ctx.agent', 'line' => 1,
//   'schema' => '{"type":"object","properties":{"ok":{"type":"boolean"},"note":{"type":"string"}},'
//             . '"required":["ok"],"additionalProperties":false}']]
```

Canonical JSON text, identified by call ordinal so reformatting can't repoint it, carrying the call's line for consumers that must find their schema at runtime, and anything with no faithful schema form is refused by member path rather than approximated. See [docs/api.md](docs/api.md#type-argument-schemas).

The sandbox is **synchronous** — there is no event loop, so a program that suspends can never resume. That failure is never silent: a JS/TS eval that yields a promise, leaves callbacks queued, or registers a promise reaction raises `AsyncIncomplete`, and `new Terrarium(..., syncOnly: true)` makes the TypeScript guest reject `async`, `await`, `function*`, `yield` **and every use of a promise** at compile time, with the source line and a message naming the synchronous alternative. Exactly what the run-time guard does and does not catch is spelled out in [errors.md](docs/errors.md#exactly-what-asyncincomplete-catches-and-what-it-does-not); see also [synchronous-only guests](docs/api.md#synchronous-only-guests).

The TypeScript guest's `check()` also refuses what the sandbox **engine** cannot parse even though the compiler accepts it (`accessor` class members), and its `lib` declares nothing the engine lacks (no `Intl`, no `Atomics`) — so a clean `check()` means "this will run", not merely "this type-checks".

Installation
------------

[](#installation)

Prebuilt binaries are attached to each [release](https://github.com/eddmann/terrarium/releases) for PHP 8.4 / 8.5 — self-hosted Linux, AWS Lambda (a ready Bref layer), and macOS (Apple Silicon) — plus the platform-independent PHP library and guest wasm. Enable the extension and point the facade at a guest:

```
; php.ini
extension=/path/to/terrarium-...so
```

Then pull the PHP library (the `Terrarium\Terrarium` facade + type inference) via Composer, and grab a guest engine from the `guests.zip` release artifact:

```
composer require eddmann/terrarium
```

The package requires `ext-terrarium`, so Composer errors clearly if the extension binary isn't enabled.

Or build from source (Rust 1.96+, clang, PHP dev headers — a plain cargo `cdylib`, no `phpize`; the guest fixtures are committed, so no wasm toolchain is needed):

```
git clone https://github.com/eddmann/terrarium && cd terrarium
make build      # -> target/debug/libterrarium.{so,dylib}
make test       # Rust unit tests + the PHP suites
```

→ Full matrix, Docker, and AWS Lambda / Bref instructions: **[docs/install.md](docs/install.md)**.

How it works
------------

[](#how-it-works)

```
PHP (trusted)  ──ext-php-rs──►  Rust bridge  ──wasmtime──►  WASM guest (untrusted)
   register()                  dispatch table                a real language engine
   eval()                      host_call(name, bytes)        SDK names as globals

```

1. **PHP defines the SDK.** `register()` typed PHP closures (and `grant()` live objects as opaque handles). That allowlist is the entire trust boundary.
2. **The guest runs sandboxed in WASM.** A real engine compiled to wasm runs your source and sees the SDK as frozen, multi-level globals installed from the registered names — contained by memory / CPU / time limits and zero ambient authority. Values cross as MessagePack over linear memory through one `host_call` import.
3. **Types flow to the guest author.** The SDK's types are *inferred from the PHP signatures* (Reflection + PHPDoc, incl. nested `array{…}` shapes) and emitted as `.d.ts`, `.pyi`, or a `.php` stub.

→ **[docs/architecture.md](docs/architecture.md)** for the full design.

The engines
-----------

[](#the-engines)

Five are bundled; the same bridge serves any language that targets WASM. Each guest pins the upstream version it tracks; the committed fixtures are built from these. See each guest's README for build details and internals.

- [**QuickJS-ng**](guests/quickjs/README.md) `v0.16.2` — JavaScript, C via the WASI SDK (the reference guest).
- [**Boa**](guests/boa/README.md) `0.20` — JavaScript, pure Rust (no C toolchain).
- [**RustPython**](guests/rustpython/README.md) `0.5` — Python, pure Rust.
- [**PHP**](guests/php/README.md) `8.3.14` — real php-src via its embed SAPI: sandboxed PHP *inside* PHP.
- [**TypeScript**](guests/typescript/README.md) — QuickJS-ng + `tsc` `6.0.3` as bytecode; type-checked against the SDK, erased, and run — Wizer-snapshotted so a checked eval is ~20 ms warm.

Scope
-----

[](#scope)

Terrarium's boundary is the WebAssembly VM, in-process. Unlike embedding an engine natively, a memory-corruption bug **inside the guest engine is contained** — it cannot form a pointer outside its linear memory or call a syscall you didn't import. The capability model contains *what the guest can reach*; the resource limits contain *abuse* (infinite loops, alloc bombs); and the VM contains *the engine itself*. That's a stronger default than a natively-embedded interpreter, with no outer microVM/gVisor required for memory safety.

The honest residual: a bug in **Wasmtime itself** is in the trust base — but that's a small, Rust, memory-safe, heavily-fuzzed surface, a far better bet than trusting each bundled engine's C codebase.

Documentation
-------------

[](#documentation)

- [Installation](docs/install.md) — the three pieces, prebuilt binaries, AWS Lambda (Bref), and building from source.
- [API reference](docs/api.md) — the `Terrarium` class and every method.
- [Architecture](docs/architecture.md) — why WebAssembly, the capability bridge and `host_call` ABI, marshaling, type inference, and the trust model.
- [Execution modes](docs/execution-modes.md) — shared vs. isolated instances.
- [Errors](docs/errors.md) — the exception family, the `$error` sentinel, and output capture.
- Per-engine notes under [`guests/`](guests).

License
-------

[](#license)

[MIT](LICENSE). The committed guest fixtures (`tests/wasm/*.wasm`) embed third-party engines under their own licenses — see [THIRD\_PARTY\_LICENSES.md](THIRD_PARTY_LICENSES.md).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance95

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

47d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/801020?v=4)[Edd Mann](/maintainers/eddmann)[@eddmann](https://github.com/eddmann)

---

Top Contributors

[![eddmann](https://avatars.githubusercontent.com/u/801020?v=4)](https://github.com/eddmann "eddmann (27 commits)")

---

Tags

capability-securityext-php-rsisolationjavascriptphpphp-extensionplugin-systempythonquickjsrustsandboxtypescriptuntrusted-codewasmwasmtimewebassemblyisolationtypescriptpythonsandboxphp-extensionwebassemblywasmquickjscapability-security

### Embed Badge

![Health badge](/badges/eddmann-terrarium/health.svg)

```
[![Health](https://phpackages.com/badges/eddmann-terrarium/health.svg)](https://phpackages.com/packages/eddmann-terrarium)
```

###  Alternatives

[bamarni/composer-bin-plugin

No conflicts for your bin dependencies

53025.4M1.2k](/packages/bamarni-composer-bin-plugin)[rubix/tensor

A library and extension that provides objects for scientific computing in PHP.

2801.6M5](/packages/rubix-tensor)[ihor/nspl

Non-standard PHP library (NSPL) - functional primitives toolbox and more

380369.8k](/packages/ihor-nspl)[based/laravel-typescript

Transform Laravel models into TypeScript interfaces

400157.0k](/packages/based-laravel-typescript)[swoole/phpy

Connecting the Python and PHP ecosystems together

6518.6k1](/packages/swoole-phpy)[scrumble-nl/laravel-model-ts-type

This package makes it possible to generate TypeScript types based on your models

6990.6k](/packages/scrumble-nl-laravel-model-ts-type)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
