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

# Permissions and the sandbox

> What a Noorle plugin can reach: default-deny policy, host allowlisting, the three storage mounts, environment keys, host-boundary credential injection, and resource limits.

A plugin starts with nothing. Every capability it has comes from an explicit
grant in the `permissions` block of its `noorle.yaml`.

With no `permissions` block, the platform applies a default policy whose every
field is empty:

* no outbound network,
* no storage mounts,
* no environment variables,
* no host system access of any kind.

The key syntax lives in the [noorle.yaml reference](/docs/build/plugins/configuration).
This page is about what each grant actually opens and where it is enforced.

## Network

Outbound HTTP is the only network path. Requests go through
`wasmtime-wasi-http`, and the runtime deliberately leaves WASI TCP, UDP, and
DNS **disabled** — so a plugin cannot open a raw socket to sidestep the host,
path, and method filtering.

An entry under `network.allow` is checked on every request:

```yaml theme={null}
permissions:
  network:
    allow:
      - host: "api.stripe.com"
        path_prefix: "/v1/charges"
        methods: ["POST"]
```

* **Host.** `api.example.com` matches exactly. `*.example.com` matches any
  subdomain **and the apex** `example.com`. Writing the host with a scheme
  (`https://api.example.com`) additionally pins the scheme.
* **Path.** With `path_prefix` set, only requests whose path starts with it
  pass. Omitted means every path on that host.
* **Method.** With `methods` set, only those verbs pass (matched
  case-insensitively). Omitted means every method.

A request that matches nothing is denied at the host boundary. An absent or
empty `allow` list denies everything.

**CIDR ranges are not supported.** `- cidr: "10.0.0.0/8"` fails validation at
upload with an explicit message rather than silently granting nothing.

### Request cap

Each execution may make at most **100 outbound HTTP requests**. Over the cap,
further requests fail with an HTTP-denied error. The number comes from platform
configuration and is the same for every plugin; it is not settable in
`noorle.yaml`.

### Outbound leak scanning

Request bodies are scanned frame by frame for secret-shaped content as they
stream out, with host-injected credentials exempted. **A finding is logged, not
blocked** — treat it as a detection signal, not as a guardrail that will stop
an exfiltration.

## Storage

There is no host filesystem. `permissions.storage` grants access to
object-store-backed mounts, and only these three exist:

| Grant         | Guest path          | Scoped to                  | The mount permits |
| ------------- | ------------------- | -------------------------- | ----------------- |
| `fs://input`  | `/workspace/input`  | the current session        | read              |
| `fs://output` | `/workspace/output` | the current session        | read, write       |
| `fs://home`   | `/workspace/home`   | the agent, across sessions | read, write       |

```yaml theme={null}
permissions:
  storage:
    allow:
      - uri: "fs://input"
        access: [read]
      - uri: "fs://output"
        access: [read, write]
```

The `access` list is required on every entry, and it records intent rather
than granting anything: a mount's permissions are fixed to the table above
whatever you list. Read and write access follow the mount you asked for, so
choose the mount that matches the job.

Granting a mount is necessary but not sufficient. The host layer, not the
policy, decides whether each one is actually preopened:

* `/workspace/input` and `/workspace/output` are **session-scoped**, and are
  materialised only when the call carries a session. On a call without one — an
  anonymous request on a public gateway, for instance — the guest sees neither
  directory.
* `/workspace/home` is **agent-scoped persistent storage**. The policy may
  grant it on any surface, but it is materialised only when the caller is an
  agent; on a gateway or workflow surface it is skipped and the guest simply
  does not see the directory.

Session mounts are keyed by account and session, so one caller's bytes are not
reachable from another's.

A `fs://` URI that is not one of the three names is treated as a host path
preopen, which will not resolve to anything useful. `fs://working` is a retired
name and is ignored with a warning.

## Environment variables

```yaml theme={null}
permissions:
  environment:
    allow:
      - key: "API_BASE_URL"
      - key: "LOG_LEVEL"
```

The guest sees exactly the intersection of the keys listed here and the values
configured for the version. Nothing from the host process environment reaches
it.

Values come from the archive's `.env` file at upload, or from the Portal per
version — **Plugins → your plugin → Versions → Edit configuration**. They are
write-only in the UI: you can replace a value, not read it back.

## Credentials

For anything secret, prefer `credentials` over an environment variable. The
difference is where the secret lives:

* an **environment value** is handed to your code, so your code can log it,
  send it anywhere the network policy allows, or return it in a tool result;
* a **credential** is decrypted and injected **outside the sandbox**, into the
  outbound request, on the way past. The ciphertext never enters guest memory
  and the plaintext never exists inside it.

```yaml theme={null}
credentials:
  - name: github_token
    injection:
      type: bearer
    host_patterns: ["api.github.com"]
    path_prefix: "/repos/"
    required: true
```

Seven injection shapes are supported — bearer, basic auth, a named header, a
query parameter, a path placeholder, a JSON body field, and a form body field.
See the [reference](/docs/build/plugins/configuration#credentials) for the exact
keys.

Marking a credential `required: true` means an execution with that slot unset
**fails before the component runs**, rather than making an unauthenticated
call and returning a confusing 401.

Account admins fill credential slots in the Portal, on the version's **Edit
configuration** drawer.

## Resource limits

Three stops bound a single call, and any of them ends it:

| Stop       | Default           | Bounds                   | Set where                                  |
| ---------- | ----------------- | ------------------------ | ------------------------------------------ |
| Wall clock | 30 s              | 1–120 s                  | `permissions.resources.limits.timeout`     |
| Memory     | 128 MB            | 128–512 MB               | `permissions.resources.limits.memory`      |
| CPU fuel   | 200,000,000 units | 10,000,000–1,000,000,000 | platform config; per-account override only |

A value outside the bounds is clamped, not rejected. An account-level override
(`wasm_cpu_fuel_limit`, `wasm_memory_limit_mb`) is applied last and wins,
itself clamped to the same bounds.

**Fuel exhaustion is reported to the caller as a timeout**, not as a distinct
error — a plugin that burns CPU without doing I/O will look like it hung.

## Integrity and isolation

* Component bytes are stored **content-addressed**: a version points at its
  component by digest, not by a mutable path.
* Admission validation runs before storage: the binary must be a Component
  Model component, may import only the nine allowed WASI prefixes, and must
  export at least one function. See
  [How plugins run](/docs/build/plugins/overview#admission-validation).

## Practices worth keeping

**Name hosts, not patterns.** `*.com` is a grant you did not mean to write.
The platform's own plugin-build sandbox enforces this on generated plugins:
network permissions must be scoped to specific hosts, never wildcards.

**Constrain the verb and the path** when the API lets you. `methods: ["GET"]`
on a read-only integration turns a compromised plugin into a much smaller
problem than a bare host grant.

**Never put a secret in your source.** Declare a credential, or at minimum an
environment key, and let the platform hold the value.

**Grant `fs://output` before `fs://home`.** Session-scoped output disappears
with the session; agent home persists and is shared across every session that
agent runs.

## What a denial looks like

* **Network** — the request fails at the WASI HTTP boundary with a
  denied-request error; your code sees a failed request, not a panic.
* **Environment** — the key is simply absent, so `std::env::var` returns an
  error. There is no distinct "denied" signal.
* **Storage** — an ungranted mount is not preopened, so the directory does not
  exist from the guest's point of view.
* **Missing required credential** — the execution fails before the component
  starts, with an error naming the slot.
* **Limits** — memory and fuel exhaustion and wall-clock expiry all surface as
  a timeout error to the caller.

## Next

* [noorle.yaml reference](/docs/build/plugins/configuration)
* [Publishing and versions](/docs/build/plugins/publishing)
