Skip to content

hail CLI v0.9.0

Choose a tag to compare

@r13i r13i released this 08 Jul 10:29

Changelog

All notable changes to Hail are documented here. The format is based on Keep a Changelog, and Hail adheres to Semantic Versioning.

[0.9.0] β€” 2026-07-07

Breaking change. Component versions cut alongside this release:
sdk-v0.6.0 (PyPI: hail-sdk==0.6.0), cli-v0.9.0 (Homebrew + GitHub Releases).

  • POST /calls and POST /emails now require a recipient_consent: bool
    field (rejected with 422 if missing or false). This attests that the
    caller has obtained the lawful consent required to contact the recipient β€”
    Hail does not verify consent itself. Optional consent_source and
    consent_obtained_at fields record how/when consent was obtained; a
    required non-empty consent_source is enforced when the new optional
    message_type: "marketing" field is set (default remains
    "informational").
    • Every existing integration must add recipient_consent: true (or the
      real attestation) to its calls/emails, or requests will start failing
      with 422 the moment the API deploys this change.
      The CLI (--recipient-consent),
      SDK (recipient_consent= kwarg), and MCP tools (recipient_consent
      param) all already require and forward it as of this release β€” pin to
      hail-sdk>=0.6.0 and cli-v0.9.0+ before or at the same time as
      upgrading against a Hail API instance running this change.
  • Outbound voice calls now open with a fixed, non-configurable AI-disclosure
    line spoken before anything else; outbound email carries an equivalent
    disclosure appended at send time, outside the caller-authored body.
  • New global suppression list + one-click email unsubscribe
    (List-Unsubscribe/List-Unsubscribe-Post headers); a pre-send
    compliance gate blocks suppressed recipients, premium-rate destinations,
    and per-org rate limits before any call or email goes out.
  • New data-retention purge job and DSAR (lookup/export/delete) tooling for
    recipient data requests.
  • /legal/* pages (Terms of Use, AUP, Privacy Policy, DPA, sub-processors,
    Cookie Policy) published on hail-website.

[0.8.1] β€” 2026-07-02

Internal consolidation release. cli-v0.8.1 cut alongside; the SDK is
unchanged and stays at hail-sdk==0.5.0.

  • One shared cursor-pagination helper now backs every paginated API route
    (was seven hand-copies) and one generic page-walker backs all three CLI
    list commands. Wire contract unchanged, with one edge unified: an
    empty-string cursor query param is now ignored everywhere instead of
    returning 400 on some routes.
  • hail email events --all now emits the same walked-items stderr warning
    as call list --all and email list --all.

[0.8.0] β€” 2026-07-02

Component versions cut alongside this umbrella release:
sdk-v0.5.0 (PyPI: hail-sdk==0.5.0), cli-v0.8.0 (Homebrew + GitHub Releases).

  • GET /emails/{id}/events is now cursor-paginated (cursor, limit 1..1000,
    next_cursor in the response). hail email events gains
    --cursor/--limit/--all; SDK emails.events() and MCP get_email_events
    accept cursor/limit. Existing callers are unaffected (default limit 100).

[0.7.0] β€” 2026-07-02

Email deliverability milestone. Every outbound email now has a tracked
lifecycle β€” delivered, delayed, bounced, complained, rejected, opened,
clicked β€” visible per message, aggregated per account, and pushed to
customer webhooks.

Component versions cut alongside this umbrella release:
sdk-v0.4.0 (PyPI: hail-sdk==0.4.0), cli-v0.7.0 (Homebrew + GitHub Releases).

Deliverability tracking

  • New email_events table (append-only, migration 0020); outbound sends write a synthetic sent event, and SES configuration-set events (Delivery, Bounce, Complaint, Reject, DeliveryDelay, Open, Click) are ingested via SNS β†’ ingest Lambda β†’ POST /internal/ses-events. Duplicate SNS deliveries are absorbed by a dedup constraint; status transitions are guarded so terminal states never regress. emails.status gains delivered.
  • GET /emails/{id}/events β€” per-email lifecycle timeline. GET /emails/{id} gains last_event_at.
  • GET /emails/stats?from=&to=&bucket=hour|day β€” account-level counts, rates (delivery, hard-bounce, complaint, unique open/click), and a zero-filled time series.
  • Email events join the unified GET /events stream (source: "email", id=email:<uuid> filter).
  • Webhooks: email.delivered, email.delivery_delayed, email.opened, email.clicked are new; email.bounced and email.complained now actually fire.
  • CLI: hail email events <id>, hail email stats. SDK: client.emails.events(), client.emails.stats(). MCP: get_email_events, get_email_stats.
  • Infra: SES configuration set (HAIL_SES_CONFIGURATION_SET, Terraform default hail-events), SNS topic with explicit SES publish policy, subscription DLQ. Open/click tracking uses the default SES tracking domain in v1.

[0.6.0] β€” 2026-06-28

Custom sender domains milestone. Tenants can now send and receive on their own
domain, with DKIM and a custom MAIL FROM configured automatically.

Custom sender domains β€” send and receive on your own domain

  • Register a tenant-controlled domain: POST /email-domains with kind=custom (or hail email domain register --kind custom --domain acme.com). Hail calls SES CreateEmailIdentity and auto-configures a custom MAIL FROM on send.<domain> via PutEmailIdentityMailFromAttributes β€” no AWS-console step for the tenant.
  • The registration response returns the full DNS record set to publish: three DKIM _domainkey CNAMEs plus the MAIL FROM send.<domain> MX (feedback-smtp.<region>.amazonses.com) and SPF TXT (v=spf1 include:amazonses.com ~all). DnsRecord gains an optional priority for MX rows.
  • POST /email-domains/{id}/verify re-polls SES for both DKIM and MAIL FROM, surfacing mail_from_status alongside verification_status. On-demand only β€” clients/console re-poll; there is no background poller.
  • Receive inbound mail on verified custom domains. Inbound is matched by the owning identity, so a message to two of an org's domains yields one inbound row + webhook per matched domain, with the domain name on the event payload.
  • New IAM grant ses:PutEmailIdentityMailFromAttributes for the production identity (infra/terraform/iam.tf) β€” required for the custom MAIL FROM call.

Auth

  • The managed console now authenticates to the API with a short-lived Better Auth session JWT (carrying sub and the session activeOrganizationId) rather than a cached per-browser API key. The API resolves the request's organization from the active-org claim, validated against membership β€” closing a cross-tenant write path.

0.5.0 β€” 2026-06-12

Inbound email milestone. Hail can now receive mail at hail-mail addresses,
forward it, and fire webhooks on receipt and on bounce/complaint events.

Component versions cut alongside this umbrella release:
sdk-v0.3.0 (PyPI: hail-sdk==0.3.0), cli-v0.5.0 (Homebrew + GitHub Releases).

Inbound email

  • SES Receipt Rule β†’ S3 (raw MIME) + Lambda (HMAC-signed JSON) β†’ POST /internal/ses-events ingest pipeline.
  • Email.direction='inbound' rows with message_id, in_reply_to, references_ids, raw_s3_key, and SES SPF/DKIM/DMARC/spam/virus verdicts persisted.
  • email_attachments table β€” MIME attachments stored as separate S3 objects under attachments/<email_id>/<att_id>.
  • Threading via In-Reply-To / References chains.
  • Spam/virus FAIL verdicts persist the row but suppress forwarding and webhook fan-out.
  • Idempotency via a partial unique index on (organization_id, message_id) WHERE direction='inbound' β€” duplicate SES deliveries short-circuit.

Forwarding

  • Per-domain forward_to list on email_domains. Inbound matches trigger one outbound row per target through the existing send loop.
  • Header-rewrite forwarding (Cloudflare Email Routing-style): From: = forwarder+<org>@<base_domain> (SPF/DKIM aligned), Reply-To: = original sender, References: preserves threading.
  • Loop guards: 3-hop max via X-Hail-Forward-Hops, refuse targets on HAIL_MAIL_BASE_DOMAIN, per-domain rate cap (default 200/hour, override via forward_rate_per_hour).

Webhooks

  • Org-wide webhook_subscriptions (multi-event firehose) + per-domain email_domains.webhook_url (single-target ergonomic). Both fire independently.
  • Background WebhookWorker polls webhook_deliveries with FOR UPDATE SKIP LOCKED, signs (X-Hail-Signature: t=<unix>,v1=<hmac-sha256>), POSTs via httpx with a private-network guard.
  • Retry ladder: 0s, 30s, 2m, 10m, 1h, 6h, 24h. After the last slot the row is dead; 50 consecutive deads auto-disable the subscription.
  • POST /webhooks/{id}/deliveries/{did}/redeliver to replay a dead row.
  • Webhook secrets are Fernet-encrypted at rest (HAIL_WEBHOOK_SECRET_KEY), so the worker survives an API restart without secret rotation.

Schema rename: sender_domains β†’ email_domains

  • The table now carries both directions, so the historical "sender" name no longer fits. Renames the table, indexes, trigger, CHECK constraints, and the FK column on emails.
  • Wire format: routes moved from /sender-domains β†’ /email-domains; audit-action strings from sender_domain.* β†’ email_domain.*; pydantic schemas SenderDomain* β†’ EmailDomain*.

API

  • GET /emails?direction=inbound|outbound filter.
  • GET /emails/{id}/raw and GET /emails/{id}/attachments/{aid} β€” 302 redirect to presigned S3 URLs (5-min TTL).
  • PATCH /email-domains/{id} accepts inbound action fields (inbound_enabled, forward_to, webhook_url, forward_rate_per_hour). Setting webhook_url mints a secret returned once.
  • POST /email-domains/{id}/rotate-webhook-secret rotates and returns the new plaintext once.
  • Full /webhooks CRUD + /rotate-secret + /deliveries + /deliveries/{id}/redeliver.

CLI (cli-v0.5.0)

  • New hail webhooks subcommand tree (create, list, deliveries, redeliver).
  • hail email list --direction inbound filter.
  • hail email sender-domain renamed to hail email email-domain (alias ed); the legacy sd / sender-domain aliases are removed β€” breaking for users with scripts hitting the old name.
  • Short-ID prefix resolution on every UUID-taking subcommand (was call status only): email get, email-domain get/verify/delete, webhooks deliveries, webhooks redeliver all accept the 4+ char hex prefix shown in list output, mirroring git rev-parse's short-hash UX.

SDK (sdk-v0.3.0)

  • client.webhooks.* resource added.
  • client.emails.list(direction=...) parameter added.
  • client.email_domains.patch() accepts inbound action fields; client.email_domains.rotate_webhook_secret(id) added.
  • SenderDomain* back-compat aliases removed (deprecated since sdk-v0.2.0) β€” breaking for users importing the legacy names.

Infrastructure

  • Terraform module under infra/terraform/ provisions S3 (raw + attachments + lifecycle), SES Receipt Rule + Rule Set, Lambda + IAM + log group, and emits the MX record + the set-active-receipt-rule-set activation command.
  • Stdlib-only infra/ses-ingest-lambda/handler.py bridges SES events into the HMAC-signed POST.
  • Three migrations: 0006 (rename + auxiliary objects), 0007 (Email inbound columns + email_attachments + email_domains action columns + idempotency partial unique index), 0008 (webhook_subscriptions + webhook_deliveries). See docs/operations.md β†’ "Inbound email rollout" for the staged deploy sequence.
  • InboundProvider interface in core/hailhq/core/providers/email/inbound/ with SesInboundProvider shipping and SmtpInboundProvider stubbed for a future cloud-agnostic milestone.
  • Operator-facing env vars (HAIL_INBOUND_EMAIL_NAME_PREFIX, HAIL_IAM_USER_NAME) are required in .env β€” terragrunt fails fast on missing values instead of silently using a hardcoded default. The inbound bucket name is now a single source of truth (Settings.hail_inbound_bucket is a computed ${prefix}-raw), so Terraform and the API can't drift.

Deferred to next milestone

  • Custom-domain inbound (tenant MX β†’ SES) β€” schema is ready; the operator-side flow isn't.
  • inbound_routes table for per-mailbox routing within custom domains.
  • SmtpInboundProvider implementation (cloud-agnostic / OSS-only path).
  • Reply-composition endpoint (POST /emails/{id}/reply).
  • MCP push transport for inbound events.

0.1.0 β€” 2026-04-30

First public release. Outbound phone calls for AI agents, end-to-end.

Phone calls

  • Outbound calls via Twilio SIP through LiveKit Cloud.
  • POST /calls, GET /calls/{id}, and GET /calls with cursor pagination.
  • Per-org API-key auth with audit logging on every authenticated request.
  • Idempotency-Key support on POST /calls (24h TTL).

Voice pipeline

  • Deepgram STT.
  • Cartesia TTS (primary) with ElevenLabs fallback.
  • Silero VAD (prewarmed once per worker process).
  • LiveKit turn-detector for end-of-utterance detection.
  • LLM system-prompt mode with OpenAI to Gemini to Anthropic fallback chain.
  • LLM BYO-endpoint mode for any OpenAI chat-completions-compatible endpoint.
  • Per-turn call_events rows for transcript reconstruction.

Distribution

  • OpenAPI 3.1 spec at openapi/openapi.yaml as the source of truth.
  • hail CLI binary published via GitHub Releases (darwin and linux, amd64 and arm64) and a Homebrew tap.
  • Remote MCP server bundled with every Hail deploy at /sse, exposing a place_call tool.
  • hail-sdk on PyPI (imports as hail) with Client.calls.create / get / list.

Infrastructure

  • docker compose up brings up api, voicebot, mcp, and Postgres.
  • Alembic migrations for the v1 schema.
  • CI runs lint, pytest with a Postgres service container, Go build and test, and a docker-compose build smoke.

Deferred to v1.x

  • LiveKit Egress recording (recording.py is stubbed; Call.recording_s3_key is always NULL).
  • idempotency_keys GC sweeper and in-flight reaper.
  • Inbound calls (LIVEKIT_SIP_INBOUND_TRUNK_ID is reserved in env but unused).
  • SMS channel (Twilio outbound and inbound).
  • Email channel (AWS SES outbound and inbound).
  • CallEvent dedupe across voicebot redispatch.