Skip to content

Stage 1 — Security & Guardrail Hardening#110

Merged
atc964 merged 4 commits into
mainfrom
release/stage-1-hardening
Jul 14, 2026
Merged

Stage 1 — Security & Guardrail Hardening#110
atc964 merged 4 commits into
mainfrom
release/stage-1-hardening

Conversation

@atc964

@atc964 atc964 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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.

  • Spend safety: deal booking now enforces a hard, deterministic budget/CPM ceiling — a quote above the campaign's limit is rejected, not booked. Previously the limit reached the model only as prose and the seller only as an advisory filter.
  • Audit integrity: money-relevant events (bookings, negotiation concessions, approvals) can no longer be silently dropped — delivery is fail-closed with a durable fallback log.

Later stages introduce a shared contract library and structural consolidation (in progress on integration branches).

🤖 Generated with Claude Code

atc964 and others added 4 commits July 13, 2026 20:26
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
@atc964
atc964 merged commit 162fd7f into main Jul 14, 2026
1 of 3 checks passed
@atc964
atc964 deleted the release/stage-1-hardening branch July 14, 2026 16:42
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant