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

# Publishing Custom Policy Bundles

> Publish named, versioned OPA policy bundles to your account, reference them from any sandbox by name@version / sha256 / blob, and pin a non-bypassable account-wide policy floor.

A **policy bundle** is a set of [OPA](https://www.openpolicyagent.org/) Rego modules you publish once to your account and then reference from any sandbox by a short `policy_ref`. Bundles are versioned and immutable: a published `name@version` always resolves to the exact same Rego, so a sandbox config can pin a policy that can never silently change underneath it.

This page covers the admin side — **publishing** bundles, **referencing** them, **managing** the registry, and setting a **non-bypassable account-wide floor**. For writing the Rego itself, see [Authoring custom policy](/security/custom-policy). For curated, framework-aligned bundles you can enable without writing any Rego, see [Governance Packs](/security/governance-packs).

<Note>
  Custom policy bundles build on declaw's **non-bypassable platform floor** (which already blocks living-off-the-land commands, kernel-module loading, and cloud-metadata/IMDS access). Every bundle is compile-checked against that floor at publish time. A bundle can only *add* denials — it can never relax the platform defaults.
</Note>

## Publish a bundle

`POST /admin/accounts/<account_id>/policy-bundles` with a name, a version, and the Rego modules that make up the bundle:

```bash theme={null}
curl -X POST https://api.declaw.ai/admin/accounts/<account_id>/policy-bundles \
  -H "X-Admin-Secret: $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-baseline",
    "version": "v3",
    "modules": [
      "package declaw.platform.cmd\n\ndeny[msg] {\n  input.command == \"curl\"\n  msg := \"curl is not allowed\"\n}",
      "package declaw.platform.net\n\ndeny[msg] {\n  input.host == \"169.254.169.254\"\n  msg := \"metadata egress blocked\"\n}"
    ]
  }'
```

The response confirms the stored content and gives you both ways to reference it:

```json theme={null}
{
  "account_id": "<account_id>",
  "name": "acme-baseline",
  "version": "v3",
  "content_hash": "3b1f…e9a2",
  "policy_ref": "acme-baseline@v3",
  "sha256_ref": "sha256:3b1f…e9a2"
}
```

| Field          | Meaning                                                                                   |
| -------------- | ----------------------------------------------------------------------------------------- |
| `content_hash` | The sha256 hex of the bundle's canonical bytes — the bundle's content address.            |
| `policy_ref`   | The mutable-pointer reference (`name@version`) you set on a sandbox or account floor.     |
| `sha256_ref`   | The content-addressed reference (`sha256:<hex>`) — an immutable pin to these exact bytes. |

### Validation rules

* **Compile-checked.** The modules are compiled together with the platform floor at publish time, so a bundle that does not compile is rejected with `400` *before* it is stored — never at sandbox-create time.
* **Size limit.** A bundle's canonical bytes may not exceed **1 MiB**. Larger bundles are rejected at publish (they would otherwise be unresolvable on the read path).
* **Versions are immutable.** A `name@version` is write-once. Re-publishing the *same* content is idempotent (returns the same `content_hash`), but re-publishing a `name@version` with **different** content returns `409 Conflict` — the existing `sha256_ref` is included in the error so you can see what is already pinned there. To ship a change, publish a new version (`acme-baseline@v4`).

<Tip>
  Because versions are immutable, treat `@vN` like a Git tag, not a branch. Bump the version for every change; never try to "update" a published version in place.
</Tip>

## Reference a bundle from a sandbox

A sandbox references a bundle through `custom_policy.policy_ref`. There are three reference forms, all resolving against declaw-controlled storage only (there is **no arbitrary URL fetch**):

| Form                                     | Scope                                                             | Use it for                                                                                 |
| ---------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `name@version` (e.g. `acme-baseline@v3`) | **Account-scoped**, with a `platform` fallback for built-in packs | Production. Human-readable, versioned, isolated to your account.                           |
| `sha256:<hex>`                           | **Content-addressed, global**                                     | Immutable pins / reproducibility — the bytes can never change, regardless of the registry. |
| `blob:<key>`                             | **Global**, raw object under the `policies/` prefix only          | Escape hatch for bundles placed in the policy bucket out-of-band.                          |

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

  sandbox = Sandbox.create(
      security=SecurityPolicy(
          custom_policy=CustomPolicyConfig(
              enabled=True,
              policy_ref="acme-baseline@v3",
          ),
      ),
  )
  ```

  ```typescript TypeScript theme={null}
  import { Sandbox } from "declaw";

  const sandbox = await Sandbox.create({
    security: {
      customPolicy: { enabled: true, policyRef: "acme-baseline@v3" },
    },
  });
  ```

  ```go Go theme={null}
  sb, err := declaw.CreateSandbox(declaw.SandboxConfig{
      Security: &declaw.SecurityPolicy{
          CustomPolicy: &declaw.CustomPolicyConfig{
              Enabled:   true,
              PolicyRef: "acme-baseline@v3",
          },
      },
  })
  ```
</CodeGroup>

### Resolution precedence

A `name@version` ref is resolved at sandbox-create time as follows:

1. Look up `(your account, name, version)` in the registry.
2. If not found, fall back to the shared `platform` account (this is how built-in [governance packs](/security/governance-packs) resolve, and how a platform team can publish org-wide baselines once).
3. If still not found, the create fails with `400` (a genuinely missing ref is a client error, not a server error).

The resolved `content_hash` is then fetched from the content-addressed store and the Rego is appended to the sandbox's policy. `sha256:` and `blob:` refs skip the registry entirely and read the object directly.

<Note>
  **Versioned name vs. sha256 — which to use.** Use **`name@version`** for production: it is account-scoped, readable, and the natural unit you bump and roll out. Use **`sha256:<hex>`** when you need an absolute immutable pin — the content address can never resolve to anything but those exact bytes, even if a registry pointer is later deleted or repointed. (The `sha256:` and `blob:` forms are content-addressed-global by design: they read the dedicated, non-tenant-writable policy bucket and ignore the account, so confidentiality of policy *text* under these forms rests on hash opacity, not access control — hashes are not secrets. For per-account isolation, use `name@version`.)
</Note>

## Manage published bundles

| Operation                                       | Request                                                               |
| ----------------------------------------------- | --------------------------------------------------------------------- |
| **List** all bundles published under an account | `GET /admin/accounts/<account_id>/policy-bundles`                     |
| **Fetch** a single bundle's metadata            | `GET /admin/accounts/<account_id>/policy-bundles/<name>/<version>`    |
| **Delete** a bundle's `name@version` pointer    | `DELETE /admin/accounts/<account_id>/policy-bundles/<name>/<version>` |

```bash theme={null}
curl https://api.declaw.ai/admin/accounts/<account_id>/policy-bundles \
  -H "X-Admin-Secret: $ADMIN_SECRET"
```

List and fetch return the same fields as publish — `name`, `version`, `content_hash`, `policy_ref`, `sha256_ref`, and `created_at`. A missing bundle returns `404`.

<Warning>
  **Delete removes the pointer, not the content.** Deleting a bundle removes only its `name@version` registry entry. The content-addressed object survives (it may be shared across versions and is verified by hash on every read), so any `sha256:<hex>` ref to the same bytes keeps resolving. To make a policy permanently unreferenceable you must stop referencing its content hash, not just delete the named pointer.
</Warning>

## Set an account-wide policy floor

To enforce a policy on **every** sandbox an account creates — with no per-sandbox change and no way for a sandbox to opt out — set the account floor:

```bash theme={null}
curl -X POST https://api.declaw.ai/admin/accounts/<account_id>/policy \
  -H "X-Admin-Secret: $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "policy_ref": "acme-baseline@v3",
    "inline_rego": "package declaw.platform.cmd\n\ndeny[msg] {\n  input.command == \"nc\"\n  msg := \"netcat is not allowed\"\n}",
    "default_deny": true
  }'
```

| Field          | Meaning                                                                                          |
| -------------- | ------------------------------------------------------------------------------------------------ |
| `enabled`      | Master switch. When `false` (or the row is absent), the account uses only the platform defaults. |
| `policy_ref`   | A published bundle (`name@version` / `sha256:` / `blob:`) to merge in as the floor.              |
| `inline_rego`  | Rego merged directly, without publishing a bundle first. Compile-checked at this call.           |
| `default_deny` | When `true`, the floor's posture is deny-by-default for the gates it covers.                     |

You can set `policy_ref`, `inline_rego`, or both — an enabled floor needs at least one. Inline Rego is **compile-validated synchronously** (a broken module returns `400` here); a `policy_ref` is validated **lazily at sandbox-create time**, since the referenced bundle may be published independently.

<Note>
  **The floor is non-bypassable.** The account floor is merged into every sandbox's policy as an additional module at create time. A sandbox's own `custom_policy` can *add* rules on top, but it **cannot remove or override** the account floor — bundle denials are additive and tighten-only.
</Note>

<Warning>
  The floor is resolved and frozen **at sandbox-create time**. Enabling or tightening the account policy does **not** retroactively apply to already-running sandboxes, and resume / snapshot-restore reuse the create-time policy rather than re-merging the current floor. Roll out a tightened floor with the expectation that it takes effect on newly created sandboxes.
</Warning>

## Related

* [Authoring custom policy](/security/custom-policy) — writing the Rego gates (`cmd`, `net`, `content`, `lifecycle`) that go into a bundle.
* [Governance Packs](/security/governance-packs) — curated, framework-aligned bundles (OWASP, NIST, EU AI Act, …) you can enable without writing Rego.
* [Compliance reporting](/security/compliance) — per-account denial evidence grouped by control, framework, and gate.
