Skip to content

feat(enrichment): add opt-in Precleared compliance manifest - #125

Open
gabrielaoimevbore-beep wants to merge 1 commit into
debpalash:mainfrom
gabrielaoimevbore-beep:feat/precleared-compliance-manifest
Open

gabrielaoimevbore-beep wants to merge 1 commit into
debpalash:mainfrom
gabrielaoimevbore-beep:feat/precleared-compliance-manifest

Conversation

@gabrielaoimevbore-beep

@gabrielaoimevbore-beep gabrielaoimevbore-beep commented Oct 29, 2026 •

Copy link
Copy Markdown

First PR from #117: the Precleared manifest as opt-in enrichment for the current Go backend. The send gate will follow as a separate proposal against internal/outreachsend and internal/destdeliver, as you suggested.

Disclosure: I build Precleared. The code was written with Claude (Anthropic's AI) at my direction. Anthropic's terms assign me any rights it has in that output, and I know of no licence or other restriction on it.

How this meets the checklist in #117

Manifest and registration

  • apps/backend-go/internal/manifest/builtin/manifests/compliance/precleared.yaml, loaded with the other built-ins (precleared_compliance, capability compliance).
  • Listed in wbregistry so it appears in the column provider picker (/api/workbooks/meta/providers). The recorded meta/providers* fixtures are updated.
  • A precleared card in the settings integrations registry, so a workspace can save its own key and context through the existing PUT /api/settings/workspace-integrations/{id} route. The recorded integrations/* list fixtures are updated.
  • No Python paths are touched. TestEmbeddedManifestsMatchLegacy gets a small goOnly allowlist for this Go-only manifest, so the local legacy parity check still passes.

Explicit BYOK key and workspace opt-in

  • Bearer auth from PRECLEARED_API_KEY. With no key, manifest.Prepare refuses with precleared_compliance: missing PRECLEARED_API_KEY and nothing is sent, so the keyless free tier is never used. This is covered by a test.
  • Credentials still resolve through secrets.Resolver. A new secrets.WorkspaceOwned set makes the four PRECLEARED_* keys resolve like Resolver.Credential: only the workspace's own value counts unless ALLOW_GLOBAL_CREDENTIAL_FALLBACK=true. That is the same policy CRM and outreach credentials follow, and it means no operator-wide key enrols a workspace.
  • It is in no default waterfall, so it runs only in a column that names it.

Decision inputs, nothing invented

Body field Comes from
recipient_email lead email
company_name lead company
sender_country workspace PRECLEARED_SENDER_COUNTRY
address_source workspace PRECLEARED_ADDRESS_SOURCE
pitch workspace PRECLEARED_PITCH
  • An unset value is sent blank. Precleared reads a blank as not given; a blank address_source becomes unknown, the cautious value.
  • No recipient country, consent or existing-customer fact is ever supplied.

What it returns

  • compliance holds the verdict (ok_to_email, consent_needed, blocked or unknown).
  • It also returns compliance_confidence, compliance_rule, compliance_rule_source, compliance_reason, compliance_note and compliance_next_step.
  • compliance_footer says what the footer must include, and compliance_recipient_country names the country whose rules applied.
  • Receipt metadata: compliance_receipt_id, compliance_receipt_url (public verify endpoint), compliance_checked_at and compliance_rules_version.
  • compliance_disclaimer carries the decision-support notice. unknown is a successful but uncertain answer, documented as "do not send yet".

Bounded calls, cost and fixtures

  • 25 second manifest timeout. The engine's 1 MiB response cap applies, and http_4xx/http_5xx bodies are never parsed, so payment links in a 402 never reach a lead.

  • cost_per_lookup: 0.03 is the declared worst case for spend estimates. It stays BYOK: Precleared bills the workspace's key, and a 400, 401, 402 or 429 runs no check.

  • Fixtures in internal/manifest/builtin/testdata/precleared/ are replayed in process. They cover:

    • success
    • consent needed with blank context
    • uncertain (unknown)
    • invalid input (no email)
    • 401, 402 and 429
    • 500
    • a malformed response
    • network failure and timeout

    No network or paid request is made in CI. They use example.co.uk-style data only.

Security-sensitive parts (per CONTRIBUTING)

  • Secrets. secrets.Resolver.Get gains the WorkspaceOwned check, placed beside GlobalOnly. It affects only the four PRECLEARED_* keys.
  • Outbound fetch. POST https://precleared.dev/api/v1/check?src=opengtm sends the lead's email and company name and the workspace's declared context. The URL is fixed, not templated, and goes through the normal egress guard. Credential values are scrubbed from errors as for every manifest.
  • Cache. providerrun no longer reads or writes the shared enrichment cache for declarative providers of the compliance capability. That cache is keyed per company or record, so it could otherwise serve one recipient's verdict to another at the same company, or a stale one. Other providers keep the cache, and a test covers both sides.

Notes

  • docs/providers/precleared.md covers setup (workspace and single-tenant), inputs, outputs, cost, errors and behaviour. .env.example gets two commented lines.
  • The non-production workbook path that runs without a provider runner (workbook.Deps.Runner == nil) does not carry auth.extra_env (runplan.manifestDoc, credentials.ManifestAllow). There the context would go out blank, which Precleared treats cautiously. The worker and API paths use providerrun, which reads it. I left that as is to keep this PR focused.
  • I put the card in the integrations registry because it is the existing workspace-scoped secret surface. Happy to move it if you would rather keep that list to output destinations.
  • Verification: I checked the fixtures against the manifest engine's rendering and projection logic. The Go toolchain wasn't available where I wrote this, so the full make test run is left to CI here.

PR template

  • Type: new enrichment provider / source.
  • Commits are signed off (DCO).
  • CLA: I'll sign it from the CLA check on this PR.
  • Tests and build: left to CI, as above.
  • No scraping of paywalled, pirated or login-gated content.
  • The credentialed provider is opt-in and inert by default.

Refs #117

🤖 Generated with Claude Code

https://claude.ai/code/session_01FQdvgNwwnTYBHUGD2dPwFh

Add a declarative provider manifest, compliance/precleared.yaml, that asks
Precleared whether a cold or marketing email may be sent to one recipient.
It returns the verdict, its confidence, the deciding rule and its source, the
next step and signed receipt metadata (receipt id and verify URL, check time
and rules version). The result is decision support, not a legal guarantee.

Opt-in and inert by default:
- Bearer auth from PRECLEARED_API_KEY. Without a key, Prepare refuses and no
  request is made, so Precleared's keyless free tier is never used.
- secrets.WorkspaceOwned makes the PRECLEARED_* settings resolve like
  Credential: the workspace's own value only, unless
  ALLOW_GLOBAL_CREDENTIAL_FALLBACK. Workspaces save them through the existing
  workspace-integrations route (new "precleared" integration card).
- Not in any default waterfall: it runs only in a column that names it.

Decision inputs are the lead's email and company plus the workspace's
declared sender country, address provenance and pitch. Unset values are sent
blank, which Precleared reads as not given; no consent or country fact is
inferred. providerrun never serves or stores compliance verdicts in the
shared enrichment cache, which is keyed per company or record.

cost_per_lookup is the worst case for spend estimates. The 25 second timeout
and the engine's 1 MiB response cap bound each call; error bodies are not
parsed. Fixtures cover success, consent needed with blank context, uncertain,
invalid input, auth, quota, rate limit, server error, malformed response,
network failure and timeout, with no network or paid requests in CI.

Refs debpalash#117

Signed-off-by: Gabe AO <support@precleared.dev>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQdvgNwwnTYBHUGD2dPwFh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant