Stage 1 — Security & Guardrail Hardening#110
Merged
Merged
Conversation
Nothing on the buyer's money-committing path previously compared the final price to the campaign's limits: max_cpm reached the selection LLM only as prose and the seller only as an advisory search filter, so a seller quoting basePrice 200.0 against a target CPM of 20 was still issued a Deal ID at ~8.5x the ceiling. Add a pure, deterministic guard (enforce_spend_ceiling / SpendCeilingExceeded in ad_buyer.booking.spend_ceiling) and call it at both money-commit points: - RequestDealTool._create_deal_response: the computed final CPM is checked against the buyer's max_cpm BEFORE a Deal ID is minted; a breach returns a structured DEAL REJECTED error. BuyerDealFlow threads state.max_cpm onto the tool deterministically (not via an LLM-visible argument). - DealBookingFlow._execute_bookings: the total cost of LLM-parsed approved recommendations is checked against the campaign budget before any line is booked; a breach rejects the booking and marks the flow FAILED. Missing limits (max_cpm/budget is None) fail open with a warning log — an explicit choice to preserve current demo behavior; a supplied limit is always enforced and there is no flag to disable the guard. MultiSellerOrchestrator is intentionally untouched: evaluate_and_rank already filters by max_cpm and select_and_book already skips quotes whose minimum spend exceeds the remaining budget. bead: ar-70eh Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Event emission was fail-open across the board: emit_event and emit_event_sync swallowed every exception, so audit-trail events for money decisions (deal booked/cancelled, booking submitted, budget allocation, negotiation rounds, approval decisions) could silently drop when the event bus failed. Surgical hotfix, not a bus redesign: - AUDIT_EVENT_TYPES (events/models.py): the money-relevant event types whose loss would break the audit trail. - events/audit_fallback.py: append-only JSONL fallback writer with fsync-on-write; path from settings (audit_fallback_path, default data/audit_fallback.jsonl). - emit_event / emit_event_sync (events/helpers.py): on bus failure for an audit-class event, write the event to the fallback JSONL and continue; if the fallback write also fails, re-raise so the transaction surfaces the error (fail-closed). The fire-and-forget running-loop path in emit_event_sync gets a done-callback that writes the fallback on publish failure (best effort; cannot raise into a caller that already moved on). Non-audit events keep the existing fail-open behavior unchanged. bead: ar-z64h Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emit_event_sync consumes the thread default loop; test_event_bus.py's deprecated asyncio.get_event_loop() then finds none when the files run adjacently. Fresh loop on fixture teardown removes the order dependency. bead: ar-z64h Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPyrrnxpTatTciiNo7eK2y
…n/fix/audit-fail-closed' into release/stage-1-hardening
Sirajmx
added a commit
that referenced
this pull request
Jul 15, 2026
test_spend_ceiling.py's budget-ceiling tests used a bare MagicMock client, which broke once DealBookingFlow's booking path (from the Meta integration work) started reading a real base_url and issuing HTTP calls via _book_via_seller_api. Mock that call the same way the rest of the suite does. Also applies a ruff-format-only fix to helpers.py that was already failing `ruff format --check` on main.
therevoltingx
pushed a commit
that referenced
this pull request
Jul 15, 2026
* fix buyer booking flow with seller * meta ad booking flow using graph api * GAM and Meta Reporting Integration * naming refactor * docs for meta ads and Reporting * fallback allocation if llm fails * Access GAM report via seller agent * lint formatting and unit test fixes * replace cli with api * Fix lint (ruff format + E501 + I001) and CrewAI >=1.14 Flow.__init__ state kwargs * Fix 7 test failures introduced by Meta integration merge - test_deal_library_agent_has_memory / test_linear_tv_agent_has_memory: patch settings.crew_memory_enabled=True inside each test so the assertion holds regardless of the .env default (CREW_MEMORY_ENABLED=false) - test_approve_specific_recommendations / test_approve_all_recommendations: mock _book_via_seller_api — this method now makes real HTTP calls after the Meta integration added it to _execute_bookings; integration tests must not reach out to a live seller URL - test_execute_bookings_no_store / test_execute_bookings_persists_records: same mock for the same reason - test_crew_with_memory_true: skipif crewai[bedrock] not installed locally; crewai[bedrock] is in core deps so CI runs the test normally * Fix DealBookingFlow.__init__ state kwargs for crewai >=1.15 crewai 1.15 restructured Flow as a BaseModel — extra kwargs passed to super().__init__() are silently dropped by pydantic instead of being forwarded to _initialize_state. Fix: call super().__init__() with no kwargs, then apply state_kwargs directly via setattr on self.state. This works across all crewai versions. * fix channel allocation: add social to BudgetAllocationOutput and restore LLM-driven split with equal-split fallback * restore crew_memory_enabled=True to match main branch default * Fix booking mock and ruff format drift exposed by PR #110 merge test_spend_ceiling.py's budget-ceiling tests used a bare MagicMock client, which broke once DealBookingFlow's booking path (from the Meta integration work) started reading a real base_url and issuing HTTP calls via _book_via_seller_api. Mock that call the same way the rest of the suite does. Also applies a ruff-format-only fix to helpers.py that was already failing `ruff format --check` on main. * Fix get_latest_pacing_snapshot to order by insertion time, not business time PacingStore.get_latest_pacing_snapshot() ordered by the timestamp column, which is caller-supplied business/simulated time and not guaranteed to be monotonic with write order. The campaign demo's READY-stage snapshot is stamped with real wall-clock time while its later ACTIVE-stage snapshot uses a simulated "35% through flight" timestamp — once real time passes that simulated point, the stale zero-value READY snapshot outranks the real one, and every reporting consumer (campaign_report.py, mcp_server.py) reads zero spend for an active campaign. created_at already records true insertion order and is the correct column to sort by.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 1 — Security & Guardrail Hardening
First release in a staged re-architecture of the agent. Ships only safety-critical fixes; fully backward-compatible — no API, schema, or configuration changes.
Later stages introduce a shared contract library and structural consolidation (in progress on integration branches).
🤖 Generated with Claude Code