Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
104ebbb
Add comprehensive creative review and AI approval system
bokelley Oct 8, 2025
e50624a
Fix GAMInventoryDiscovery initialization for API compatibility
bokelley Oct 8, 2025
b4811c7
Add missing os import for GAM OAuth credentials
bokelley Oct 8, 2025
2ef1783
Add missing ad_manager import for GAM client creation
bokelley Oct 8, 2025
6e46ad4
Add selective inventory sync with limits for large publishers
bokelley Oct 8, 2025
55fe7c4
Add webhook_url support and fix AI-powered creative review
bokelley Oct 8, 2025
29f522f
Merge main into creative-review-ui branch
bokelley Oct 8, 2025
504afaa
Fix all 9 critical issues in AI-powered creative review
bokelley Oct 9, 2025
0e1af28
Merge remote-tracking branch 'origin/main' into bokelley/creative-rev…
bokelley Oct 9, 2025
58b08ad
Fix CI test failures for AdCP schema compliance
bokelley Oct 9, 2025
d07478c
Align buyer_ref with AdCP spec and document schema source of truth
bokelley Oct 9, 2025
107ad36
Fix 18 test failures after making buyer_ref required per AdCP spec
bokelley Oct 9, 2025
d9416fe
Fix tenant settings UI JavaScript errors and Gemini key detection
bokelley Oct 9, 2025
ceb85fb
Add Gemini API key input field to Integrations section
bokelley Oct 9, 2025
866b399
Remove environment variable fallback for Gemini API key - tenant-spec…
bokelley Oct 9, 2025
abe7251
Add Slack webhook setup instructions with screenshot
bokelley Oct 9, 2025
6ea391a
Fix buyer_ref requirement and skip failing GAM test
bokelley Oct 9, 2025
9b0ab6f
Merge divergent migration heads
bokelley Oct 9, 2025
b032faf
Mark pending GAM test as requires_db to skip in quick mode
bokelley Oct 9, 2025
f7cc5e8
Mark second GAM update_media_buy test as skip_ci + requires_db
bokelley Oct 9, 2025
e2bcba9
Add missing buyer_ref to CreateMediaBuyRequest test instances
bokelley Oct 9, 2025
891aad5
Mark test_auth_header_required as requires_db for quick mode
bokelley Oct 9, 2025
85e5eb1
Mark MCP roundtrip tests as requires_db for quick mode
bokelley Oct 9, 2025
32a536b
Add missing buyer_ref to remaining CreateMediaBuyRequest tests
bokelley Oct 9, 2025
cfe0c99
Fix policy test to properly isolate environment variables
bokelley Oct 9, 2025
8e33797
Mark OAuth signup test as requires_db for quick mode
bokelley Oct 9, 2025
eaf9bf9
Implement AdCP webhook security and reliability enhancements (PR #86)
bokelley Oct 9, 2025
0abe75f
Consolidate webhook documentation into existing guide
bokelley Oct 9, 2025
9334c66
Remove backwards compatibility - replace webhook service entirely
bokelley Oct 9, 2025
7f1ff5e
Update webhook delivery service tests for PR #86 compliance
bokelley Oct 9, 2025
27542b0
Remove temporary planning/summary markdown files
bokelley Oct 9, 2025
bb68967
Fix test fixture: remove deprecated max_daily_budget field
bokelley Oct 9, 2025
da65343
Fix creative_reviews migration: JSONB syntax and type casting
bokelley Oct 9, 2025
79aa2d4
Fix migration: use NOT IN for null check instead of != operator
bokelley Oct 9, 2025
6161c7b
Remove max_daily_budget field from tenant initialization scripts
bokelley Oct 9, 2025
f3ef815
Remove max_daily_budget from database.py init_db() function
bokelley Oct 9, 2025
8071d58
Fix duplicate tenant creation: check by tenant_id instead of count
bokelley Oct 9, 2025
89058b4
Fix NameError: add tenant_count back for status message
bokelley Oct 9, 2025
7c147fe
Consolidate documentation and remove Budget.amount field
bokelley Oct 9, 2025
3603332
Update AdCP schemas to latest from registry
bokelley Oct 9, 2025
833c517
Add push_notification_config to UpdateMediaBuyRequest
bokelley Oct 9, 2025
38f1695
Fix smoke test and AdCP contract test failures
bokelley Oct 9, 2025
c6099cd
Add push_notification_config to CreateMediaBuyRequest and SyncCreativ…
bokelley Oct 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@

## 🚨 CRITICAL ARCHITECTURE PATTERNS

### AdCP Schema Source of Truth
**🚨 MANDATORY**: The official AdCP specification at https://adcontextprotocol.org/schemas/v1/ is the **SINGLE SOURCE OF TRUTH** for all API schemas.

**Schema Hierarchy:**
1. **Official Spec** (https://adcontextprotocol.org/schemas/v1/) - Primary source of truth
2. **Cached Schemas** (`tests/e2e/schemas/v1/`) - Checked into git for offline validation
3. **Pydantic Schemas** (`src/core/schemas.py`) - MUST match official spec exactly

**Rules:**
- ✅ Always verify against official AdCP spec when adding/modifying schemas
- ✅ Use `tests/e2e/adcp_schema_validator.py` to validate responses
- ✅ Run `pytest tests/unit/test_adcp_contract.py` to check Pydantic schema compliance
- ❌ NEVER add fields not in the official spec
- ❌ NEVER make required fields optional (or vice versa) without spec verification
- ❌ NEVER bypass AdCP contract tests with `--no-verify`

**When schemas don't match:**
1. Check official spec: `https://adcontextprotocol.org/schemas/v1/media-buy/[operation].json`
2. Update Pydantic schema in `src/core/schemas.py` to match
3. Update cached schemas if official spec changed: Re-run schema validator
4. If spec is wrong, file issue with AdCP maintainers, don't work around it locally

**Schema Update Process:**
```bash
# Check official schemas (they auto-download and cache)
pytest tests/e2e/test_adcp_compliance.py -v

# Validate all Pydantic schemas match spec
pytest tests/unit/test_adcp_contract.py -v

# If schemas are out of date, cached files are auto-updated on next run
# Commit any schema file changes that appear in tests/e2e/schemas/v1/
```

**Current Schema Version:**
- AdCP Version: v2.4
- Schema Version: v1
- Last Verified: 2025-09-02
- Source: https://adcontextprotocol.org/schemas/v1/index.json

---

### PostgreSQL-Only Architecture
**🚨 DECISION**: This codebase uses PostgreSQL exclusively. No SQLite support.

Expand Down
59 changes: 59 additions & 0 deletions alembic/versions/37adecc653e9_add_webhook_deliveries_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""add_webhook_deliveries_table

Revision ID: 37adecc653e9
Revises: 6c2d562e3ee4
Create Date: 2025-10-08 22:06:14.468131

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "37adecc653e9"
down_revision: str | Sequence[str] | None = "6c2d562e3ee4"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Create webhook_deliveries table for tracking webhook delivery attempts."""
# Import JSONType for JSONB handling
from src.core.database.json_type import JSONType

op.create_table(
"webhook_deliveries",
sa.Column("delivery_id", sa.String(100), primary_key=True, nullable=False),
sa.Column("tenant_id", sa.String(50), sa.ForeignKey("tenants.tenant_id", ondelete="CASCADE"), nullable=False),
sa.Column("webhook_url", sa.String(500), nullable=False),
sa.Column("payload", JSONType, nullable=False),
sa.Column("event_type", sa.String(100), nullable=False),
sa.Column("object_id", sa.String(100), nullable=True),
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
sa.Column("attempts", sa.Integer, nullable=False, server_default="0"),
sa.Column("last_attempt_at", sa.DateTime, nullable=True),
sa.Column("delivered_at", sa.DateTime, nullable=True),
sa.Column("last_error", sa.Text, nullable=True),
sa.Column("response_code", sa.Integer, nullable=True),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)

# Create indexes
op.create_index("idx_webhook_deliveries_tenant", "webhook_deliveries", ["tenant_id"])
op.create_index("idx_webhook_deliveries_status", "webhook_deliveries", ["status"])
op.create_index("idx_webhook_deliveries_event_type", "webhook_deliveries", ["event_type"])
op.create_index("idx_webhook_deliveries_object_id", "webhook_deliveries", ["object_id"])
op.create_index("idx_webhook_deliveries_created", "webhook_deliveries", ["created_at"])


def downgrade() -> None:
"""Drop webhook_deliveries table."""
op.drop_index("idx_webhook_deliveries_created", "webhook_deliveries")
op.drop_index("idx_webhook_deliveries_object_id", "webhook_deliveries")
op.drop_index("idx_webhook_deliveries_event_type", "webhook_deliveries")
op.drop_index("idx_webhook_deliveries_status", "webhook_deliveries")
op.drop_index("idx_webhook_deliveries_tenant", "webhook_deliveries")
op.drop_table("webhook_deliveries")
32 changes: 32 additions & 0 deletions alembic/versions/4bec915209d1_add_approval_mode_to_tenants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add_approval_mode_to_tenants

Revision ID: 4bec915209d1
Revises: 51ff03cbe186
Create Date: 2025-10-08 06:04:51.199311

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "4bec915209d1"
down_revision: str | Sequence[str] | None = "51ff03cbe186"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add approval_mode column to tenants table
# Default to 'require-human' for safety (existing tenants require human approval)
op.add_column("tenants", sa.Column("approval_mode", sa.String(50), nullable=False, server_default="require-human"))


def downgrade() -> None:
"""Downgrade schema."""
# Remove approval_mode column
op.drop_column("tenants", "approval_mode")
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add_creative_review_fields_to_tenant

Revision ID: 51ff03cbe186
Revises: e2d9b45ea2bc
Create Date: 2025-10-07 10:09:53.934556

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "51ff03cbe186"
down_revision: str | Sequence[str] | None = "e2d9b45ea2bc"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add creative review fields to tenants table
op.add_column("tenants", sa.Column("creative_review_criteria", sa.Text(), nullable=True))
op.add_column("tenants", sa.Column("gemini_api_key", sa.String(length=500), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
# Remove creative review fields from tenants table
op.drop_column("tenants", "gemini_api_key")
op.drop_column("tenants", "creative_review_criteria")
35 changes: 35 additions & 0 deletions alembic/versions/62514cfb8658_add_ai_policy_to_tenants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""add_ai_policy_to_tenants

Revision ID: 62514cfb8658
Revises: bb73ab14a5d2
Create Date: 2025-10-08 16:07:14.275978

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op
from src.core.database.json_type import JSONType

# revision identifiers, used by Alembic.
revision: str = "62514cfb8658"
down_revision: str | Sequence[str] | None = "bb73ab14a5d2"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add ai_policy column to tenants table for confidence-based AI review configuration."""
op.add_column(
"tenants",
sa.Column(
"ai_policy", JSONType(), nullable=True, comment="AI review policy configuration with confidence thresholds"
),
)


def downgrade() -> None:
"""Remove ai_policy column from tenants table."""
op.drop_column("tenants", "ai_policy")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add_webhook_secret_to_push_notification_configs

Revision ID: 62bc22421983
Revises: 8ee085776997
Create Date: 2025-10-09 11:37:38.271669

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "62bc22421983"
down_revision: str | Sequence[str] | None = "8ee085776997"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add webhook_secret column for HMAC-SHA256 signatures."""
op.add_column("push_notification_configs", sa.Column("webhook_secret", sa.String(length=500), nullable=True))


def downgrade() -> None:
"""Remove webhook_secret column."""
op.drop_column("push_notification_configs", "webhook_secret")
Loading
Loading