-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathconfig.example.yaml
More file actions
488 lines (461 loc) · 28 KB
/
Copy pathconfig.example.yaml
File metadata and controls
488 lines (461 loc) · 28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# Copy to config.yaml and edit for your deployment.
# Secrets should be set via env vars (E2A_*) — see README "Configuration".
# Set to "production" to enforce TLS, HTTPS webhooks, HMAC secret strength
# checks, and real DNS domain verification (development short-circuits
# domain verification to "found" with no lookup, logging a WARNING per
# check). The E2A_ENV environment variable overrides this value — the only
# way to reach production mode on the published Docker image, which bakes
# this file to a fixed path. Any value other than "development" or
# "production" fails startup.
env: "development"
# Optional. Names WHICH deployment this is ("prod" or "staging"), for
# classification metadata e2a attaches to resources it creates in external
# systems — today the e2a-env tag on provisioned SES sender identities, so a
# shared AWS account can be audited without this server's database. NOT the
# same as `env` above: that is a development/production mode switch, and a
# hosted staging deployment runs env: "production" too. Self-hosters leave
# this empty and simply get no environment tag. Any other value is logged
# once at startup and treated as empty — never a startup failure.
# Override with E2A_DEPLOYMENT_NAME.
deployment_name: ""
# Optional. When set, users can register agents by slug (no DNS setup required)
# and the server provisions <slug>@<shared_domain>. Leave empty on a self-host
# that doesn't operate a shared mail domain — every agent then needs a custom
# domain that the user verifies via DNS. The shared domain itself becomes
# reserved (cannot be claimed as a custom domain).
shared_domain: ""
smtp:
listen_addr: ":2525"
domain: "example.com" # the domain whose MX record points at this relay
# tls_cert: "/path/to/cert.pem"
# tls_key: "/path/to/key.pem"
# CIDRs whose peers may present a PROXY protocol header (e.g. HAProxy in
# front of this listener). Empty = PROXY parsing off. Untrusted peers are
# never parsed — their real IP is always used for SPF.
# Caveat: a trusted peer that connects WITHOUT a header is given up to 5s
# to present one before the SMTP banner is written — so health checks
# against a PROXY-enabled listener must be TCP-connect-only (or tolerate
# the ~5s banner delay), and direct SMTP clients connecting from a trusted
# CIDR see a ~5s greeting delay. Proxied traffic (always has a header) is
# unaffected.
# List only the proxy's own address(es) — ideally a /32. A catch-all like
# "0.0.0.0/0" or "::/0" would let any peer spoof source IPs and is
# rejected at startup.
# proxy_trusted_cidrs: []
http:
listen_addr: ":8080"
public_url: "http://localhost:8080" # externally visible base URL (e.g. https://e2a.example.com); required for HITL magic-link emails
database:
url: "postgres://e2a:e2a@localhost:5433/e2a?sslmode=disable"
oauth:
google_client_id: ""
google_client_secret: ""
redirect_url: "http://localhost:3000/api/auth/callback"
# auth.md agent-identity token signing (Slice 5b). PEM-encoded RSA private
# key (PKCS#1 or PKCS#8); the public half is published at
# /.well-known/jwks.json so agents can verify e2a-minted JWTs. Empty leaves
# the agent-auth surface disabled (JWKS serves {"keys":[]}). Override via
# E2A_OAUTH_SIGNING_KEY. Never generated or persisted by e2a.
signing_key: ""
# kid advertised in the JWKS and stamped on every issued JWT (default "v1").
# Rotation: advertise a new kid, then retire the old after the longest token
# TTL. Override via E2A_OAUTH_SIGNING_KID.
signing_kid: "v1"
# — OpenID Connect login ————————————————————————————————————————————————————
# Optional, off by default. Lets any standards-compliant OIDC provider (such
# as TokenCanopy, WorkOS, or Auth0) sign an existing e2a user in through the
# Authorization Code flow with PKCE, state, and nonce. The provider's ID token
# must contain user_id_claim with an unchanged local users.id. e2a never
# provisions or email-matches a user from OIDC claims.
#
# The provider must register redirect_url for this confidential web client.
# When enabled, startup performs discovery at issuer_url and fails closed if
# discovery is unavailable or invalid. Legacy Google login remains available.
oidc:
enabled: false
issuer_url: "" # exact issuer/discovery base. Override: E2A_OIDC_ISSUER_URL
client_id: "" # registered OIDC client ID. Override: E2A_OIDC_CLIENT_ID
client_secret: "" # keep secret; used only at token endpoint. Override: E2A_OIDC_CLIENT_SECRET
redirect_url: "" # e.g. "https://e2a.example.com/api/auth/oidc/callback". Override: E2A_OIDC_REDIRECT_URL
user_id_claim: "" # TokenCanopy uses "e2a_user_id". Override: E2A_OIDC_USER_ID_CLAIM
# Optional fixed upstream logout handoff. It must be an absolute http(s)
# URL with no query or fragment; do not derive it from browser input.
# Override: E2A_OIDC_LOGOUT_URL
logout_url: ""
# Enable with: E2A_OIDC_ENABLED=true
# — Delegated access tokens ——————————————————————————————————————————————————
# Optional, off by default. Lets an external control plane (any OIDC issuer
# — the operator's own platform, for example) call the e2a API with
# short-lived, audience-bound RFC 9068 access tokens (JOSE typ "at+jwt")
# minted server-side for its signed-in humans. e2a verifies the token
# against the issuer's published JWKS and maps the verified (issuer,
# subject) pair to an existing local user via external_principal_mappings
# — populated by provisioning/attach, never on token presentation. A
# verified token acts with account scope, exactly like an account API key.
#
# Enabled requires every field below; a malformed static configuration
# refuses to start, and in production the issuer must be https. Issuer
# NETWORK unavailability is deliberately not startup-fatal: discovery
# retries in the background and delegated requests fail 503 until it
# succeeds. Existing credentials (API keys, OAuth, agent JWTs, sessions)
# are unaffected by any delegated outage. Compact tokens whose protected
# header says typ "at+jwt" belong to this verifier even while it is
# disabled — they never fall through to the API-key path.
delegated:
enabled: false # Override: E2A_DELEGATED_ENABLED
issuer_url: "" # exact issuer/discovery base, byte-compared against `iss`, e.g. "https://auth.platform.example.com/oidc"
audience: "" # exact single-string `aud`, e.g. "https://api.e2a.example.com"
authorized_party: "" # exact `azp` the issuer stamps on console-minted tokens
required_scope: "" # exact singleton scope, e.g. "account" — the whole claim, not a member
allowed_algorithms: ["RS256", "ES256"] # closed allowlist; only these two are supported
max_token_lifetime_seconds: 120 # bound on exp - iat
clock_skew_seconds: 5 # tolerated iat-future / exp-past drift
# Context claims every token must carry as bounded nonempty strings;
# allowed_values (optional) closes a claim to a value set. These claim
# NAMES and role VALUES are your issuer's identity policy — not shipped
# with any default — so set them to your deployment's own claims. The
# generic placeholders below are illustrative only; enabling the
# verifier with an empty required_claims or forbidden_claims list is a
# startup error.
required_claims:
- name: tenant_id
- name: seat_id
- name: seat_role
allowed_values: [admin, member]
# Claims that must be absent entirely (present-as-null still rejects):
# your issuer's machine-credential claim names, so a non-human token can
# never pass the human-token policy.
forbidden_claims: [service_client_id, robot_id]
# — Internal user provisioning ———————————————————————————————————————————————
# Optional, off by default, internal-only (not part of the public /v1 API or
# the OpenAPI spec). When enabled, an external control plane can create e2a
# users idempotently via POST /api/internal/users/provision — keyed by its
# own external_ref — ahead of each user's first sign-in. Requests are
# authenticated by a shared HMAC over the request body
# (X-E2A-Internal-Signature), signed with the provisioning secret. The
# secret is env-only (E2A_PROVISIONING_SECRET), is deliberately separate
# from limits.internal_api_secret so each can be rotated independently, and
# must be set to the same value on both ends. Leave disabled for self-host —
# the endpoint 503s.
provisioning:
enabled: false
# Enable with: E2A_PROVISIONING_ENABLED=true
# Secret (env only, generate with `openssl rand -hex 32`): E2A_PROVISIONING_SECRET
signing:
# Generate with: openssl rand -hex 32
# This placeholder is >=32 bytes so `make run` boots for local dev
# without edits. Production refuses to start with this exact placeholder
# value or with anything < 32 bytes — see placeholderHMACSecret in
# internal/config/config.go (keep the two in sync if you ever change this).
hmac_secret: "change-me-in-production-this-is-not-a-real-secret"
outbound_smtp:
# Upstream SMTP relay for outbound mail (HITL approval notifications,
# /api/v1/agents/{email}/test sends, etc.). The defaults below are
# empty (host: "") so the binary will refuse to send rather than
# silently route through a wrong host — set them to one of:
#
# - Local dev: `make docker-up` runs Mailpit on localhost:1025.
# Set host: "localhost", port: 1025, from_domain: "e2a.localhost".
# Captured mail appears at http://localhost:8025. No creds needed.
#
# - Production (e.g. AWS SES): set host: "email-smtp.us-east-1.amazonaws.com",
# port: 587, username/password from your IAM relay credentials,
# from_domain to a domain DKIM-signed by your upstream.
host: ""
port: 587
username: ""
password: ""
from_domain: "relay.example.com"
# Domain the provider stamps on outgoing Message-ID headers (SES:
# "<region>.amazonses.com"). Used to qualify the bare id SES returns in the
# SMTP 250 response so replies thread correctly. Leave empty to derive it
# from a standard SES host (email-smtp.<region>.amazonaws.com); set it
# explicitly only for non-standard endpoints.
# message_id_domain: "us-east-2.amazonses.com"
# Platform notification emails: HITL approval requests, plus webhook health
# alerts (an early warning when a webhook's deliveries start failing, and a
# notice when e2a auto-disables one). Both senders read the settings below.
notifications:
# Sender address for those emails. Optional — when empty, each kind sends
# from its own fixed local part on outbound_smtp.from_domain (approvals@
# and webhooks@, kept distinct so a time-boxed approval stays filterable
# apart from routine webhook mail), so self-hosted deployments need no
# configuration here. SETTING THIS COLLAPSES BOTH INTO ONE IDENTITY.
# If set, it must be an address your upstream is authorized to send as.
# If e2a holds a DKIM key for the address's domain (a domain registered
# with this server), each notification is DKIM-signed in-process for that
# domain; with no stored key it is sent unsigned.
# from_address: "support@send.your-company.example"
#
# Optional Reply-To for those emails. Set it when the sending domain is
# relay-only (no mailbox behind from_address) so replies land in a real
# support inbox — the same From-on-the-relay-domain + Reply-To pattern
# shared-domain agent sends use. Leave unset when from_address is itself
# a real mailbox — replies then follow the sender address either way
# (webhook mail emits no Reply-To; approval mail emits one pointing at
# its own sender, preserving its long-standing behaviour).
# reply_to: "support@your-company.example"
# Outbound delivery is always asynchronous and at-least-once. The send API
# durably persists the message and enqueues a River job in one transaction,
# returning status=accepted. A background worker submits it to the relay and
# records the terminal outcome (email.sent / email.failed webhooks and
# GET /v1/messages/{id}). Postgres-backed River is therefore mandatory.
# Custom-domain sender identity (decision 4 / Slice 4). When ses_region is set,
# verifying a domain registers an AWS SES sending identity via BYODKIM (reusing
# the per-domain DKIM key e2a already generates), and once SES confirms it,
# outbound mail from agents on that domain uses the agent's OWN address as the
# From header (DKIM-aligned → DMARC passes; replies reach the agent directly).
# Leave empty (the default) to disable: every domain's sending_status stays
# "none" and outbound keeps the relay "… via e2a" From — the fail-closed
# default for dev/self-host without SES. Needs AWS credentials in the ambient
# environment (env vars / instance role) with ses:CreateEmailIdentity,
# ses:TagResource (CreateEmailIdentity supplies the required
# e2a-managed=sender-identity-v1 tag),
# ses:PutEmailIdentityDkimSigningAttributes,
# ses:PutEmailIdentityMailFromAttributes, ses:GetEmailIdentity,
# ses:DeleteEmailIdentity, ses:ListEmailIdentities. (Provisioning calls
# both Put operations when refreshing an existing identity — a role missing
# either permission fails closed and River retries.) For least privilege,
# require aws:RequestTag/e2a-managed=sender-identity-v1 on Create/Tag and
# aws:ResourceTag/e2a-managed=sender-identity-v1 on Delete/Put. Before upgrading
# an existing SES-enabled install, follow the audited legacy-tag adoption steps
# in docs/design/sender-identity-mailfrom.md; untagged identities are deliberately
# treated as foreign and are never updated or deleted.
# Override with E2A_SENDER_IDENTITY_SES_REGION.
sender_identity:
ses_region: "" # e.g. "us-east-1"
# Two-phase blue/green rollout switch for the versioned sender-identity job
# lanes. Phase 1: deploy the new release with this true — it produces the
# LEGACY job kinds (consumable by the previous release, so rollback strands
# no teardown work) while consuming both lanes. This is job-lane
# compatibility only: before a pre-ownership binary overlaps, follow the
# design's tag-conditioned IAM setup and sender-identity mutation freeze.
# Phase 2: once this release is the stable rollback target, flip to false
# (config-only deploy) to switch producers to the versioned v2 lane.
# Single-instance deployments have no overlap and can leave this false.
legacy_job_compat: false
# How long a FIXTURE sending identity — one provisioned for a non-customer
# account class (internal dogfooding, synthetic monitoring) — is assumed to
# still be in use. Stamped onto the identity as the e2a-expires tag so an
# abandoned test fixture is distinguishable from a live customer domain in
# the provider account alone. Customer identities never carry it. Written as
# a Go duration string; "0s" disables the tag. Override with
# E2A_SENDER_IDENTITY_FIXTURE_TTL.
fixture_ttl: "24h"
# Orphan reclaim: let the hourly reaper DELETE sending identities that exist
# at the provider with no ledger row and no domain row (crashed test runs
# leak these). Off by default, and arming it is not enough on its own — an
# identity is deleted only when ALL of these hold: it carries e2a's ownership
# tag, its e2a-env tag names THIS deployment (deployment_name must be set),
# its e2a-purpose tag says "fixture", its e2a-expires stamp is in the past,
# its e2a-created stamp clears reclaim_min_age, its name falls under a
# reclaim_zones entry, and the provider reports it as NOT verified for
# sending. Anything missing or unparseable refuses. Deletion runs through the
# same ownership-verifying teardown path the ledgered phase uses.
#
# With reap_orphans false the reaper still runs the whole decision and logs
# "WOULD DELETE <domain>" (or the refusal reason) without touching the
# provider — run there for a few days and read the logs before arming.
reap_orphans: false
# DNS zones that hold ONLY e2a's own test fixtures. This is the strongest
# guard: customer domains are never under the test zone. Matching is on a
# label boundary, so "example.test" covers "a.example.test" and
# "example.test" itself but never "evilexample.test". EMPTY (the default)
# reclaims NOTHING — it is never read as "any zone".
reclaim_zones: []
# Absolute age floor before an identity may be reclaimed, checked
# independently of its expiry tag so clock skew or a mis-set fixture_ttl
# cannot make a brand-new identity instantly reclaimable. Go duration string;
# "0s" reclaims nothing.
reclaim_min_age: "168h"
# Cap on deletions per reaper JOB (the orphan phase is paginated across
# jobs, so this is a per-page budget, not a global one). A systematic
# mistake then costs a handful of identities and a loud log, not the
# account. 0 reclaims nothing.
reclaim_max_per_sweep: 5
# Operator-managed recipient-volume ramp for newly verified custom sender
# domains. Disabled by default for self-hosts. When enabled, the schedule is
# snapshotted on a domain's first eligible external send. A UTC day advances
# only after provider-accepted recipient volume reaches 50% of that day's cap;
# the higher limit starts the following UTC day. Existing verified domains are
# persistently exempt; operator reset procedures are in docs/runbooks/sending-ramp.md.
# This policy is visible read-only in the domain API and cannot be disabled by
# end users. start_daily must be at least 50, the API's per-message recipient
# maximum, so one accepted message can never be stranded permanently. Hosted
# database generation zero explicitly uses 150; this example keeps the
# established self-host-compatible default of 50, so the hashes intentionally
# differ even though both policies are disabled.
sending_ramp:
enabled: false
start_daily: 50
target_daily: 2000
ramp_days: 30
# Sending protection (abuse prevention). Every control ships DISABLED; this
# block existing changes nothing until a control is deliberately enabled. The
# ramp schedule above stays the custom-domain schedule SSOT and is not
# duplicated here. runtime_policy_source selects where the server reads its
# policy: "config" (this file — the default, right for self-host) or
# "database" (hosted deployments, where activation is an audited CAS via
# `e2a -activate-sending-protection-policy` instead of a redeploy). The two
# secrets (E2A_SENDING_FEEDBACK_HMAC_KEYS, E2A_SENDING_PROTECTION_OPERATOR_EMAILS)
# are required only for database source or when any control below is enabled.
sending_protection:
runtime_policy_source: config
budget_mode: disabled # disabled | shadow | enforce
detector_mode: disabled # disabled | shadow | enforce
tenant_header_mode: disabled # disabled | canary | enforce
tenant_provisioning_mode: disabled # disabled | enforce
tenant_suppression_sync_mode: disabled # disabled | enforce
tenant_header_canary_account_ids: []
default_account_daily_recipients: 100
shared_domain_account_daily_recipients: 50
probation_global_daily_recipients: 150
all_customer_global_daily_recipients: 5000
critical_operational_daily_recipients: 100
violation_operational_daily_recipients: 100
daily_unlimited_plan_codes: ["starter", "pro", "scale"]
budget_hold_max_days: 7
bounce_pause_basis_points: 400 # pause strictly above 4% hard bounce
complaint_pause_basis_points: 8 # pause strictly above 0.08% complaints
bounce_min_outcomes: 50 # minimum terminal outcomes before the all-path rate can pause
shared_reputation_bounce_min_outcomes: 1
detector_interval_seconds: 300
detector_window_days: 7
sending_control_audit_retention_days: 90
sending_feedback_post_account_retention_days: 30
operator_notice_recipient_version: 1
# Outbound delivery feedback (decision 9 / Slice 4b). When ses_configuration_set
# is set, outbound mail is tagged with X-SES-CONFIGURATION-SET so SES publishes
# delivery/bounce/complaint events to an SNS topic; e2a's public endpoint
# POST /webhooks/ses consumes them (SNS signature verified,
# fail-closed) to drive messages.delivery_status, fire
# email.delivered/bounced/complained, and auto-suppress hard-bounced/complained
# recipients. sns_topic_arns is the allow-list of SNS topics that endpoint
# accepts — EMPTY = reject everything (so the endpoint is inert until ops wires
# the topic). Both empty (the default) disables delivery feedback entirely.
# Override with E2A_DELIVERY_SES_CONFIGURATION_SET and E2A_DELIVERY_SNS_TOPIC_ARNS
# (comma-separated). Setup: create an SES configuration set with an SNS event
# destination (delivery+bounce+complaint), then subscribe e2a's endpoint to that
# topic and list its ARN here.
delivery_feedback:
ses_configuration_set: "" # e.g. "e2a-delivery"
sns_topic_arns: [] # e.g. ["arn:aws:sns:us-east-2:123456789012:e2a-ses-events"]
# Per-user resource caps applied when a user has no row in the
# account_limits table. Hosted-service deployments overwrite these per
# user via an external provisioner (the e2a-billing sidecar, an admin
# tool, manual SQL). Self-host operators who do not run any provisioner
# rely solely on these defaults — values below are intentionally high so
# the limits subsystem is effectively off until you opt in.
#
# To convert a self-host into a paywall-aware install, set these to your
# "free" shape and write the upgraded caps into account_limits from
# whatever billing system you run.
limits:
plan_code: "default"
max_agents: 1000000
max_domains: 1000000
# Message-flow allowances count OUTBOUND recipient-deliveries: a message
# sent to N distinct recipients consumes N units; received mail is free
# and unmetered (it never consumes the allowance).
max_messages_month: 1000000000
# Optional per-UTC-day send cap for users without an account_limits row.
# ABSENT (the default) = no daily policy; 0 = block all sends. Hosted
# deployments set this to the free tier's daily allowance and let the
# billing provisioner override it per account.
# max_messages_day: 100
max_storage_bytes: 1125899906842624 # 1 PiB — effectively unbounded
cache_ttl_seconds: 60
# Shared HMAC secret used to authenticate /api/internal/limits/invalidate.
# Set this on both the e2a server and the external limits provisioner
# (e.g. a hosted-service billing sidecar). Leave empty for self-host
# without a provisioner — the endpoint will 503. Generate with:
# openssl rand -hex 32. Override with E2A_INTERNAL_API_SECRET.
internal_api_secret: ""
# URL the OSS server POSTs (HMAC-signed) when a user deletes their
# account, so an external billing service can cancel their
# subscription before the cascade drops the user row. Empty
# disables the call (self-host without billing). Example for a
# docker compose deployment alongside the hosted billing sidecar:
# billing_hook_url: "http://billing:9000/api/internal/billing/cancel"
billing_hook_url: ""
# Per-user budget shared by authenticated message, conversation, and webhook
# reads across every agent client and dashboard session on the account. The
# default allows 240 requests per minute; tune this for the combined workload,
# not for one polling client. Rate limits are in-memory and apply per process.
rate_limits:
poll_per_minute: 240
# — Trash (soft delete) ———————————————————————————————————————————————————————
# How many days a soft-deleted resource (agent inbox or message) stays
# restorable in the trash before it is purged permanently. The public API
# documents "30 days by default (deployment-configurable)" — this is the
# knob. Minimum 1; live message data is otherwise retained indefinitely.
# Override with
# E2A_TRASH_RETENTION_DAYS.
trash:
retention_days: 30
# — Metrics (Prometheus) ——————————————————————————————————————————————————————
# Off by default. When enabled, the server swaps its telemetry backend from
# structured log lines to Prometheus and binds a SEPARATE listener serving
# GET /metrics — deliberately never mounted on the public API handler. The
# default bind is loopback-only; point your Prometheus scraper (or the
# e2a-prober host) at it over a private network of your choosing. Metric
# catalog, aggregation guidance, and SLO targets: docs/observability.md.
# Override with E2A_METRICS_ENABLED / E2A_METRICS_LISTEN_ADDR /
# E2A_METRICS_BUILD.
metrics:
enabled: false
listen_addr: "127.0.0.1:9091"
build: "unknown" # release/image tag; attached to every sample
# — Outbound footer ———————————————————————————————————————————————————————————
# Optional footer appended to all SMTP-egress outbound mail at composition
# time (inside the DKIM-signed body, above the managed-unsubscribe line, and
# counted against the composed-size ceiling). Fully inert when enabled: false
# (the default). Not applied to self-send loopback delivery or to non-standard
# account classes (internal/system/demo). Useful for hosted branding or
# self-host AI-disclosure notices.
#
# Per-account gating reads account_limits.outbound_footer_enabled: an account
# WITH a row gets the footer only when that column is true (written by
# whatever provisions the row); default_enabled covers accounts with NO row.
# text is appended to the plain-text part after a blank line + the RFC 3676
# signature separator ("-- " with a trailing space, so clients trim it on reply);
# html is an operator-trusted fragment appended verbatim to the HTML part when
# the message has one. Both empty = no-op. Override the master switch with
# E2A_OUTBOUND_FOOTER_ENABLED.
# outbound_footer:
# enabled: false # master switch
# default_enabled: false # applies to accounts with NO account_limits row
# text: "" # plain-text footer
# html: "" # HTML fragment appended to the HTML part
# One-question onboarding survey ("Where did you hear about e2a?") shown
# once to each dashboard user before the rest of the app. Off by default;
# the answer is stored write-once on the user row (migration 120) and is
# only useful to operators who read their own database for analytics.
# Override with E2A_ONBOARDING_SURVEY_ENABLED.
# onboarding_survey:
# enabled: false
# — Content screening (piguard) ——————————————————————————————————————————————
# Master switch: set the environment variable E2A_CONTENT_SCAN_ENABLED=true
# to turn content screening on for this deployment (internal/identity/
# protection.go). Off by default, including here in the self-host example —
# this is what "self-host to use it today" (README → Content screening)
# means in practice. There is no YAML key for this; it is env-only.
#
# This is a deployment-level unlock, not a per-agent one: while it's unset
# or false, PUT /v1/agents/{email}/protection clamps every agent's
# scan_sensitivity to "off" no matter what a caller requests. With it
# enabled, still set each agent's scan_sensitivity to low/medium/high via
# that endpoint (or the dashboard) to actually turn scanning on for that
# agent — the two below only affect which detector runs once scanning is on.
#
# The Gemini LLM-as-detector layer is enabled by setting the environment
# variable GEMINI_API_KEY (or GOOGLE_API_KEY) to a Google AI Studio key.
# When the variable is absent the Gemini detector is silently skipped and
# the dependency-free heuristics detector runs alone. It only screens INBOUND
# mail (internal/relay); outbound agent-mail screening is heuristics-only.
#
# Obtain a key: https://aistudio.google.com/apikey
# Model default: gemini-3.1-flash-lite (change via GEMINI_EVAL_MODEL env var)
# Kill-switch: set E2A_GEMINI_DETECTOR_ENABLED=false to disable Gemini even when
# a key is configured (e.g. to isolate whether Gemini or heuristics drove a given
# block/review outcome, or to roll back without removing the credential).