> ## Documentation Index
> Fetch the complete documentation index at: https://noorle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# WebAssembly Plugins

> The .npack format, the WASI Preview 2 sandbox, the nine allowed imports, and the limits a plugin actually runs under

A **plugin** is a WebAssembly component you upload. It runs in a Wasmtime sandbox under WASI Preview 2, with no ambient authority: no filesystem it was not handed, no network beyond an explicit allowlist, and hard caps on memory and CPU.

Write a plugin when the logic is yours and you want it to stay that way — proprietary scoring, a deterministic calculation, something touching values you would rather not hand to a model.

## The sandbox

A plugin gets exactly what it is granted, and nothing else.

|        |                                                                                                                                                    |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Can    | Compute; allocate memory; call outbound HTTP to allowlisted hosts; read and write within its granted filesystem scope; read declared configuration |
| Cannot | Reach the host filesystem; open arbitrary sockets; read host environment variables; talk to other processes or other plugins                       |

Only nine WASI import prefixes are permitted. Every import in the component must start with one of them:

```
wasi:clocks/   wasi:random/   wasi:cli/      wasi:sockets/   wasi:io/
wasi:filesystem/   wasi:http/   wasi:config/   wasi:keyvalue/
```

Of `wasi:keyvalue`, **only `store` is implemented**. A component importing `atomics`, `batch`, or `watch` will fail to instantiate.

The filesystem is not a real one — it is backed by object storage. A plugin sees either a session scope (`input`, read-only; `output`, read-write) or, on an agent surface only, an agent-scoped home directory. MCP and workflow surfaces never receive a home mount.

Outbound request bodies pass through a leak-detection scan on their way out.

## The `.npack` format

A `.npack` is a **gzip-compressed tar** archive. Format validation checks the gzip magic bytes.

Entries are recognized **by pattern, not by fixed filename**:

| Pattern                       | Role                                                       |
| ----------------------------- | ---------------------------------------------------------- |
| any `*.wasm`                  | The component. **Required.**                               |
| `noorle.yaml` or `noorle.yml` | Configuration. Optional.                                   |
| any `*.wit`                   | WIT interface, used to enrich tool descriptions. Optional. |
| `.env` or `env`               | Environment values. Optional.                              |

Directories are skipped; anything else is ignored with a log line.

<Note>
  There is no `manifest.json` and no `schema.json`. Tool schemas are **not** declared in a file — they are derived from the component's exported functions by reflecting over its component type. The WIT file, if present, only improves the descriptions.
</Note>

The plugin name comes from `noorle.yaml`'s metadata name, falling back to the archive filename, falling back to the WASM file stem.

### `noorle.yaml`

```yaml theme={null}
schema_version: "1.0"      # must be exactly "1.0"

metadata:
  name: loan_analyzer
  # ...

runtime: v2                # "v1" | "v2" | "edge" | "native"

permissions:
  # network allowlist, resource limits

credentials:
  # secret declarations and where to inject them
```

`schema_version` must be exactly `"1.0"`; anything else is rejected.

<Warning>
  `runtime` is validated and stored but **nothing dispatches on it**. Every plugin runs on WASI Preview 2 under Wasmtime regardless of the value. Setting `runtime: edge` or `runtime: native` does not select a different engine.
</Warning>

## Network permissions

Network access is an allowlist of hosts. A host entry may carry a wildcard (`*.example.com`), an optional path prefix, and an optional list of permitted methods. Omitting either of the latter means unrestricted on that dimension.

**An empty allowlist means no network access at all.** And if `noorle.yaml` has no `permissions` block, the plugin runs under the default sandbox — every permission field unset, which is to say no external access whatsoever.

CIDR permissions exist as a type but are **rejected at validation** with a message pointing you at host-based permissions instead.

## Credentials

Declare what secrets a plugin needs and where they go. Names must match `^[a-z0-9_]+$` and be unique.

Seven injection modes: bearer token, basic auth, a named header, a URL query parameter, a URL path placeholder, a JSON body field, and a form body field.

Secrets are decrypted **at the host boundary** and injected into the outbound request there. **Ciphertext never reaches guest memory, and neither does the plaintext secret as a readable value.** A missing required credential fails the call before the WASM runs.

## Admission validation

Every upload is checked before it can be activated:

1. It must be a valid **Component Model** binary. A core WebAssembly module is rejected.
2. Every import must match one of the nine allowed WASI prefixes.
3. It must export at least one callable function.
4. Above 20 MB, a warning is logged. This is advisory, not a rejection.

The archive is also guarded against decompression bombs: expansion is capped at **256 MB** total across all entries, enforced incrementally rather than trusting tar headers.

## Limits

Every execution runs under a platform minimum, default, and maximum:

| Limit    | Minimum    | Default     | Maximum       |
| -------- | ---------- | ----------- | ------------- |
| Timeout  | 1 s        | 30 s        | 120 s         |
| Memory   | 128 MB     | 128 MB      | 512 MB        |
| CPU fuel | 10,000,000 | 200,000,000 | 1,000,000,000 |

What can move a limit off its default differs by limit, so it is worth being precise:

| Limit    | What your `noorle.yaml` can do                | What an account setting can do               |
| -------- | --------------------------------------------- | -------------------------------------------- |
| Timeout  | Declare one; it is clamped to the range above | —                                            |
| Memory   | Declare one; it is clamped to the range above | `wasm_memory_limit_mb`, clamped, and it wins |
| CPU fuel | Nothing — a declared fuel limit is not read   | `wasm_cpu_fuel_limit`, clamped               |

Those two account settings are the only per-account WASM overrides.

The platform converts fuel to CPU time at roughly two million units per millisecond, so the 200-million default is on the order of 100 ms of CPU. That ratio is an empirical approximation the platform uses for its own accounting, not a guarantee — size a plugin with headroom rather than against the boundary.

Two independent stop mechanisms run at once: a fuel budget and an epoch deadline, plus an outer wall clock. Fuel exhaustion is reported as a timeout.

**Outbound HTTP requests are capped at 100 per execution.** This is a platform default and is not settable from `noorle.yaml` — a `max_http_requests` in your resource limits parses but is ignored.

## Upload size

| Surface            | Limit     |
| ------------------ | --------- |
| Portal, per file   | **50 MB** |
| Management API     | **20 MB** |
| Decompressed total | 256 MB    |

## Activation

A plugin version is registered **dormant**. The version row exists and the bytes are stored, but it is not serving until it is activated — so a partially-completed registration cannot start answering calls.

The WASM binary itself is content-addressed by its SHA-256. Identical bytes uploaded twice are stored once. On load, a BLAKE3 integrity check runs against the recorded hash; a mismatch is a hard failure.

If the final activation step fails, the new version stays dormant and the previously active version keeps serving.

## Performance

Compiled components are cached and pre-instantiated per content hash, so identical WASM shared across several capabilities compiles once. A second, byte-weighted cache holds plugin bytes per pod so a warm plugin skips the object-store fetch.

The practical consequence: the first call to a newly uploaded plugin pays compilation, and subsequent calls do not.

## Languages

Five toolchains are supported, each with a starter template: **Rust**, **Python**, **Go**, **JavaScript**, and **TypeScript**.

Any language that compiles to a WASI Preview 2 **component** works. The requirement is the Component Model, not just a core `.wasm` module — for Rust that means building for the `wasm32-wasip2` target. A core WebAssembly module is rejected at validation.

For per-language toolchain setup, see the [Build tab language guides](/docs/build/languages/rust).

## Practical guidance

<CardGroup cols={2}>
  <Card title="Declare the narrowest allowlist" icon="shield">
    No permissions block means no network at all. That is usually the right starting point — add hosts only when a call actually needs one.
  </Card>

  <Card title="Let the host hold your secrets" icon="key">
    Declare credentials and an injection mode instead of reading a secret inside the guest. The plaintext then never enters WASM memory.
  </Card>

  <Card title="Keep plugins deterministic" icon="function">
    Same input, same output. Retries and durable replay both assume it.
  </Card>

  <Card title="Every plugin tool is Act tier" icon="triangle-exclamation">
    The platform cannot see inside your component, so it classifies conservatively. Under Supervised autonomy your plugin's calls pause until auto-approved.
  </Card>
</CardGroup>

***

Next: [Authentication](/docs/learn/auth/overview).
