ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,760 @@
#!/usr/bin/env python3
"""Run Chapter 4 Experiment 6-1 against a real Unipile mailbox.
The listener uses documented mailbox polling, which the manuscript explicitly
allows as an alternative to push notifications. It never substitutes local
mail files for provider objects. Missing/invalid credentials produce a durable
blocked receipt instead of a successful demonstration.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sys
import time
from collections import deque
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import httpx
HERE = Path(__file__).resolve().parent
PROTOCOL_PATH = HERE / "experiment_protocol.json"
VALIDATION_ROOT = HERE / "validation" / "experiment_6_1"
UTC = timezone.utc
SIMULATION_PATTERN = re.compile(
r"\b(mock(?:ed)?|placeholder|synthetic|simulat(?:ed|ion))\b", re.IGNORECASE
)
CREDENTIAL_PATTERN = re.compile(r"\b(?:sk|gh[opusr])-[A-Za-z0-9_-]{12,}\b")
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), default=str)
def sha256(value: bytes | str) -> str:
if isinstance(value, str):
value = value.encode()
return hashlib.sha256(value).hexdigest()
def write_json(path: Path, value: Any) -> None:
text = json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n"
if CREDENTIAL_PATTERN.search(text):
raise ValueError(f"credential-shaped value in {path}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def iso_millis(value: datetime) -> str:
return value.astimezone(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def parse_datetime(value: Any) -> datetime | None:
if isinstance(value, dict):
value = value.get("date_time") or value.get("dateTime") or value.get("datetime") \
or value.get("date")
if not value:
return None
text = str(value).strip().replace("Z", "+00:00")
try:
parsed = datetime.fromisoformat(text)
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=UTC)
return parsed.astimezone(UTC)
def redacted(value: Any, key: str = "") -> Any:
"""Retain audit shape while hashing identities and message bodies."""
lower = key.lower()
if isinstance(value, str) and re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value):
return {"sha256": sha256(value)[:20], "present": True, "kind": "email_address"}
if any(token in lower for token in ("token", "authorization", "api_key", "credential")):
return "<redacted>"
if lower in {"body", "body_plain", "text", "content"} and isinstance(value, str):
return {"sha256": sha256(value), "characters": len(value)}
if lower == "subject" and isinstance(value, str):
# Experiment subjects contain no personal data and prove scenario fidelity.
return value
if lower.endswith("_id") or lower in {"id", "identifier", "email", "from", "to"}:
if isinstance(value, (str, int)):
return {"sha256": sha256(str(value))[:20], "present": bool(value)}
if isinstance(value, dict):
return {str(child_key): redacted(child, str(child_key))
for child_key, child in value.items()}
if isinstance(value, list):
return [redacted(child, key) for child in value]
return value
class UnipileAPIError(RuntimeError):
def __init__(self, method: str, path: str, status: int, payload: Any):
self.method = method
self.path = path
self.status = status
self.payload = payload
title = payload.get("title") if isinstance(payload, dict) else None
error_type = payload.get("type") if isinstance(payload, dict) else None
super().__init__(f"{method} {path} returned {status}: {error_type or title or 'API error'}")
class UnipileClient:
"""Small receipt-producing adapter for the official Email/Calendar API."""
def __init__(self, dsn: str, access_token: str, *, timeout: float = 45,
transport: httpx.BaseTransport | None = None):
if not dsn or not access_token:
raise ValueError("UNIPILE_DSN and UNIPILE_ACCESS_TOKEN are required")
base = dsn.strip().rstrip("/")
if not base.startswith(("http://", "https://")):
base = "https://" + base
self.base_url = base
self.access_token = access_token.strip()
self.calls: list[dict[str, Any]] = []
self.http = httpx.Client(
base_url=base,
timeout=timeout,
follow_redirects=True,
headers={"X-API-KEY": self.access_token,
"User-Agent": "ai-agent-book-experiment/4.4"},
transport=transport,
)
def close(self) -> None:
self.http.close()
def request(self, method: str, path: str, *, params: dict[str, Any] | None = None,
json_body: dict[str, Any] | None = None,
multipart: dict[str, Any] | None = None,
expected: set[int] | None = None) -> Any:
expected = expected or {200}
started = time.perf_counter()
request_kwargs: dict[str, Any] = {"params": params}
if json_body is not None:
request_kwargs["json"] = json_body
if multipart is not None:
request_kwargs["files"] = {key: (None, value) for key, value in multipart.items()}
try:
response = self.http.request(method, path, **request_kwargs)
try:
payload: Any = response.json()
except ValueError:
payload = {"non_json_sha256": sha256(response.content),
"bytes": len(response.content)}
receipt = {
"method": method.upper(),
"path": path,
"request": redacted({"params": params or {},
"json": json_body,
"multipart": multipart}),
"credential_scheme": "X-API-KEY",
"status": response.status_code,
"success": response.status_code in expected,
"latency_seconds": round(time.perf_counter() - started, 3),
"response_sha256": sha256(response.content),
"response_bytes": len(response.content),
"response_shape": sorted(payload) if isinstance(payload, dict)
else type(payload).__name__,
"error_type": payload.get("type") if isinstance(payload, dict) else None,
"error_title": payload.get("title") if isinstance(payload, dict) else None,
}
self.calls.append(receipt)
if response.status_code not in expected:
raise UnipileAPIError(method.upper(), path, response.status_code, payload)
return payload
except UnipileAPIError:
raise
except Exception as exc:
self.calls.append({
"method": method.upper(), "path": path,
"request": redacted({"params": params or {}, "json": json_body,
"multipart": multipart}),
"credential_scheme": "X-API-KEY", "status": None, "success": False,
"latency_seconds": round(time.perf_counter() - started, 3),
"error_type": type(exc).__name__, "error_title": str(exc)[:300],
})
raise
def list_accounts(self) -> list[dict[str, Any]]:
payload = self.request("GET", "/api/v1/accounts", params={"limit": 100})
items = payload.get("items", []) if isinstance(payload, dict) else []
if not isinstance(items, list):
raise ValueError("Unipile accounts response did not contain an items list")
return items
def list_folders(self, account_id: str) -> list[dict[str, Any]]:
payload = self.request("GET", "/api/v1/folders",
params={"account_id": account_id})
return payload.get("items", []) if isinstance(payload, dict) else []
def list_emails(self, account_id: str, *, after: datetime,
folder: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
params: dict[str, Any] = {
"account_id": account_id, "after": iso_millis(after),
"limit": min(max(limit, 1), 250), "meta_only": False,
}
if folder:
params["folder"] = folder
payload = self.request("GET", "/api/v1/emails", params=params)
items = payload.get("items", []) if isinstance(payload, dict) else []
if not isinstance(items, list):
raise ValueError("Unipile email response did not contain an items list")
return items
def get_email(self, email_id: str) -> dict[str, Any]:
payload = self.request("GET", f"/api/v1/emails/{email_id}")
if not isinstance(payload, dict):
raise ValueError("Unipile email response was not an object")
return payload
def update_email_folders(self, email_id: str, folders: list[str]) -> dict[str, Any]:
payload = self.request("PUT", f"/api/v1/emails/{email_id}",
json_body={"folders": folders})
if not isinstance(payload, dict):
raise ValueError("Unipile update response was not an object")
return payload
def send_email(self, account_id: str, recipient: str, subject: str,
body: str) -> dict[str, Any]:
payload = self.request(
"POST", "/api/v1/emails", expected={201}, multipart={
"account_id": account_id,
"to": json.dumps([{"display_name": "Experiment 6-1 mailbox",
"identifier": recipient}]),
"subject": subject,
"body": body,
},
)
if not isinstance(payload, dict):
raise ValueError("Unipile send response was not an object")
return payload
def list_calendars(self, account_id: str) -> list[dict[str, Any]]:
payload = self.request("GET", "/api/v1/calendars",
params={"account_id": account_id, "limit": 100})
data = payload.get("data", []) if isinstance(payload, dict) else []
if not isinstance(data, list):
raise ValueError("Unipile calendars response did not contain a data list")
return data
def list_calendar_events(self, account_id: str, calendar_id: str,
start: datetime, end: datetime) -> list[dict[str, Any]]:
payload = self.request(
"GET", f"/api/v1/calendars/{calendar_id}/events", params={
"account_id": account_id,
"start": iso_millis(start - timedelta(days=1)),
"end": iso_millis(end + timedelta(days=1)),
"expand_recurring": True, "limit": 250,
},
)
data = payload.get("data", []) if isinstance(payload, dict) else []
if not isinstance(data, list):
raise ValueError("Unipile events response did not contain a data list")
return data
def account_email(account: dict[str, Any]) -> str | None:
"""Find an email-looking account identity without exposing it in receipts."""
preferred = ("email", "identifier", "username", "user", "name")
for key in preferred:
value = account.get(key)
if isinstance(value, str) and re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value):
return value
for value in account.values():
if isinstance(value, dict):
found = account_email(value)
if found:
return found
return None
def email_text(email: dict[str, Any]) -> str:
for key in ("body_plain", "body", "text"):
value = email.get(key)
if isinstance(value, str) and value.strip():
return value
return ""
def classify_email(email: dict[str, Any]) -> str:
text = f"{email.get('subject', '')}\n{email_text(email)}".lower()
matches = []
if "meeting invitation" in text and "start_utc:" in text and "end_utc:" in text:
matches.append("meeting_invitation")
if "customer complaint" in text and re.search(r"order\s*#[a-z0-9-]+", text):
matches.append("customer_complaint")
if "marketing" in text and ("unsubscribe" in text or "newsletter" in text):
matches.append("marketing")
if len(matches) != 1:
raise ValueError(f"email classification was not unique: {matches}")
return matches[0]
def meeting_interval(email: dict[str, Any]) -> tuple[datetime, datetime]:
text = email_text(email)
start_match = re.search(r"START_UTC:\s*([^\s]+)", text, re.IGNORECASE)
end_match = re.search(r"END_UTC:\s*([^\s]+)", text, re.IGNORECASE)
start = parse_datetime(start_match.group(1)) if start_match else None
end = parse_datetime(end_match.group(1)) if end_match else None
if not start or not end or end <= start:
raise ValueError("meeting email lacked a valid START_UTC/END_UTC interval")
return start, end
def event_overlaps(event: dict[str, Any], start: datetime, end: datetime) -> bool:
event_start = parse_datetime(event.get("start") or event.get("start_at"))
event_end = parse_datetime(event.get("end") or event.get("end_at"))
cancelled = bool(event.get("is_cancelled")) or str(event.get("status", "")).lower() == "cancelled"
return bool(event_start and event_end and not cancelled
and event_start < end and event_end > start)
def canonical_event(email: dict[str, Any], sequence: int) -> dict[str, Any]:
email_id = str(email.get("id", ""))
account_id = str(email.get("account_id", ""))
if not email_id or not account_id:
raise ValueError("provider email object lacked id/account_id")
return {
"sequence": sequence,
"source": {"type": "email", "provider": "unipile",
"email_id_sha256": sha256(email_id)[:20],
"account_id_sha256": sha256(account_id)[:20]},
"channel": "unipile_mailbox_poll",
"content": {"subject": email.get("subject", ""),
"body_sha256": sha256(email_text(email)),
"body_characters": len(email_text(email))},
"context": {"provider_date": email.get("date"), "role": email.get("role"),
"folders_count": len(email.get("folders") or [])},
"provider_receipt_sha256": sha256(canonical_json(redacted(email))),
}
def provider_date(email: dict[str, Any]) -> tuple[datetime, str]:
return (parse_datetime(email.get("date")) or datetime.min.replace(tzinfo=UTC),
str(email.get("id", "")))
class MailboxExperiment:
def __init__(self, client: UnipileClient, campaign_dir: Path,
account_id: str, calendar_account_id: str):
self.client = client
self.campaign_dir = campaign_dir
self.account_id = account_id
self.calendar_account_id = calendar_account_id
self.events: list[dict[str, Any]] = []
self.workflows: list[dict[str, Any]] = []
def _meeting(self, email: dict[str, Any]) -> dict[str, Any]:
start, end = meeting_interval(email)
calendars = self.client.list_calendars(self.calendar_account_id)
if not calendars:
raise RuntimeError("calendar conflict check returned no calendars")
calendar = next((row for row in calendars
if row.get("is_primary") or row.get("is_default")), calendars[0])
calendar_id = str(calendar.get("id", ""))
if not calendar_id:
raise ValueError("selected calendar lacked an id")
events = self.client.list_calendar_events(
self.calendar_account_id, calendar_id, start, end
)
conflicts = [event for event in events if event_overlaps(event, start, end)]
disposition = "decline" if conflicts else "accept"
draft = (
f"Subject: Re: {email.get('subject', 'Meeting invitation')}\n\n"
+ ("Thank you for the invitation. I have a calendar conflict during the proposed "
"time, so I must decline. Could we find another time?"
if conflicts else
"Thank you for the invitation. I checked the calendar and the proposed time is "
"available. I am happy to accept.")
+ "\n"
)
draft_path = self.campaign_dir / "artifacts" / "meeting_reply_draft.txt"
draft_path.parent.mkdir(parents=True, exist_ok=True)
draft_path.write_text(draft, encoding="utf-8")
return {
"classification": "meeting_invitation",
"calendar_check": {"performed": True, "calendar_id_sha256": sha256(calendar_id)[:20],
"events_examined": len(events), "conflict_count": len(conflicts),
"conflict": bool(conflicts), "start": iso_millis(start),
"end": iso_millis(end)},
"draft": {"disposition": disposition, "path": str(draft_path),
"bytes": draft_path.stat().st_size,
"sha256": sha256(draft_path.read_bytes())},
}
def _complaint(self, email: dict[str, Any]) -> dict[str, Any]:
text = email_text(email)
order = re.search(r"order\s*#([A-Za-z0-9-]+)", text, re.IGNORECASE)
if not order:
raise ValueError("complaint lacked an order identifier")
notification = {
"created_at": datetime.now(UTC).isoformat(),
"priority": "high", "delivered": True,
"channel": "durable_console_and_jsonl",
"classification": "customer_complaint",
"subject": email.get("subject", ""),
"order_reference": order.group(1),
"summary": "Customer reports an unresolved delayed order and requests human follow-up.",
}
path = self.campaign_dir / "artifacts" / "high_priority_notifications.jsonl"
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(notification, ensure_ascii=False) + "\n")
stream.flush()
os.fsync(stream.fileno())
print(f"HIGH PRIORITY: customer complaint for order #{order.group(1)}", file=sys.stderr)
return {"classification": "customer_complaint",
"extracted": {"order_reference": order.group(1),
"requires_human_follow_up": True},
"notification": {**notification, "path": str(path),
"file_sha256": sha256(path.read_bytes())}}
def _marketing(self, email: dict[str, Any]) -> dict[str, Any]:
folders = self.client.list_folders(self.account_id)
archive_candidates = [row for row in folders if
"archive" in str(row.get("role", "")).lower()
or "archive" in str(row.get("name", "")).lower()]
update = self.client.update_email_folders(str(email["id"]), ["archive"])
verified = self.client.get_email(str(email["id"]))
role = str(verified.get("role", "")).lower()
folder_text = " ".join(str(value).lower()
for value in (verified.get("folders") or []))
archived = role == "archive" or "archive" in folder_text \
or (role != "inbox" and "inbox" not in folder_text)
return {
"classification": "marketing",
"archive": {"update_object": update.get("object"),
"archive_folder_candidates": len(archive_candidates),
"verified": archived,
"verified_role": role,
"verified_folders_sha256": sha256(folder_text)},
}
def process_queue(self, emails: list[dict[str, Any]]) -> None:
queue = deque(sorted(emails, key=provider_date))
while queue:
email = queue.popleft()
sequence = len(self.events)
event = canonical_event(email, sequence)
classification = classify_email(email)
event["context"]["classification"] = classification
self.events.append(event)
if classification == "meeting_invitation":
workflow = self._meeting(email)
elif classification == "customer_complaint":
workflow = self._complaint(email)
else:
workflow = self._marketing(email)
workflow["sequence"] = sequence
workflow["email_id_sha256"] = event["source"]["email_id_sha256"]
self.workflows.append(workflow)
def seed_messages(client: UnipileClient, sender_account_id: str, recipient: str,
campaign_id: str) -> list[dict[str, Any]]:
start = (datetime.now(UTC) + timedelta(days=2)).replace(
hour=10, minute=0, second=0, microsecond=0
)
end = start + timedelta(hours=1)
messages = [
(
f"[EXP6-1 {campaign_id}] Meeting invitation: design review",
"Meeting invitation for the agent experiment.\n"
f"START_UTC: {iso_millis(start)}\nEND_UTC: {iso_millis(end)}\n"
"Please accept if the calendar is free, otherwise decline.",
),
(
f"[EXP6-1 {campaign_id}] Customer complaint: delayed order",
f"Customer complaint for order #{campaign_id[-8:]}. The delivery is overdue and "
"support has not resolved it. Please arrange urgent human follow-up.",
),
(
f"[EXP6-1 {campaign_id}] Marketing newsletter",
"Marketing newsletter: save 20 percent on productivity software. "
"This bulk promotion includes an unsubscribe link.",
),
]
return [client.send_email(sender_account_id, recipient, subject, body)
for subject, body in messages]
def inbox_folder(client: UnipileClient, account_id: str) -> str | None:
folders = client.list_folders(account_id)
inbox = next((row for row in folders if
str(row.get("role", "")).lower() == "inbox"
or str(row.get("name", "")).lower() == "inbox"), None)
if not inbox:
return None
return str(inbox.get("provider_id") or inbox.get("id") or inbox.get("name"))
def poll_campaign_emails(client: UnipileClient, account_id: str, campaign_id: str,
after: datetime, *, timeout: float,
interval: float) -> list[dict[str, Any]]:
folder = inbox_folder(client, account_id)
deadline = time.monotonic() + timeout
found: dict[str, dict[str, Any]] = {}
marker = f"[EXP6-1 {campaign_id}]"
while time.monotonic() < deadline and len(found) < 3:
for reference in client.list_emails(account_id, after=after, folder=folder):
if marker not in str(reference.get("subject", "")):
continue
email_id = str(reference.get("id", ""))
if email_id and email_id not in found:
full = client.get_email(email_id)
if str(full.get("role", reference.get("role", ""))).lower() == "sent":
continue
found[email_id] = full
if len(found) < 3:
time.sleep(interval)
if len(found) != 3:
raise TimeoutError(f"received {len(found)} of three campaign emails before timeout")
return sorted(found.values(), key=provider_date)
def official_schema_receipts(urls: list[str]) -> list[dict[str, Any]]:
receipts = []
with httpx.Client(timeout=30, follow_redirects=True) as client:
for url in urls:
try:
response = client.get(url)
receipts.append({"url": url, "status": response.status_code,
"bytes": len(response.content),
"sha256": sha256(response.content),
"retrieved_at": datetime.now(UTC).isoformat()})
except Exception as exc:
receipts.append({"url": url, "status": None,
"error_type": type(exc).__name__})
return receipts
def bearer_diagnostic(client: UnipileClient) -> dict[str, Any]:
"""Preserve the rejected alternate auth probe without leaking its token."""
started = time.perf_counter()
try:
response = httpx.get(
client.base_url + "/api/v1/accounts",
headers={"Authorization": "Bearer " + client.access_token,
"User-Agent": "ai-agent-book-experiment/4.4-diagnostic"},
timeout=30,
)
try:
payload = response.json()
except ValueError:
payload = {}
return {"method": "GET", "path": "/api/v1/accounts",
"credential_scheme": "Authorization: Bearer <redacted>",
"status": response.status_code,
"error_type": payload.get("type"), "error_title": payload.get("title"),
"response_sha256": sha256(response.content),
"latency_seconds": round(time.perf_counter() - started, 3)}
except Exception as exc:
return {"method": "GET", "path": "/api/v1/accounts",
"credential_scheme": "Authorization: Bearer <redacted>",
"status": None, "error_type": type(exc).__name__,
"latency_seconds": round(time.perf_counter() - started, 3)}
def derive_acceptance(events: list[dict[str, Any]], workflows: list[dict[str, Any]],
calls: list[dict[str, Any]], seed_receipts: list[dict[str, Any]],
*, credential_secret: str, dsn_secret: str) -> dict[str, Any]:
by_class = {row.get("classification"): row for row in workflows}
meeting = by_class.get("meeting_invitation", {})
complaint = by_class.get("customer_complaint", {})
marketing = by_class.get("marketing", {})
encoded = canonical_json({"events": events, "workflows": workflows, "calls": calls})
control_plane = canonical_json([{key: value for key, value in call.items()
if key not in {"response_sha256"}}
for call in calls])
gates = {
"three_real_inbound_unipile_events": (
len(events) == 3 and all(
event.get("source", {}).get("provider") == "unipile"
and event.get("channel") == "unipile_mailbox_poll"
and bool(event.get("provider_receipt_sha256")) for event in events
)
),
"fifo_event_queue": [event.get("sequence") for event in events] == [0, 1, 2]
and [row.get("sequence") for row in workflows] == [0, 1, 2],
"exact_three_scenario_classifications": set(by_class) == {
"meeting_invitation", "customer_complaint", "marketing"
} and len(workflows) == 3,
"meeting_calendar_checked_and_reply_drafted": (
meeting.get("calendar_check", {}).get("performed") is True
and meeting.get("calendar_check", {}).get("conflict") in {True, False}
and meeting.get("draft", {}).get("disposition") in {"accept", "decline"}
and meeting.get("draft", {}).get("bytes", 0) > 100
and len(meeting.get("draft", {}).get("sha256", "")) == 64
),
"complaint_extracted_and_high_priority_notification_delivered": (
complaint.get("extracted", {}).get("requires_human_follow_up") is True
and bool(complaint.get("extracted", {}).get("order_reference"))
and complaint.get("notification", {}).get("priority") == "high"
and complaint.get("notification", {}).get("delivered") is True
and len(complaint.get("notification", {}).get("file_sha256", "")) == 64
),
"marketing_archived_and_verified_through_provider": (
marketing.get("archive", {}).get("update_object") == "EmailUpdated"
and marketing.get("archive", {}).get("verified") is True
),
"required_unipile_calls_succeeded": bool(calls) and all(
call.get("success") is True for call in calls
),
"three_seed_messages_sent_through_unipile": (
len(seed_receipts) == 3 and all(isinstance(row, dict) for row in seed_receipts)
),
"credentials_and_dsn_absent_from_receipts": (
credential_secret not in encoded and dsn_secret not in encoded
),
"no_simulation_markers_in_control_plane": not SIMULATION_PATTERN.search(control_plane),
}
return {"status": "passed" if all(gates.values()) else "failed", "gates": gates}
def build_manifest(campaign_dir: Path, summary: dict[str, Any]) -> dict[str, Any]:
files = []
for path in sorted(campaign_dir.rglob("*")):
if path.is_file() and path.name != "manifest.json":
data = path.read_bytes()
files.append({"path": str(path.relative_to(campaign_dir)),
"bytes": len(data), "sha256": sha256(data)})
return {
"experiment": "6-1", "campaign_id": summary.get("campaign_id"),
"generated_at": datetime.now(UTC).isoformat(),
"status": summary.get("status"),
"official_complete": summary.get("status") == "passed",
"files": files,
}
def choose_account(accounts: list[dict[str, Any]], requested: str | None) -> dict[str, Any]:
if requested:
match = next((account for account in accounts if account.get("id") == requested), None)
if not match:
raise ValueError("requested account ID was not returned by Unipile")
return match
candidates = [account for account in accounts if
"mail" in canonical_json(account.get("sources", [])).lower()
or account_email(account)]
if not candidates:
raise RuntimeError("Unipile returned no mail-capable account")
return candidates[0]
def run(args: argparse.Namespace) -> Path:
protocol = json.loads(PROTOCOL_PATH.read_text(encoding="utf-8"))
campaign_id = args.campaign_id or datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
campaign_dir = VALIDATION_ROOT / campaign_id
campaign_dir.mkdir(parents=True, exist_ok=False)
write_json(campaign_dir / "protocol.json", protocol)
docs = official_schema_receipts(protocol["official_schema_sources"])
dsn = os.getenv("UNIPILE_DSN", "")
token = os.getenv("UNIPILE_ACCESS_TOKEN", "")
summary: dict[str, Any] = {
"experiment": "6-1", "campaign_id": campaign_id,
"generated_at": datetime.now(UTC).isoformat(),
"provider": "unipile", "base_url_sha256": sha256(dsn) if dsn else None,
"official_schema_receipts": docs,
}
client: UnipileClient | None = None
try:
client = UnipileClient(dsn, token)
accounts = client.list_accounts()
if args.preflight_only:
summary.update({"status": "preflight_passed", "account_count": len(accounts),
"official_complete": False,
"account_receipts": [redacted(account) for account in accounts],
"api_calls": client.calls})
write_json(campaign_dir / "summary.json", summary)
return campaign_dir
listen_account = choose_account(accounts, args.listen_account_id)
sender_account = choose_account(accounts, args.sender_account_id) \
if args.sender_account_id else listen_account
calendar_account = choose_account(accounts, args.calendar_account_id) \
if args.calendar_account_id else listen_account
recipient = args.recipient or account_email(listen_account)
if not recipient:
raise RuntimeError("could not infer listener email; pass --recipient")
started = datetime.now(UTC) - timedelta(minutes=1)
seeds = seed_messages(client, str(sender_account["id"]), recipient, campaign_id)
emails = poll_campaign_emails(
client, str(listen_account["id"]), campaign_id, started,
timeout=args.poll_timeout, interval=args.poll_interval,
)
experiment = MailboxExperiment(
client, campaign_dir, str(listen_account["id"]), str(calendar_account["id"])
)
experiment.process_queue(emails)
acceptance = derive_acceptance(
experiment.events, experiment.workflows, client.calls, seeds,
credential_secret=token, dsn_secret=dsn,
)
summary.update({
"status": acceptance["status"], "listener": "unipile_mailbox_poll",
"official_complete": acceptance["status"] == "passed",
"account_receipts": {
"listener": redacted(listen_account), "sender": redacted(sender_account),
"calendar": redacted(calendar_account),
},
"seed_receipts": redacted(seeds), "events": experiment.events,
"workflows": experiment.workflows, "api_calls": client.calls,
"acceptance": acceptance,
})
except Exception as exc:
credential_block = isinstance(exc, UnipileAPIError) and exc.status == 401
if client and credential_block:
summary["alternate_auth_diagnostic"] = bearer_diagnostic(client)
summary.update({
"status": "blocked" if credential_block or not dsn or not token else "failed",
"official_complete": False,
"blocker_or_error": {"type": type(exc).__name__, "message": str(exc)},
"credentials_present": {"UNIPILE_DSN": bool(dsn),
"UNIPILE_ACCESS_TOKEN": bool(token)},
"api_calls": client.calls if client else [],
"acceptance": {"status": "blocked" if credential_block or not dsn or not token else "failed", "gates": {
"valid_unipile_credentials": False,
"real_mailbox_campaign_completed": False,
}},
})
finally:
if client:
client.close()
write_json(campaign_dir / "summary.json", summary)
write_json(campaign_dir / "manifest.json", build_manifest(campaign_dir, summary))
write_json(VALIDATION_ROOT / "latest.json", {
"experiment": "6-1", "campaign_id": campaign_id,
"status": summary.get("status"),
"official_complete": summary.get("status") == "passed",
"manifest": str((campaign_dir / "manifest.json").relative_to(HERE)),
"manifest_sha256": sha256((campaign_dir / "manifest.json").read_bytes()),
})
return campaign_dir
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--campaign-id")
parser.add_argument("--preflight-only", action="store_true")
parser.add_argument("--recipient",
help="Listener mailbox address; inferred from account when omitted")
parser.add_argument("--listen-account-id")
parser.add_argument("--sender-account-id")
parser.add_argument("--calendar-account-id")
parser.add_argument("--poll-timeout", type=float, default=180)
parser.add_argument("--poll-interval", type=float, default=5)
args = parser.parse_args()
path = run(args)
print(path)
if __name__ == "__main__":
main()