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

# Custom OPA Policies

> Author your own OPA/Rego rules and attach them to a sandbox: deny rules at the command, network, content, lifecycle, PTY, and volume gates — additive to declaw's non-bypassable platform floor.

[Governance packs](/security/governance-packs) give you curated, framework-aligned policy with one `policy_ref`. When you need rules specific to *your* threat model — a command denylist, an egress allowlist, a model allowlist — you can author them yourself in [OPA](https://www.openpolicyagent.org/) Rego and attach them to a sandbox with `CustomPolicyConfig`.

Your rules are **additive to declaw's non-bypassable platform floor** (which already blocks living-off-the-land commands, kernel-module loading, device/storage operations, and cloud-metadata/IMDS access). You write `deny` rules that *tighten* policy; you can never relax a platform default.

## The six gates

Declaw evaluates Rego at six enforcement gates. Each gate is its own Rego **package** — write your rules in the package that matches the action you want to govern:

| Gate        | Package                     | Governs                                                   | When it runs                                         |
| ----------- | --------------------------- | --------------------------------------------------------- | ---------------------------------------------------- |
| `cmd`       | `declaw.platform.cmd`       | Every command the agent (or a sub-agent) runs             | Before the command executes                          |
| `network`   | `declaw.platform.network`   | Every outbound connection                                 | Per egress connection, at the proxy                  |
| `content`   | `declaw.platform.content`   | The request body on LLM egress (model, scan findings)     | On intercepted LLM traffic ([opt-in](#content-gate)) |
| `lifecycle` | `declaw.platform.lifecycle` | Sandbox provisioning (tier, template, requested features) | At sandbox create                                    |
| `pty`       | `declaw.platform.pty`       | Interactive PTY sessions                                  | At PTY creation                                      |
| `volume`    | `declaw.platform.volume`    | Volume mounts                                             | Once per attached volume, at create                  |

The gates also enforce **differently**, which matters when you test:

* **`cmd` deny** → the command is *rejected before it runs* (surfaces as an HTTP 403 / raised exception, not a non-zero exit code).
* **`network` deny** → the command *runs*, but the outbound connection is dropped at the proxy (e.g. `curl` exits non-zero with a reset/timeout — there is no 403).

Because the agent runs *inside* the declaw microVM, these gates are not advisory — they are the only path the agent's actions can take.

### The deny-only model

You only ever write `deny` rules:

```rego theme={null}
package declaw.platform.cmd

deny contains msg if {
    input.action.command == "rm"
    msg := "rm is not permitted by org policy"
}
```

<Warning>
  **Do not redeclare `default allow`.** Each platform package already derives `allow` in Go purely from "zero denials" — an `allow` rule you write is ignored, and redeclaring `default allow` collides with the platform module your rules are compiled alongside. Express *everything* as `deny contains msg if { ... }`. An allowlist is just "deny anything not in the approved set" (see the [allowlist pattern](#allowlists-with-a-deny-only-engine)).
</Warning>

`deny` is a partial set, and partial-set rules are additive across modules — so your denials compose with the platform defaults (and with each other) without ever being able to remove a platform denial.

## `CustomPolicyConfig`

| Field            | Type        | Description                                                                                                                                                                                                                                   |
| ---------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`        | `bool`      | Activates custom-policy evaluation for the sandbox.                                                                                                                                                                                           |
| `inline_rego`    | `str`       | A **single** Rego module string (one `package`). Simplest option when you only need one gate.                                                                                                                                                 |
| `inline_modules` | `list[str]` | A list of independent Rego module strings — one per `package`. Use this to govern multiple gates at once (you cannot put two packages in one `inline_rego` string).                                                                           |
| `policy_ref`     | `str`       | Reference a [built-in pack](/security/governance-packs) or a [published bundle](/security/policy-bundles). One of `name@version` (e.g. `owasp-llm-top10@v1`), `sha256:<hex>`, or `blob:<key>`.                                                |
| `default_deny`   | `bool`      | Fail behavior when the *evaluator itself* errors. `true` = an evaluator error **denies** the action (fail-closed — recommended for hard security gates). `false` = the action is allowed on evaluator error (fail-open — for advisory rules). |

<Note>
  `inline_rego`, `inline_modules`, and `policy_ref` can be combined — all sources are concatenated with the platform defaults before evaluation. Use `inline_rego` for the single-package case and `inline_modules` when your policy spans gates.
</Note>

## Worked examples

<CodeGroup>
  ```python Python theme={null}
  from declaw import Sandbox
  from declaw.security import SecurityPolicy
  from declaw.security.custom_policy import CustomPolicyConfig

  # (a) Command denylist — block ad-hoc network tools at the cmd gate.
  #     Only the listed binaries are blocked; everything else runs normally.
  CMD_DENYLIST = """\
  package declaw.platform.cmd

  blocked := {"curl", "wget", "nc", "ncat"}

  deny contains m if {
      input.action.command in blocked
      m := sprintf("command '%s' blocked by custom policy (no ad-hoc network tools)",
                   [input.action.command])
  }
  """

  sbx = Sandbox.create(
      template="python",
      security=SecurityPolicy(
          custom_policy=CustomPolicyConfig(
              enabled=True,
              default_deny=True,        # fail-closed: deny if the evaluator errors
              inline_rego=CMD_DENYLIST,
          ),
      ),
  )

  # (b) Multi-package policy via inline_modules — cmd + network in one shot.
  CMD_MODULE = (
      "package declaw.platform.cmd\n\n"
      'deny contains m if { input.action.command == "rm"; m := "no rm allowed by org policy" }'
  )
  NET_MODULE = (
      "package declaw.platform.network\n\n"
      'deny contains m if { endswith(input.action.destination, ".ru"); m := "no .ru egress" }'
  )

  sbx = Sandbox.create(
      security=SecurityPolicy(
          custom_policy=CustomPolicyConfig(
              enabled=True,
              default_deny=True,
              inline_modules=[CMD_MODULE, NET_MODULE],
          ),
      ),
  )

  # (c) Reference a built-in governance pack.
  sbx = Sandbox.create(
      security=SecurityPolicy(
          custom_policy=CustomPolicyConfig(
              enabled=True,
              policy_ref="owasp-llm-top10@v1",
          ),
      ),
  )
  ```

  ```typescript TypeScript theme={null}
  import { Sandbox, createSecurityPolicy } from "@declaw/sdk";

  // (a) Command denylist — block ad-hoc network tools at the cmd gate.
  const CMD_DENYLIST =
    "package declaw.platform.cmd\n\n" +
    'blocked := {"curl", "wget", "nc", "ncat"}\n\n' +
    "deny contains m if {\n" +
    "    input.action.command in blocked\n" +
    "    m := sprintf(\"command '%s' blocked by custom policy (no ad-hoc network tools)\",\n" +
    "                 [input.action.command])\n" +
    "}";

  let sbx = await Sandbox.create({
    template: "python",
    security: createSecurityPolicy({
      customPolicy: {
        enabled: true,
        defaultDeny: true,        // fail-closed: deny if the evaluator errors
        inlineRego: CMD_DENYLIST,
      },
    }),
  });

  // (b) Multi-package policy via inlineModules — cmd + network in one shot.
  const CMD_MODULE =
    "package declaw.platform.cmd\n\n" +
    'deny contains m if { input.action.command == "rm"; m := "no rm allowed by org policy" }';
  const NET_MODULE =
    "package declaw.platform.network\n\n" +
    'deny contains m if { endswith(input.action.destination, ".ru"); m := "no .ru egress" }';

  sbx = await Sandbox.create({
    security: createSecurityPolicy({
      customPolicy: {
        enabled: true,
        defaultDeny: true,
        inlineModules: [CMD_MODULE, NET_MODULE],
      },
    }),
  });

  // (c) Reference a built-in governance pack.
  sbx = await Sandbox.create({
    security: createSecurityPolicy({
      customPolicy: { enabled: true, policyRef: "owasp-llm-top10@v1" },
    }),
  });
  ```

  ```go Go theme={null}
  import (
      "context"

      declaw "github.com/declaw-ai/declaw-go"
  )

  // (a) Command denylist — block ad-hoc network tools at the cmd gate.
  const cmdDenylist = `
  package declaw.platform.cmd

  blocked := {"curl", "wget", "nc", "ncat"}

  deny contains m if {
      input.action.command in blocked
      m := sprintf("command '%s' blocked by custom policy (no ad-hoc network tools)",
                   [input.action.command])
  }
  `

  sbx, err := declaw.Create(ctx,
      declaw.WithTemplate("python"),
      declaw.WithSecurity(declaw.SecurityPolicy{
          CustomPolicy: &declaw.CustomPolicyConfig{
              Enabled:     true,
              DefaultDeny: true, // fail-closed: deny if the evaluator errors
              InlineRego:  cmdDenylist,
          },
      }),
  )

  // (b) Multi-package policy via InlineModules — cmd + network in one shot.
  const cmdModule = `
  package declaw.platform.cmd
  deny contains m if { input.action.command == "rm"; m := "no rm allowed by org policy" }
  `
  const netModule = `
  package declaw.platform.network
  deny contains m if { endswith(input.action.destination, ".ru"); m := "no .ru egress" }
  `

  sbx, err = declaw.Create(ctx,
      declaw.WithSecurity(declaw.SecurityPolicy{
          CustomPolicy: &declaw.CustomPolicyConfig{
              Enabled:       true,
              DefaultDeny:   true,
              InlineModules: []string{cmdModule, netModule},
          },
      }),
  )

  // (c) Reference a built-in governance pack.
  sbx, err = declaw.Create(ctx,
      declaw.WithSecurity(declaw.SecurityPolicy{
          CustomPolicy: &declaw.CustomPolicyConfig{
              Enabled:   true,
              PolicyRef: "owasp-llm-top10@v1",
          },
      }),
  )
  ```
</CodeGroup>

### Allowlists with a deny-only engine

Since you can only write `deny` rules, an allowlist is expressed as "deny anything *not* in the approved set". Combine it with `default_deny=True` so an evaluator fault also blocks:

```rego theme={null}
package declaw.platform.cmd

allowed := {"node", "npm", "ls", "cat", "echo"}

deny contains m if {
    not input.action.command in allowed
    m := sprintf("command '%s' not in allowlist", [input.action.command])
}
```

The same pattern at the network gate — an egress allowlist using `glob.match` for wildcard domains:

```rego theme={null}
package declaw.platform.network

allowed_exact := {"registry.npmjs.org"}

deny contains m if {
    not glob.match("*.github.com", [], input.action.destination)
    not input.action.destination in allowed_exact
    m := sprintf("egress to '%s' is not in the allowed destination set",
                 [input.action.destination])
}
```

## Input schema per gate

Each gate evaluates your rules against an `input` document. The key fields:

| Gate (`package`) | Key `input` fields                                                                                                                                                                                                                                                                                                                                        |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cmd`            | `input.action.command` (the binary, e.g. `"curl"` — not the full shell line), `input.action.args` (argument list)                                                                                                                                                                                                                                         |
| `network`        | `input.action.destination` (target host or IP; may include a `:port` suffix — strip it with `split(..., ":")[0]` if matching the host)                                                                                                                                                                                                                    |
| `content`        | `input.attributes.model` (LLM model from the JSON body), `input.attributes.destination` (request hostname), `input.attributes.scan_results.injection.is_injection`, `input.attributes.scan_results.toxicity.is_toxic`, `input.attributes.scan_results.pii`, `input.attributes.scan_results.code_security`, `input.attributes.scan_results.invisible_text` |
| `lifecycle`      | `input.attributes.policy` (the requested `SecurityPolicy`, e.g. `input.attributes.policy.toxicity.enabled`)                                                                                                                                                                                                                                               |
| `pty`            | `input.context.tier` (and the shared context below)                                                                                                                                                                                                                                                                                                       |
| `volume`         | `input.action.mode` (`"copy"` \| `"mount"` \| `"mount-ro"`), `input.action.mount_path` (absolute in-sandbox path)                                                                                                                                                                                                                                         |

Every gate also receives a shared **context** block: `input.context.tier`, `input.context.account_id`, `input.context.sandbox_id`, and `input.context.template`. Use it to scope a rule, e.g. forbid interactive shells on a locked-down tier:

```rego theme={null}
package declaw.platform.pty

deny contains msg if {
    input.context.tier == "free"
    msg := "interactive PTY not available on the free tier"
}
```

## Content gate

The content gate (`declaw.platform.content`) sees the **decrypted LLM request body** — the model name and the ML scanner's findings — so you can enforce a model allowlist or write cross-signal rules that combine multiple scan results.

This gate runs inside the body-scanning path, so by default it only fires when a scanner is already intercepting that domain. To run a model-allowlist rule **without** enabling an ML scanner, opt the sandbox in with `ContentGateConfig` and list the LLM API hosts to intercept:

```python theme={null}
from declaw import Sandbox
from declaw.security import SecurityPolicy
from declaw.security.custom_policy import CustomPolicyConfig
from declaw.security.content_gate import ContentGateConfig

CONTENT_MODULE = """\
package declaw.platform.content

# Rule A — block if the ML scanner flagged a prompt-injection attempt.
deny contains m if {
    input.attributes.scan_results.injection.is_injection == true
    m := "request blocked: prompt injection detected by ML scanner"
}

# Rule B — model allowlist.
approved_models := {"gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-20241022"}

deny contains m if {
    input.attributes.model != ""
    not input.attributes.model in approved_models
    m := sprintf("model '%s' is not in the org-approved model list",
                 [input.attributes.model])
}
"""

sbx = Sandbox.create(
    security=SecurityPolicy(
        # Opt this sandbox into the content gate for these hosts so the
        # model-allowlist rule runs even with no ML scanner enabled.
        content_gate=ContentGateConfig(
            enabled=True,
            domains=["api.openai.com", "api.anthropic.com"],
        ),
        custom_policy=CustomPolicyConfig(
            enabled=True,
            default_deny=True,
            inline_modules=[CONTENT_MODULE],
        ),
    ),
)
```

The model is read from the request JSON body and exposed as `input.attributes.model`. Rule A and Rule B are independent — either firing blocks the request before it reaches the upstream LLM.

<Note>
  **Content-gate caveats.** The gate is only evaluated when the connection is intercepted — either by an active ML scanner on the domain or by an explicit `ContentGateConfig` entry for that host (per-destination `network` rules are unaffected; that gate always runs). Also, `scan_results.pii.count` reflects whole-body (non-JSON) text; for JSON LLM bodies, value-level PII is scanned and redacted *separately*, so don't gate on `pii.count` for JSON traffic.
</Note>

## Testing and fail-closed behavior

Your inline Rego is **compile-checked at sandbox create**. If a module fails to parse or compile (a syntax error, a redeclared `default allow`, an unknown built-in), the create request fails with an error describing the problem — invalid policy never reaches a running sandbox.

At **runtime**, `default_deny` controls what happens if the evaluator itself errors (engine unreachable, evaluation timeout):

* `default_deny=True` (**fail-closed**) — an evaluator error *denies* the action. Recommended for any hard security gate (denylists, allowlists, model gating).
* `default_deny=False` (**fail-open**) — the action is *allowed* on evaluator error. Acceptable only for advisory rules where availability outweighs the missed check.

A quick way to validate the gate behavior end-to-end is to run a command that should be blocked and confirm the failure mode matches the gate: a `cmd`-gate denial surfaces as a 403 / raised exception, while a `network`-gate denial lets the command run but drops the connection (use a *resolvable* host so the test exercises the gate and not a DNS failure).

## Next steps

* [Governance packs](/security/governance-packs) — curated, framework-aligned policy (OWASP, NIST, EU AI Act, and more) you enable with a single `policy_ref`, with per-denial compliance evidence.
* [Policy bundles](/security/policy-bundles) — publish your own modules as a versioned, reusable bundle and reference it by `name@version`, `sha256:`, or `blob:` instead of inlining the Rego on every sandbox.
* [Network policies](/security/network-policies) — the domain-allowlist / IP-CIDR layer that runs alongside the `network` gate.
