replylayer 0.22.1__tar.gz → 0.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {replylayer-0.22.1 → replylayer-0.23.0}/PKG-INFO +8 -8
  2. {replylayer-0.22.1 → replylayer-0.23.0}/README.md +7 -7
  3. {replylayer-0.22.1 → replylayer-0.23.0}/pyproject.toml +1 -1
  4. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/__init__.py +1 -1
  5. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/_http.py +1 -1
  6. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/mailboxes.py +17 -13
  7. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/types.py +26 -24
  8. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_hitl_review_types.py +1 -1
  9. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_instruction_trust.py +1 -1
  10. {replylayer-0.22.1 → replylayer-0.23.0}/.gitignore +0 -0
  11. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/__main__.py +0 -0
  12. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/_client.py +0 -0
  13. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/_pagination.py +0 -0
  14. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/errors.py +0 -0
  15. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/py.typed +0 -0
  16. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/__init__.py +0 -0
  17. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/account.py +0 -0
  18. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/api_keys.py +0 -0
  19. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/attachments.py +0 -0
  20. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/domains.py +0 -0
  21. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/drafts.py +0 -0
  22. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/health.py +0 -0
  23. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/inbound_blocklist.py +0 -0
  24. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/legal_holds.py +0 -0
  25. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/messages.py +0 -0
  26. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/recipients.py +0 -0
  27. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/suppressions.py +0 -0
  28. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/threads.py +0 -0
  29. {replylayer-0.22.1 → replylayer-0.23.0}/replylayer/resources/webhooks.py +0 -0
  30. {replylayer-0.22.1 → replylayer-0.23.0}/tests/__init__.py +0 -0
  31. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_async.py +0 -0
  32. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_attachments.py +0 -0
  33. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_client.py +0 -0
  34. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_domains.py +0 -0
  35. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_drafts.py +0 -0
  36. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_governed_email_effect.py +0 -0
  37. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_http.py +0 -0
  38. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_messages_idempotency.py +0 -0
  39. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_readme_resource_parity.py +0 -0
  40. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_resources.py +0 -0
  41. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_threads.py +0 -0
  42. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_version.py +0 -0
  43. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_web_risk_types.py +0 -0
  44. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_webhooks.py +0 -0
  45. {replylayer-0.22.1 → replylayer-0.23.0}/tests/test_ws1_ws6.py +0 -0
  46. {replylayer-0.22.1 → replylayer-0.23.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replylayer
3
- Version: 0.22.1
3
+ Version: 0.23.0
4
4
  Summary: Official Python SDK for ReplyLayer — email for AI agents
5
5
  Project-URL: Homepage, https://replylayer.ai
6
6
  Project-URL: Repository, https://github.com/replylayer/rly
@@ -187,12 +187,12 @@ if draft["worst_decision"] == "allow":
187
187
  print(f"Sent {result['message_id']}")
188
188
  ```
189
189
 
190
- The send/reply/draft-send response carries two additive, nullable keys that explain a held send inline (no second `messages.get` call). `result["scan"]` is the vendor-neutral scanner verdict (`ScanSummary`); `result["hold_context"]` (`{"trigger_source", "summary_reasons"}` or `None`) is the policy/HITL reason, non-null only when the delivery `status` diverges from `scan["verdict"]` because of a policy/HITL hold — a clean scan held for review by your mailbox policy, or a scanner review-flag held as quarantine on a plan without the review queue (`trigger_source`: `mailbox_policy` | `scanner` | `both`).
190
+ The send/reply/draft-send response carries two additive, nullable keys that explain a held send inline (no second `messages.get` call). `result["scan"]` is the vendor-neutral scanner verdict (`ScanSummary`); `result["hold_context"]` (`{"trigger_source", "summary_reasons"}` or `None`) is the policy/human-review reason, non-null only when the delivery `status` diverges from `scan["verdict"]` because of a policy/human-review hold — a clean scan held for review by your mailbox policy, or a scanner review-flag held as quarantine on a plan without the review queue (`trigger_source`: `mailbox_policy` | `scanner` | `both`).
191
191
 
192
192
  By default `drafts.send()`, `messages.send()`, and `messages.reply()` return only once the scanner verdict is known, with `scan` and `hold_context` inline. Pass `async_dispatch=True` to `drafts.send()` to send the `Prefer: respond-async` hint. **The hint is advisory — the server decides.** When async dispatch is available the server returns `202` with `status="queued_for_dispatch"` (`AsyncSendAck`); otherwise it ignores the hint and returns a normal `200` `SendMessageResponse`. **Always branch on the result**: `result["status"] == "queued_for_dispatch"` ⇒ `AsyncSendAck`, otherwise `SendMessageResponse`. Poll `messages.get(message_id)` (or handle the lifecycle webhook) until `state` is terminal. Attachment-bearing drafts fail closed on the async path (`400 ATTACHMENTS_REQUIRE_SYNC_SEND`). (`messages.wait()` is a mailbox long-poll for new *inbound* mail, not a way to observe a specific message by ID.)
193
193
 
194
194
  The send endpoint raises `ReplyLayerError` with distinct `.code` values on 409:
195
- - `DRAFT_REJECTED_BY_RESCAN` — send-time scan flipped the verdict to `block`/`quarantine`. The draft stays in `draft` state; edit the body and retry. `err.details` carries `scan`, `releasable` (`True` for a `quarantine` hold the customer can release via `POST /v1/drafts/:id/release-and-send`, `False` for a terminal `block`), and, when a policy/HITL decision drove the hold, `hold_context`.
195
+ - `DRAFT_REJECTED_BY_RESCAN` — send-time scan flipped the verdict to `block`/`quarantine`. The draft stays in `draft` state; edit the body and retry. `err.details` carries `scan`, `releasable` (`True` for a `quarantine` hold the customer can release via `POST /v1/drafts/:id/release-and-send`, `False` for a terminal `block`), and, when a policy/human-review decision drove the hold, `hold_context`.
196
196
  - `DRAFT_ALREADY_SENT` — the draft was already sent (race or retry after success).
197
197
 
198
198
  ```python
@@ -387,11 +387,11 @@ Images are a separately confirmed raw-download family. When `allowed_file_famili
387
387
 
388
388
  Human dashboard sessions and admin/pre-scoping keys can download clean stored `metadata_only` attachments, including attachments held back from agent raw-download policy. Agent-role keys remain bound to the mailbox policy gate plus hard safety checks; all callers remain blocked by infected AV verdicts, non-terminal message states, missing stored bytes, and hard attachment blocks.
389
389
 
390
- See ENDPOINTS.md for the full contract and known limitations.
390
+ See the Mailboxes API reference at https://replylayer.ai/docs/api/mailboxes for the full contract and known limitations.
391
391
 
392
392
  ### Recipient allowlist (mailbox containment)
393
393
 
394
- A mailbox is in `blocklist` mode by default — the pre-send gate rejects `suppressed_addresses` hits and allows everyone else. Switching to `allowlist` mode restricts outbound to a pre-approved list; an agent (or a compromised API key) physically cannot email outside the list.
394
+ A mailbox is in `blocklist` mode by default — the pre-send gate rejects `suppressed_addresses` hits and allows everyone else. Switching to `allowlist` mode contains **agent-origin** outbound to a pre-approved list (plus thread participants): a prompt-injected or compromised **agent key** cannot email outside the list. It is a containment boundary against a hijacked agent, not an all-origin lock — a human send (your dashboard session or an admin API key) is not restricted by the allowlist; only your do-not-contact (suppression) list binds a human send.
395
395
 
396
396
  ```python
397
397
  # Populate the allowlist first. Admin-only — agent keys get 403 INSUFFICIENT_SCOPE.
@@ -413,7 +413,7 @@ rl.mailboxes.allowlist.delete(mailbox["id"], "partner@corp.com", force_empty=Tru
413
413
 
414
414
  A send/reply/draft-send to a recipient on your do-not-contact (suppression) list raises `ReplyLayerError` with `.code == "RECIPIENT_SUPPRESSED"` (HTTP 403, `details["reason"] == "suppressed"`). This is terminal — escalate, don't retry; remove the suppression or send to a different recipient.
415
415
 
416
- Allowlist mutations are admin-only — granting send permission to an LLM defeats the containment boundary. Agents *can* `list` (so they can see what they're allowed to email) but not `add`/`add_bulk`/`delete`. Three new webhook events: `recipient_allowlist.added`, `recipient_allowlist.removed`, `mailbox.recipient_policy_changed`.
416
+ Allowlist mutations are admin-only — granting mutation to an LLM defeats the agent-containment boundary. Agents *can* `list` (so they can see what they're allowed to email) but not `add`/`add_bulk`/`delete`. Three new webhook events: `recipient_allowlist.added`, `recipient_allowlist.removed`, `mailbox.recipient_policy_changed`.
417
417
 
418
418
  ### Domain entries
419
419
 
@@ -500,7 +500,7 @@ if ctx and ctx.get("instruction_trust"):
500
500
  # content-safety judgment.
501
501
  print(ctx["guidance"])
502
502
  print(ctx["instruction_trust"])
503
- # {"version": "v1", "match": "address", "verified_domain": ..., "verdict": "verified_aligned", "provenance": "mailgun"}
503
+ # {"version": "v1", "match": "address", "verified_domain": ..., "verdict": "verified_aligned", "provenance": "managed"}
504
504
  ```
505
505
 
506
506
  A human account owner enables the mailbox mode and the key's capability, and designates the trusted sender, from the dashboard (each a loosening change requiring session re-auth). Both the mailbox mode and the per-key capability default to off, so existing integrations are unaffected until a customer opts in. This is a read-path signal only — a copied or hijacked API key cannot self-grant the capability, and there is nothing for a client to set to request it.
@@ -549,7 +549,7 @@ Error classes: `ReplyLayerError` (base), `AuthenticationError` (401), `Forbidden
549
549
 
550
550
  ## Webhook signature verification
551
551
 
552
- > For a full integration guide (event catalog, retry behavior, idempotency, security, troubleshooting), see the hosted webhook docs (coming).
552
+ > For a full integration guide (event catalog, retry behavior, idempotency, security, troubleshooting), see https://replylayer.ai/docs/webhooks.
553
553
 
554
554
  ```python
555
555
  from replylayer import verify_webhook_signature
@@ -167,12 +167,12 @@ if draft["worst_decision"] == "allow":
167
167
  print(f"Sent {result['message_id']}")
168
168
  ```
169
169
 
170
- The send/reply/draft-send response carries two additive, nullable keys that explain a held send inline (no second `messages.get` call). `result["scan"]` is the vendor-neutral scanner verdict (`ScanSummary`); `result["hold_context"]` (`{"trigger_source", "summary_reasons"}` or `None`) is the policy/HITL reason, non-null only when the delivery `status` diverges from `scan["verdict"]` because of a policy/HITL hold — a clean scan held for review by your mailbox policy, or a scanner review-flag held as quarantine on a plan without the review queue (`trigger_source`: `mailbox_policy` | `scanner` | `both`).
170
+ The send/reply/draft-send response carries two additive, nullable keys that explain a held send inline (no second `messages.get` call). `result["scan"]` is the vendor-neutral scanner verdict (`ScanSummary`); `result["hold_context"]` (`{"trigger_source", "summary_reasons"}` or `None`) is the policy/human-review reason, non-null only when the delivery `status` diverges from `scan["verdict"]` because of a policy/human-review hold — a clean scan held for review by your mailbox policy, or a scanner review-flag held as quarantine on a plan without the review queue (`trigger_source`: `mailbox_policy` | `scanner` | `both`).
171
171
 
172
172
  By default `drafts.send()`, `messages.send()`, and `messages.reply()` return only once the scanner verdict is known, with `scan` and `hold_context` inline. Pass `async_dispatch=True` to `drafts.send()` to send the `Prefer: respond-async` hint. **The hint is advisory — the server decides.** When async dispatch is available the server returns `202` with `status="queued_for_dispatch"` (`AsyncSendAck`); otherwise it ignores the hint and returns a normal `200` `SendMessageResponse`. **Always branch on the result**: `result["status"] == "queued_for_dispatch"` ⇒ `AsyncSendAck`, otherwise `SendMessageResponse`. Poll `messages.get(message_id)` (or handle the lifecycle webhook) until `state` is terminal. Attachment-bearing drafts fail closed on the async path (`400 ATTACHMENTS_REQUIRE_SYNC_SEND`). (`messages.wait()` is a mailbox long-poll for new *inbound* mail, not a way to observe a specific message by ID.)
173
173
 
174
174
  The send endpoint raises `ReplyLayerError` with distinct `.code` values on 409:
175
- - `DRAFT_REJECTED_BY_RESCAN` — send-time scan flipped the verdict to `block`/`quarantine`. The draft stays in `draft` state; edit the body and retry. `err.details` carries `scan`, `releasable` (`True` for a `quarantine` hold the customer can release via `POST /v1/drafts/:id/release-and-send`, `False` for a terminal `block`), and, when a policy/HITL decision drove the hold, `hold_context`.
175
+ - `DRAFT_REJECTED_BY_RESCAN` — send-time scan flipped the verdict to `block`/`quarantine`. The draft stays in `draft` state; edit the body and retry. `err.details` carries `scan`, `releasable` (`True` for a `quarantine` hold the customer can release via `POST /v1/drafts/:id/release-and-send`, `False` for a terminal `block`), and, when a policy/human-review decision drove the hold, `hold_context`.
176
176
  - `DRAFT_ALREADY_SENT` — the draft was already sent (race or retry after success).
177
177
 
178
178
  ```python
@@ -367,11 +367,11 @@ Images are a separately confirmed raw-download family. When `allowed_file_famili
367
367
 
368
368
  Human dashboard sessions and admin/pre-scoping keys can download clean stored `metadata_only` attachments, including attachments held back from agent raw-download policy. Agent-role keys remain bound to the mailbox policy gate plus hard safety checks; all callers remain blocked by infected AV verdicts, non-terminal message states, missing stored bytes, and hard attachment blocks.
369
369
 
370
- See ENDPOINTS.md for the full contract and known limitations.
370
+ See the Mailboxes API reference at https://replylayer.ai/docs/api/mailboxes for the full contract and known limitations.
371
371
 
372
372
  ### Recipient allowlist (mailbox containment)
373
373
 
374
- A mailbox is in `blocklist` mode by default — the pre-send gate rejects `suppressed_addresses` hits and allows everyone else. Switching to `allowlist` mode restricts outbound to a pre-approved list; an agent (or a compromised API key) physically cannot email outside the list.
374
+ A mailbox is in `blocklist` mode by default — the pre-send gate rejects `suppressed_addresses` hits and allows everyone else. Switching to `allowlist` mode contains **agent-origin** outbound to a pre-approved list (plus thread participants): a prompt-injected or compromised **agent key** cannot email outside the list. It is a containment boundary against a hijacked agent, not an all-origin lock — a human send (your dashboard session or an admin API key) is not restricted by the allowlist; only your do-not-contact (suppression) list binds a human send.
375
375
 
376
376
  ```python
377
377
  # Populate the allowlist first. Admin-only — agent keys get 403 INSUFFICIENT_SCOPE.
@@ -393,7 +393,7 @@ rl.mailboxes.allowlist.delete(mailbox["id"], "partner@corp.com", force_empty=Tru
393
393
 
394
394
  A send/reply/draft-send to a recipient on your do-not-contact (suppression) list raises `ReplyLayerError` with `.code == "RECIPIENT_SUPPRESSED"` (HTTP 403, `details["reason"] == "suppressed"`). This is terminal — escalate, don't retry; remove the suppression or send to a different recipient.
395
395
 
396
- Allowlist mutations are admin-only — granting send permission to an LLM defeats the containment boundary. Agents *can* `list` (so they can see what they're allowed to email) but not `add`/`add_bulk`/`delete`. Three new webhook events: `recipient_allowlist.added`, `recipient_allowlist.removed`, `mailbox.recipient_policy_changed`.
396
+ Allowlist mutations are admin-only — granting mutation to an LLM defeats the agent-containment boundary. Agents *can* `list` (so they can see what they're allowed to email) but not `add`/`add_bulk`/`delete`. Three new webhook events: `recipient_allowlist.added`, `recipient_allowlist.removed`, `mailbox.recipient_policy_changed`.
397
397
 
398
398
  ### Domain entries
399
399
 
@@ -480,7 +480,7 @@ if ctx and ctx.get("instruction_trust"):
480
480
  # content-safety judgment.
481
481
  print(ctx["guidance"])
482
482
  print(ctx["instruction_trust"])
483
- # {"version": "v1", "match": "address", "verified_domain": ..., "verdict": "verified_aligned", "provenance": "mailgun"}
483
+ # {"version": "v1", "match": "address", "verified_domain": ..., "verdict": "verified_aligned", "provenance": "managed"}
484
484
  ```
485
485
 
486
486
  A human account owner enables the mailbox mode and the key's capability, and designates the trusted sender, from the dashboard (each a loosening change requiring session re-auth). Both the mailbox mode and the per-key capability default to off, so existing integrations are unaffected until a customer opts in. This is a read-path signal only — a copied or hijacked API key cannot self-grant the capability, and there is nothing for a client to set to request it.
@@ -529,7 +529,7 @@ Error classes: `ReplyLayerError` (base), `AuthenticationError` (401), `Forbidden
529
529
 
530
530
  ## Webhook signature verification
531
531
 
532
- > For a full integration guide (event catalog, retry behavior, idempotency, security, troubleshooting), see the hosted webhook docs (coming).
532
+ > For a full integration guide (event catalog, retry behavior, idempotency, security, troubleshooting), see https://replylayer.ai/docs/webhooks.
533
533
 
534
534
  ```python
535
535
  from replylayer import verify_webhook_signature
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "replylayer"
7
- version = "0.22.1"
7
+ version = "0.23.0"
8
8
  description = "Official Python SDK for ReplyLayer — email for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -101,7 +101,7 @@ from .types import (
101
101
  ScannerPolicy,
102
102
  )
103
103
 
104
- __version__ = "0.22.1"
104
+ __version__ = "0.23.0"
105
105
 
106
106
  __all__ = [
107
107
  # Message delete response (0.20.0).
@@ -9,7 +9,7 @@ import httpx
9
9
 
10
10
  from .errors import ReplyLayerError, error_from_response
11
11
 
12
- _VERSION = "0.22.1"
12
+ _VERSION = "0.23.0"
13
13
  _USER_AGENT = f"replylayer-sdk-py/{_VERSION}"
14
14
  _PROTECTED_HEADER_KEYS = frozenset({"authorization", "content-type", "user-agent"})
15
15
 
@@ -464,8 +464,9 @@ class SyncMailboxes:
464
464
  # the field". Mutually exclusive with raw recipient_policy_mode/
465
465
  # agent_send_containment in the same call (the server returns 400).
466
466
  agent_send_policy: AgentSendPolicy | None = None,
467
- # Consent for opening the agent on an allowlist mailbox (also opens
468
- # human sends). None/False omits it.
467
+ # Deprecated + ignored by the server: opening the agent on an allowlist
468
+ # mailbox no longer requires consent (the allowlist only bound the
469
+ # agent). Kept for back-compat. None/False omits it.
469
470
  confirm_open_human_sends: bool | None = None,
470
471
  # PR 8.1 — per-detector redaction visibility. Pass {} to reset to
471
472
  # platform default; pass a partial map (e.g.
@@ -555,11 +556,12 @@ class SyncMailboxes:
555
556
  stored field). ``"open"`` lets the agent send to any new recipient;
556
557
  ``"restricted"`` gates it. Session/admin only server-side.
557
558
 
558
- Opening on an allowlist mailbox also opens human sends, so the server
559
- returns 409 ``OPEN_AGENT_REQUIRES_OPEN_MAILBOX`` (with
560
- ``details.also_opens_human_sends = True``) on the first call. Re-issue
561
- with ``confirm_open_human_sends=True`` to atomically flip the mailbox to
562
- blocklist + containment off.
559
+ Opening on an allowlist mailbox now succeeds directly — the server
560
+ atomically flips the mailbox to blocklist and clears agent containment.
561
+ No consent is required (the allowlist only ever bound the agent, so
562
+ opening it does not affect human sends). ``confirm_open_human_sends`` is
563
+ **deprecated and ignored** by the server; it is still accepted here for
564
+ back-compat.
563
565
  """
564
566
  return self.update(
565
567
  id,
@@ -707,8 +709,9 @@ class AsyncMailboxes:
707
709
  # Mutually exclusive with raw recipient_policy_mode/agent_send_containment
708
710
  # in the same call (the server returns 400).
709
711
  agent_send_policy: AgentSendPolicy | None = None,
710
- # Consent for opening the agent on an allowlist mailbox (also opens
711
- # human sends). None/False omits it.
712
+ # Deprecated + ignored by the server: opening the agent on an allowlist
713
+ # mailbox no longer requires consent (the allowlist only bound the
714
+ # agent). Kept for back-compat. None/False omits it.
712
715
  confirm_open_human_sends: bool | None = None,
713
716
  # PR 8.1 — per-detector redaction visibility. Pass {} to reset to
714
717
  # platform default; pass a partial map to disable redaction on
@@ -796,10 +799,11 @@ class AsyncMailboxes:
796
799
 
797
800
  ``"open"`` lets the agent send to any new recipient; ``"restricted"``
798
801
  gates it. Session/admin only server-side. Opening on an allowlist
799
- mailbox also opens human sends, so the server returns 409
800
- ``OPEN_AGENT_REQUIRES_OPEN_MAILBOX``; re-issue with
801
- ``confirm_open_human_sends=True`` to atomically flip to blocklist +
802
- containment off.
802
+ mailbox now succeeds directly — the server atomically flips the mailbox
803
+ to blocklist and clears agent containment, with no consent required
804
+ (human sends are never allowlist-restricted). ``confirm_open_human_sends``
805
+ is **deprecated and ignored** by the server; still accepted for
806
+ back-compat.
803
807
  """
804
808
  return await self.update(
805
809
  id,
@@ -175,8 +175,8 @@ RecipientPolicyMode = Literal["blocklist", "allowlist"]
175
175
  AgentSendPolicy = Literal["restricted", "open"]
176
176
 
177
177
  # When agent_send_policy == "restricted", WHY:
178
- # - "mailbox_allowlist": the whole mailbox is allowlist (humans too); opening
179
- # the agent would also open human sends.
178
+ # - "mailbox_allowlist": the mailbox is in allowlist mode — the AGENT is
179
+ # restricted to the allowlist; human sends are unaffected (agent-only).
180
180
  # - "agent_containment": blocklist mailbox + the R3 containment overlay.
181
181
  # - None: agent_send_policy == "open".
182
182
  AgentSendRestrictedBy = Literal["mailbox_allowlist", "agent_containment"] | None
@@ -204,7 +204,7 @@ class FirewallBlock(TypedDict):
204
204
  matched_field: Literal["envelope", "from"] | None
205
205
  matched_pattern: str | None
206
206
  reason_code: Literal["SENDER_BLOCKED", "SENDER_NOT_ON_ALLOWLIST"]
207
- source_table: Literal["inbound_sender_blocklists", "inbound_sender_allowlists"] | None
207
+ matched_list: Literal["account_blocklist", "mailbox_allowlist"] | None
208
208
  mode: SenderPolicyMode
209
209
 
210
210
  # Sprint 039 — allowlist + suppression entries may be either an exact email
@@ -413,30 +413,32 @@ class Mailbox(TypedDict):
413
413
  status: MailboxStatus
414
414
  scanner_policy: ScannerPolicy | None
415
415
  pii_mode: PiiMode
416
- # Legacy attachment-access acceptance flag (migration 034). Retained for
417
- # backward-compatible reads and audit history, but effective attachment
418
- # exposure now comes from `attachment_exposure_mode` when set; otherwise
419
- # the mailbox behaves as `metadata_only`.
420
- attachment_access_enabled: bool
421
- attachment_access_accepted_at: str | None
422
- attachment_access_accepted_version: str | None
416
+ # Attachment / outbound-attachment CONSENT bookkeeping. SESSION-ONLY: the
417
+ # API returns these only to a dashboard (session-cookie) caller and strips
418
+ # them from every Bearer / API-key response — which includes this SDK. They
419
+ # drive the human re-acceptance banner and an agent cannot act on them (the
420
+ # acceptance routes are session + re-auth only), so they are NotRequired and,
421
+ # for SDK callers, always absent. Read `attachment_exposure_mode` instead.
422
+ attachment_access_enabled: NotRequired[bool]
423
+ attachment_access_accepted_at: NotRequired[str | None]
424
+ attachment_access_accepted_version: NotRequired[str | None]
423
425
  attachment_exposure_mode: AttachmentExposureMode
424
426
  attachment_allowed_file_families: list[AttachmentAllowedFileFamily]
425
- attachment_reauth_at: str | None
426
- attachment_policy_version: str | None
427
- image_raw_download_confirmed: bool
428
- current_image_risk_version: str
429
- attachment_image_access_accepted_at: str | None
430
- attachment_image_access_accepted_version: str | None
431
- # Always the current disclaimer version. Compare to accepted_version to
432
- # detect when a "disclaimer updated" banner should render.
433
- current_disclaimer_version: str
427
+ attachment_reauth_at: NotRequired[str | None]
428
+ attachment_policy_version: NotRequired[str | None]
429
+ image_raw_download_confirmed: NotRequired[bool]
430
+ current_image_risk_version: NotRequired[str]
431
+ attachment_image_access_accepted_at: NotRequired[str | None]
432
+ attachment_image_access_accepted_version: NotRequired[str | None]
433
+ # The current disclaimer version, for the dashboard re-acceptance banner.
434
+ # Session-only (see the consent note above) — absent on SDK/API-key reads.
435
+ current_disclaimer_version: NotRequired[str]
434
436
  # True when the mailbox is on the legacy compat path
435
437
  # (attachment_exposure_mode is null but attachment_access_enabled is true).
436
438
  # Drives the dashboard re-acceptance banner; flips to false after the
437
- # customer accepts an explicit mode. See
438
- # docs/runbooks/legacy-attachment-access-migration.md.
439
- legacy_wildcard_active: bool
439
+ # customer accepts an explicit mode. Session-only — absent on SDK/API-key
440
+ # reads. See docs/runbooks/legacy-attachment-access-migration.md.
441
+ legacy_wildcard_active: NotRequired[bool]
440
442
  # Migration 035 — default outbound sub-addressing rewrite mode.
441
443
  default_subaddress_mode: SubaddressMode
442
444
  # Migration 036 — recipient policy mode.
@@ -1034,7 +1036,7 @@ class InstructionTrustBasis(TypedDict):
1034
1036
  # Verified sender's domain; None under pii_mode=redacted.
1035
1037
  verified_domain: str | None
1036
1038
  verdict: Literal["verified_aligned"]
1037
- provenance: Literal["mailgun"]
1039
+ provenance: Literal["managed"]
1038
1040
 
1039
1041
 
1040
1042
  class AgentSafetyContext(TypedDict):
@@ -1073,7 +1075,7 @@ class SenderAuthentication(TypedDict):
1073
1075
  verdict: SenderAuthVerdict
1074
1076
  from_domain: str | None
1075
1077
  signing_domain: str | None
1076
- provenance: Literal["mailgun", "self_hosted_imap"]
1078
+ provenance: Literal["managed", "self_hosted_imap"]
1077
1079
 
1078
1080
 
1079
1081
  class SenderAuthenticationCompact(TypedDict):
@@ -45,7 +45,7 @@ def test_review_queued_payload_constructs_with_required_fields() -> None:
45
45
  "direction": "outbound",
46
46
  "subject": "Quarterly review",
47
47
  "recipient": "ceo@corp.example",
48
- "summary_reasons": ["Mailbox requires human approval for outbound mail (hitl_mode=all_outbound)"],
48
+ "summary_reasons": ['This mailbox requires human approval for all outbound mail (review setting: "All outbound").'],
49
49
  "origin": "fresh_send",
50
50
  }
51
51
  assert payload["origin"] == "fresh_send"
@@ -19,7 +19,7 @@ def test_instruction_trust_basis_type_is_exported():
19
19
  "match": "address",
20
20
  "verified_domain": "partner.com",
21
21
  "verdict": "verified_aligned",
22
- "provenance": "mailgun",
22
+ "provenance": "managed",
23
23
  }
24
24
  assert basis["verified_domain"] == "partner.com"
25
25
 
File without changes
File without changes