> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/basic-concepts/bundles.md).

# Bundles

A **bundle** is a single JSON document holding every policy your application evaluates using the Runtime SDK. It is the unit that gets built, signed, shipped and verified.

Your application loads one, verifies it, and evaluates against it in memory. Nothing is fetched per decision.

### Shape

```ts
{
  schemaVersion: 1,
  orgId?:        string,
  projectId?:    string,
  env:           string,        // "prod" | "staging" | "dev" | "test"
  generatedAt?:  string,        // ISO-8601
  policies:      RuntimePolicy[],

  // integrity and provenance — outside the signed payload
  bundleVersion?: number,       // revision counter, ≥ 1
  checksum?:      string,       // "sha256:<hex>"
  etag?:          string,
  signature?:     { algorithm: string, keyId: string, value: string }
}
```

A minimal bundle:

```json
{
  "schemaVersion": 1,
  "env": "prod",
  "bundleVersion": 1,
  "generatedAt": "2026-08-08T09:00:00.000Z",
  "policies": [
    {
      "policyKey": "login-protection",
      "activeVersion": 1,
      "defaults": { "effect": "allow" },
      "rules": [
        {
          "id": "r_deny_after_five",
          "status": "active",
          "priority": 100,
          "target": { "service": "auth", "resource": "login", "action": "authenticate" },
          "when": { "op": "gte", "path": "failedAttempts", "value": 5 },
          "effect": { "type": "deny" }
        }
      ]
    }
  ],
  "checksum": "sha256:9f2c…",
  "signature": {
    "algorithm": "ECDSA_SHA_256",
    "keyId": "release-2026-08",
    "value": "MEUCIG…"
  }
}
```

`orgId` and `projectId` are required for bundles delivered by Govplane Cloud and optional for ones you build yourself.

### The canonical payload

Checksums and signatures do **not** cover the whole file. They cover a **canonical projection** of it:

```
schemaVersion, orgId, projectId, env,
policies[] → policyKey, activeVersion, defaults, rules
```

Everything else — `generatedAt`, `bundleVersion`, `checksum`, `etag`, `signature` — is deliberately excluded. Those describe the artifact, not the policy, and including them would make a signature depend on when the file was written.

The projection is then serialised with **every object key sorted** and no whitespace, and hashed as UTF-8. Two bundles with identical policies produce identical bytes regardless of key order or formatting.

{% hint style="info" %}
This is why re-serialising a bundle — pretty-printing it, or round-tripping it through a tool that reorders keys — does not break its signature. The signed bytes are derived, not the file itself.
{% endhint %}

### Checksum

`checksum` is `sha256:<hex>` over the canonical payload. It is an integrity check, not a security control: anyone who edits the policies can recompute it.

The SDK verifies it on load. A mismatch means the file was altered or truncated after it was built, and the bundle is rejected:

```
CHECKSUM_MISMATCH — contents do not match the embedded checksum
```

### Signatures

A signature answers a different question: *who* produced this bundle. Two algorithms are accepted:

| Algorithm       | Key                   | Signature format | Local | Cloud |
| --------------- | --------------------- | ---------------- | ----- | ----- |
| `ECDSA_SHA_256` | P-256 key pair        | base64 DER       | ✅     | ✅     |
| `HMAC_SHA256`   | 256-bit shared secret | lowercase hex    | ✅     | ❌     |

**HMAC is refused from Govplane Cloud deliberately.** Verifying an HMAC signature means holding the key that produced it, and a cloud signature is made inside a validated HSM by a key that never leaves it. A shared secret would defeat that. For a bundle you signed yourself, you own both halves and HMAC is fine.

HMAC also proves less: anyone who can verify can also sign. It tells you a bundle was not altered, not who wrote it. Use ECDSA where that distinction matters.

{% hint style="warning" %}
Ed25519 is **not** supported. It was accepted by the 1.x isolated-mode verifier, but nothing has ever produced it. A bundle carrying an Ed25519 signature is rejected with a message saying so.
{% endhint %}

#### What a signature does and does not tell you

A verified signature proves the bundle has not changed since it was signed by the key you configured. It says nothing about *where* it was signed. A bundle signed by `govplane sign` on a laptop is software-signed; only bundles materialised by Govplane Cloud are signed in a validated HSM.

`signature.algorithm` records which algorithm was used, not which module produced it. If that distinction matters to you, it has to come from your own key management.

### Verification

Signature verification is not optional by accident. A bundle decides who may do what, so the SDK will not evaluate one whose provenance it cannot establish:

```ts
const govplane = await createLocalClient({
  bundlePath: "./policy-bundle.json",
  verify: { algorithm: "ECDSA_SHA_256", publicKey: process.env.GOVPLANE_PUBLIC_KEY },
});
```

Four states are distinguished, and two of them fail:

| State        | Meaning                                               | Result    |
| ------------ | ----------------------------------------------------- | --------- |
| `verified`   | A configured key checked the signature and it matched | loads     |
| `unsigned`   | No signature, and you opted in                        | loads     |
| `missing`    | A key is configured but the bundle has no signature   | **fails** |
| `unverified` | A signature exists but no key was configured          | **fails** |

The last is the subtle one. A signature nobody checked proves nothing, and reporting it as verified would be the most dangerous thing an SDK of this kind could do.

Unsigned bundles need saying so in writing:

```ts
await createLocalClient({ bundlePath: "./policy-bundle.json", allowUnsigned: true });
```

### Where bundles come from

#### Local

A file on disk. No account, no credentials, no network:

```ts
import { createLocalClient } from "@govplane/runtime-sdk";

const govplane = await createLocalClient({
  bundlePath: "./policy-bundle.json",
  verify: { algorithm: "ECDSA_SHA_256", publicKey },
});
```

Commit it, bake it into a container image, or have configuration management drop it in — they are the same artifact to the SDK. It never fetches the file itself, which keeps every network protocol, credential and failure mode outside its trust boundary.

The file is read **once, at construction**. Replacing it changes nothing until you ask:

```ts
await createLocalClient({
  bundlePath: "./policy-bundle.json",
  verify: { algorithm: "ECDSA_SHA_256", publicKey },
  watch: { intervalMs: 5000 },
  onError: (error) => logger.warn({ error }, "policy reload failed"),
});
```

Watching polls modification time rather than using `fs.watch`, which behaves differently on every platform and misses atomic-rename writes — exactly how `govplane build` writes a bundle.

#### Remote

Govplane Cloud, with delivery on top — polling, ETag revalidation, backoff and degraded-mode reporting:

```ts
import { createRemoteClient } from "@govplane/runtime-sdk";

const govplane = await createRemoteClient({
  baseUrl:    process.env.GOVPLANE_URL,
  runtimeKey: process.env.GOVPLANE_RUNTIME_KEY,
  orgId:      "org_…",
  projectId:  "proj_…",
  env:        "prod",
  verify:     { algorithm: "ECDSA_SHA_256", publicKey },
  poll:       { intervalMs: 5000 },
});
```

Everything after delivery is the same code. Both clients return the same interface and evaluate identically.

{% hint style="info" %}
**A failed reload never replaces a good bundle with nothing.** The previous bundle stays in force and `onError` is called. After repeated remote failures the client reports `degraded`, still serving the last bundle it verified.
{% endhint %}

### Building one

The CLI toolkit builds bundles from policy drafts:

```bash
npm install --global @govplane/cli @govplane/toolkit

govplane build --signed \
  --signing-algorithm ECDSA_SHA_256 \
  --ecdsa-private-key ./keys/signing-private.pem \
  --signing-key-id release-2026-08
```

Generate a P-256 key pair first:

```bash
mkdir -p keys && chmod 700 keys

openssl ecparam -name prime256v1 -genkey -noout -out keys/signing-private.pem
chmod 600 keys/signing-private.pem

openssl pkey -in keys/signing-private.pem -pubout -out keys/signing-public.pem
```

The private key signs; the public half is what you hand to `verify`. It must be unencrypted — there is no way to prompt for a passphrase in a build.

Check a bundle before shipping it:

```bash
govplane validate                    # structure, in parity with the remote validator
govplane inspect --policies          # what it actually contains
govplane inspect --signature --public-key ./keys/signing-public.pem
```

`govplane validate` applies the same rules the control plane applies when it materialises a bundle, so a bundle that passes locally also passes remotely.

### Errors

Every failure carries a stable `code`. Match on those, not on messages:

| Code                                       | Meaning                                          |
| ------------------------------------------ | ------------------------------------------------ |
| `BUNDLE_READ_ERROR` / `BUNDLE_PARSE_ERROR` | The file is missing or not JSON                  |
| `BUNDLE_INVALID`                           | Structural validation failed                     |
| `CHECKSUM_MISMATCH`                        | Contents do not match the embedded checksum      |
| `BUNDLE_UNSIGNED`                          | Unsigned, and `allowUnsigned` was not set        |
| `BUNDLE_SIGNATURE_MISSING`                 | A key is configured; the bundle has no signature |
| `BUNDLE_SIGNATURE_UNVERIFIED`              | Signed, but no key configured to check it        |
| `BUNDLE_SIGNATURE_INVALID`                 | The signature does not match                     |
| `BUNDLE_SIGNATURE_ALGORITHM_MISMATCH`      | Key algorithm ≠ signature algorithm              |
| `BUNDLE_SIGNATURE_ALGORITHM_REMOVED`       | Ed25519                                          |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/basic-concepts/bundles.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
