Skip to main content

cofferdam

Block outbound side effects — email, API calls, payments, webhooks — from non-production environments.

A cofferdam is a temporary watertight enclosure pumped dry so work can happen on a submerged foundation. This library is the software equivalent: it holds back outbound side effects from development, staging, and test environments so they cannot reach real customers, payment processors, or live vendor APIs.

The problem

When a non-production environment is refreshed from a production database backup, the restored data includes live email accounts, API credentials, payment configuration, webhook endpoints, and scheduled job settings. Without a guard, the non-production environment behaves like production — emailing customers, charging cards, and calling live APIs against real data.

How it works

cofferdam reads a local TOML policy file that lives on the environment's filesystem — never in the database, never in a backup. That file is the execution authority for every outbound action.

Restored data owns business intent. Local environment config owns execution authority.

The engine answers one question per outbound action:

Is this specific side effect permitted here?

It fails closed: absent, incomplete, or invalid policy denies the action.

Installation

$ pip install cofferdam            # core library + CLI
$ pip install "cofferdam[http]"    # adds the policy-checked HTTP helper (httpx)

Requires Python 3.10+.

Quick start

from cofferdam import load_policy

policy = load_policy("environment_policy.toml")

# Raise PolicyDeniedError if this side effect is not explicitly permitted here.
policy.assert_allowed(
    integration="windmill",
    kind="vendor_api",
    operation="write",
    method="POST",
    host="sandbox.windmill.dev",
    credential="windmill_default",
)

# Or inspect the decision without raising.
decision = policy.decide(integration="stripe", kind="payment", operation="capture")
if not decision.allowed:
    print(decision.reason_code)   # e.g. "operation_not_allowed"

Policy-checked HTTP — host validated against the policy before any bytes leave (optional cofferdam[http] extra):

from cofferdam.http import request

response = request(
    policy=policy,
    integration="windmill",
    operation="write",
    method="POST",
    url="https://sandbox.windmill.dev/api/jobs/run",
    credential="windmill_default",
    json=payload,
)

Email decoration

When an email is sent from a non-production environment, cofferdam decorates the subject and body so recipients immediately know the source:

  • Subject: STAGING - Weekly Sales Report
  • Body: a warning banner (HTML) or a notice line (plain text) prepended to the original content

Decoration is on by default for any non-production environment and can be disabled with decorate = false under [mail].

Example policy

environment = "staging"
default_decision = "deny"

[mail]
mode = "sink"
sink = "dev-inbox@example.internal"   # all outbound mail redirected here

[integrations.windmill]
enabled = true
kind = "vendor_api"
credential = "windmill_default"
allowed_hosts = ["sandbox.windmill.dev"]
allowed_methods = ["GET", "POST"]
allowed_operations = ["read", "write"]

[credentials.windmill_default]
profile = "staging"
secret_env = "WINDMILL_API_KEY"

[integrations.stripe]
enabled = true
kind = "payment"
credential = "stripe_default"
allowed_hosts = ["api.stripe.com"]
allow_authorize = true
allow_capture = false            # never capture money outside Production

[credentials.stripe_default]
profile = "sandbox"
secret_env = "STRIPE_SANDBOX_SECRET_KEY"

[effects.email.customer]
enabled = false                  # no customer email in Staging

[effects.email.internal]
enabled = true
allow_domains = ["example.internal"]

CLI

$ cofferdam validate  path/to/environment_policy.toml
$ cofferdam inspect   path/to/environment_policy.toml
$ cofferdam decide    path/to/environment_policy.toml \
      --integration windmill --kind vendor_api --operation write \
      --method POST --url https://sandbox.windmill.dev/api/jobs/run \
      --credential windmill_default

validate exits non-zero on invalid policy. inspect prints a redacted summary — secrets are never shown. decide prints allow/deny and a stable reason code.

Local development

A real policy file can reference many credentials — one secret_env per integration. Each of those environment variables has to be present in every process that resolves it: the web server, every worker, and whatever shell a developer or an AI pair-programming agent is using while writing integration code. Prefixing every command with a wall of SECRET_X=... SECRET_Y=... doesn't scale, and cofferdam deliberately doesn't solve this for you — resolve_secret only ever reads os.environ, never an adjacent .env file or a path named in the policy (ADR-0013). That keeps "missing env var" a real, fail-closed condition (ADR-0005) and keeps the policy file itself free of secret-custody concerns (ADR-0007).

Instead, provision local secrets at the shell level with direnv, so every tool started from your project directory — an interactive shell, bench start, pytest, cofferdam decide, or an AI agent's own shell — inherits the same variables automatically.

Install:

$ brew install direnv        # macOS
$ sudo apt install direnv    # Debian/Ubuntu

Then hook it into your shell (add to ~/.bashrc or ~/.zshrc):

eval "$(direnv hook bash)"   # or: eval "$(direnv hook zsh)"

Open a new shell (or source the rc file) after adding the hook.

Use: create a .envrc file next to environment_policy.toml (or at the project root) exporting one variable per secret_env your policy references:

# .envrc — never commit this file
export WINDMILL_API_KEY="sk_test_..."
export STRIPE_SANDBOX_SECRET_KEY="sk_test_..."

Then authorize it once per directory:

$ direnv allow

From then on, cd-ing into that directory loads those variables into the shell, and leaving it unloads them again — no manual exporting, no per-tool setup. Add .envrc to .gitignore immediately; it holds real secret values.

direnv is a local-development convenience only — it affects shells it hooks into, not processes started by a process manager. Staging/production workers and the web server should get their env vars from the process manager instead (systemd EnvironmentFile=, supervisor environment=) or a real secrets manager, not from .envrc.

Frappe / ERPNext

cofferdam was designed with ERPNext and Frappe Framework in mind — the database restore pattern is a daily reality for that ecosystem. The core library has no dependency on Frappe and works with any Python application.

For full Frappe/ERPNext integration — automatic policy discovery by bench site, interception of frappe.sendmail, and webhook delivery gating — see cofferdam-app, a companion Frappe app that installs into a bench alongside your ERPNext site.

Requirements specification

This library is built with a document-driven workflow. See docs/ for the full requirements specification and docs/adr/ for architecture decisions.

License

Apache-2.0 © 2026 Brian Pond / Datahenge LLC

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cofferdam-0.1.1.tar.gz (53.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cofferdam-0.1.1-py3-none-any.whl (25.4 kB view details)

Uploaded Python 3

File details

Details for the file cofferdam-0.1.1.tar.gz.

File metadata

  • Download URL: cofferdam-0.1.1.tar.gz
  • Upload date:
  • Size: 53.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for cofferdam-0.1.1.tar.gz
Algorithm Hash digest
SHA256 81d66e75efd7de3cab539abbf23d11d5c24d8e720fef1452754dd6a5ce0bea8e
MD5 4587eff59134615f44331b69bd664110
BLAKE2b-256 4256dc54454793d9ba8cb07698e1cf7968b743f3cf5fb978b96069a5fdaed20f

See more details on using hashes here.

File details

Details for the file cofferdam-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: cofferdam-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 25.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for cofferdam-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 92ebcba746a93f1b358d5f6869a8888831c79fc64f1781f7b49c5163de8b073d
MD5 727922f7b6a0614ac7c5fef33bc3b70a
BLAKE2b-256 de06fc57936bdfb1ea31c10a9a705c0373a4672e08664f108fa33a318e6e94c5

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.1

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

0.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page