Skip to main content
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. 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:
  • 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:
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

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

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